chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 3 — Assembly Focus Reveal (hyperframes)</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (5 phases, 5 seconds total):
|
||||
0.00 – 0.67s "Just use" assembles in a discrete sequence (with hold)
|
||||
0.73 – 1.23s Pink logo pops in with back.out elastic
|
||||
1.50 – 2.20s "Just use" slides left + fades; container recenters around brand
|
||||
2.67 – 3.57s Camera zooms 5.5× into the logo (scale + counter-translate)
|
||||
3.67 – 5.00s Logo breathes (sine onUpdate, multiplicative on pop scale)
|
||||
|
||||
Key differences :
|
||||
- Single paused GSAP timeline registered to window.__timelines["main"]
|
||||
- brandTextWidth measured via getBoundingClientRect after document.fonts.ready
|
||||
- HERO_FINAL_OFFSET_X derived from real measurement, not estimate
|
||||
- Breathing uses the onUpdate (multiplicative) form so it multiplies
|
||||
onto the pop scale, not a fromTo + yoyo which would overwrite the pop value
|
||||
- Three nested transform layers: zoom-scale → zoom-translate → recenter-shift
|
||||
- Logo is loaded from a static PNG asset
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--text-white: #ffffff;
|
||||
--pink: #e91e63;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
font-family: "Google Sans", "Roboto", Inter, system-ui, sans-serif;
|
||||
color: var(--text-white);
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Three nested transform layers (outer → inner) */
|
||||
.zoom-scale {
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.zoom-translate {
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.recenter-shift {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Companion text — fixed width to prevent assembly jitter */
|
||||
.companion {
|
||||
width: 600px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: 30px;
|
||||
white-space: nowrap;
|
||||
color: var(--text-white);
|
||||
font-size: 140px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Brand group: text + hero icon */
|
||||
.brand-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.brand-text {
|
||||
color: var(--text-white);
|
||||
font-size: 140px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
.hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
/* initial scale(0) set by GSAP fromTo */
|
||||
}
|
||||
.hero .logo-mark {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hidden probe used for text measurement */
|
||||
.measure-probe {
|
||||
position: absolute;
|
||||
left: -99999px;
|
||||
top: -99999px;
|
||||
visibility: hidden;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
class="stage clip"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
id="brand-stage"
|
||||
>
|
||||
<div class="zoom-scale" data-layout-allow-overflow>
|
||||
<div class="zoom-translate">
|
||||
<div class="recenter-shift">
|
||||
<div class="companion">
|
||||
<span class="companion-text">J</span>
|
||||
</div>
|
||||
|
||||
<div class="brand-group" data-layout-allow-overflow>
|
||||
<span class="brand-text">Hyperframes</span>
|
||||
<div class="hero">
|
||||
<!-- Inline-SVG placeholder mark — swap for your logo image -->
|
||||
<svg
|
||||
class="logo-mark"
|
||||
viewBox="0 0 100 100"
|
||||
role="img"
|
||||
aria-label="hyperframes logo"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="hfMark" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#8b7bff" />
|
||||
<stop offset="1" stop-color="#3ddc97" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="92" height="92" rx="22" fill="url(#hfMark)" />
|
||||
<text
|
||||
x="50"
|
||||
y="63"
|
||||
text-anchor="middle"
|
||||
font-family="Inter, system-ui, sans-serif"
|
||||
font-size="40"
|
||||
font-weight="800"
|
||||
fill="#fff"
|
||||
>
|
||||
HF
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
CONSTANTS — composition dimensions baked in.
|
||||
================================================================ */
|
||||
const W = 1920,
|
||||
H = 1080;
|
||||
const TOTAL_DURATION = 5.0;
|
||||
|
||||
const COMPANION_WIDTH = 600;
|
||||
const COMPANION_GAP = 30; // margin-right on .companion
|
||||
const BRAND_FONT_SIZE = 140;
|
||||
const HERO_GAP = 20;
|
||||
const HERO_SIZE = 140;
|
||||
|
||||
const TIMING = {
|
||||
// Phase 1: companion assembly
|
||||
textStart: 0.0,
|
||||
textEnd: 0.67,
|
||||
|
||||
// Phase 2: hero pop
|
||||
popStart: 0.73,
|
||||
popDur: 0.5,
|
||||
|
||||
// Phase 3: slide-out + recenter
|
||||
slideStart: 1.5,
|
||||
slideDur: 0.7,
|
||||
|
||||
// Phase 4: zoom
|
||||
zoomStart: 2.67,
|
||||
zoomDur: 0.9,
|
||||
|
||||
// Phase 5: breathing
|
||||
breathStart: 3.67,
|
||||
};
|
||||
|
||||
const FINAL_RECENTER_OFFSET = -180; // pre-calculated, tuned for visual feel
|
||||
|
||||
/* ================================================================
|
||||
DISCRETE TEXT SEQUENCE for the companion assembly.
|
||||
Converted from the source frame-based sequence to seconds at 30fps.
|
||||
================================================================ */
|
||||
const SEQUENCE = [
|
||||
{ t: 0.0, text: "J" },
|
||||
{ t: 0.07, text: "Jus" },
|
||||
{ t: 0.13, text: "Just" },
|
||||
{ t: 0.27, text: "Just" }, // hold for pacing
|
||||
{ t: 0.4, text: "Just u" },
|
||||
{ t: 0.53, text: "Just us" },
|
||||
{ t: 0.67, text: "Just use" },
|
||||
];
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE — built synchronously so HyperFrames can seek it.
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
{
|
||||
/* Measure the brand text width with a hidden DOM probe. */
|
||||
const probe = document.createElement("span");
|
||||
probe.className = "measure-probe";
|
||||
probe.style.font = `700 ${BRAND_FONT_SIZE}px "Google Sans", "Roboto", Inter, system-ui, sans-serif`;
|
||||
probe.style.whiteSpace = "pre";
|
||||
probe.style.lineHeight = "1";
|
||||
probe.textContent = "Hyperframes"; // MUST match the rendered .brand-text casing
|
||||
document.body.appendChild(probe);
|
||||
const brandTextWidth = probe.getBoundingClientRect().width;
|
||||
probe.remove();
|
||||
|
||||
/* Derive the hero's post-Phase-3 offset from viewport center.
|
||||
baseHeroOffset = (C + G + B + L) / 2 <- heroSize cancels out
|
||||
HERO_FINAL_OFFSET_X = baseHeroOffset + FINAL_RECENTER_OFFSET */
|
||||
const baseHeroOffset = (COMPANION_WIDTH + COMPANION_GAP + brandTextWidth + HERO_GAP) / 2;
|
||||
const HERO_FINAL_OFFSET_X = baseHeroOffset + FINAL_RECENTER_OFFSET;
|
||||
|
||||
/* ============================================================
|
||||
PHASE 1: Discrete companion assembly
|
||||
============================================================ */
|
||||
const textEl = document.querySelector(".companion-text");
|
||||
|
||||
for (const entry of SEQUENCE) {
|
||||
tl.set(textEl, { textContent: entry.text }, entry.t);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASE 2: Hero pops in (elastic)
|
||||
============================================================ */
|
||||
tl.fromTo(
|
||||
".hero",
|
||||
{ scale: 0 },
|
||||
{
|
||||
scale: 1,
|
||||
duration: TIMING.popDur,
|
||||
ease: "back.out(2)", // spring(stiffness:200, damping:12)
|
||||
},
|
||||
TIMING.popStart,
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
PHASE 3: Companion exit + container recenter
|
||||
Two concurrent tweens at the same timeline position.
|
||||
============================================================ */
|
||||
tl.to(
|
||||
".companion",
|
||||
{
|
||||
opacity: 0,
|
||||
x: -80,
|
||||
duration: TIMING.slideDur,
|
||||
ease: "power3.out", // spring(stiffness:100, damping:20)
|
||||
},
|
||||
TIMING.slideStart,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".recenter-shift",
|
||||
{
|
||||
x: FINAL_RECENTER_OFFSET,
|
||||
duration: TIMING.slideDur,
|
||||
ease: "power3.out",
|
||||
},
|
||||
TIMING.slideStart,
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
PHASE 4: Zoom — scale (outer) + counter-translate (middle) + brand text exits.
|
||||
============================================================ */
|
||||
tl.to(
|
||||
".zoom-scale",
|
||||
{
|
||||
scale: 5.5,
|
||||
duration: TIMING.zoomDur,
|
||||
ease: "power2.out", // spring(stiffness:80, damping:20, mass:1.5)
|
||||
},
|
||||
TIMING.zoomStart,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".zoom-translate",
|
||||
{
|
||||
x: -HERO_FINAL_OFFSET_X,
|
||||
y: 0,
|
||||
duration: TIMING.zoomDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.zoomStart,
|
||||
);
|
||||
|
||||
// Brand text fades out + slides left so the logo gets all the zoom space.
|
||||
tl.to(
|
||||
".brand-text",
|
||||
{
|
||||
opacity: 0,
|
||||
x: -600,
|
||||
duration: TIMING.zoomDur * 0.4,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.zoomStart,
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
PHASE 5: Breathing — onUpdate so it MULTIPLIES on the hero's
|
||||
final pop scale, doesn't overwrite it.
|
||||
============================================================ */
|
||||
const heroEl = document.querySelector(".hero");
|
||||
const HERO_FINAL_SCALE = 1.0;
|
||||
const SCALE_PERIOD = 1.5; // seconds per cycle
|
||||
const SCALE_AMP = 0.04;
|
||||
const ROTATE_AMP = 2;
|
||||
|
||||
const breathDur = TOTAL_DURATION - TIMING.breathStart;
|
||||
|
||||
tl.to(
|
||||
{ tick: 0 },
|
||||
{
|
||||
tick: 1,
|
||||
duration: breathDur,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const idleTime = Math.max(0, tl.time() - TIMING.breathStart);
|
||||
const omega = (idleTime / SCALE_PERIOD) * Math.PI * 2;
|
||||
gsap.set(heroEl, {
|
||||
scale: HERO_FINAL_SCALE * (1 + Math.sin(omega) * SCALE_AMP),
|
||||
rotation: Math.sin(omega) * ROTATE_AMP,
|
||||
});
|
||||
},
|
||||
},
|
||||
TIMING.breathStart,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,649 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 09 — Split Comparison Reveal</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (3 phases, 5 seconds total):
|
||||
0.17 – 0.83s Title slides down from top: "Build Video With HyperFrames"
|
||||
0.50 – 1.83s Left card "HTML Composition" enters from left (+16° rotateY)
|
||||
0.83 – 1.83s Right card "Render Pipeline" enters from right (-16° rotateY)
|
||||
1.67 – 2.17s Left pill badge "Seekable Timeline" pops in with back.out(1.7)
|
||||
2.00 – 2.50s Right pill badge "Render Ready" pops in
|
||||
0 – 5.00s Continuous floating: cards y ±6 px / rotation ±1° (phase-opposed)
|
||||
Badges y ±5 px (slow shared sine)
|
||||
and .card-tilt (static rotateY + float rotation) so entry and float
|
||||
don't fight on the same alias
|
||||
- All continuous floating consolidated into one shared scene-ticker onUpdate
|
||||
(6 gsap.set calls per frame, batched by the browser)
|
||||
- Phase offset Math.PI between the two cards' floats — opposed breathing
|
||||
- Card images are HyperFrames workflow mockups (no asset files needed)
|
||||
- Ambient dual-glow via two radial-gradients in a single overlay
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #0a1415;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.65);
|
||||
--brand-cyan: #18d9e8;
|
||||
--brand-cyan-glow: rgba(24, 217, 232, 1);
|
||||
--brand-green: #7bea5a;
|
||||
--brand-green-glow: rgba(123, 234, 90, 1);
|
||||
--glass-bg: rgba(255, 255, 255, 0.08);
|
||||
--border: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-dark);
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND
|
||||
============================================================ */
|
||||
.bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 35% 50%, rgba(24, 217, 232, 0.2), transparent 45%),
|
||||
radial-gradient(ellipse at 72% 50%, rgba(123, 234, 90, 0.16), transparent 45%),
|
||||
linear-gradient(135deg, #3a3a3a 0%, #17211f 30%, #0b2328 48%, #1f3518 70%, #343434 100%);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
TITLE
|
||||
============================================================ */
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 88px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
will-change: transform, opacity;
|
||||
/* initial opacity 0 + y -40 set via gsap.set */
|
||||
}
|
||||
.title .accent {
|
||||
color: var(--brand-cyan);
|
||||
text-shadow: 0 0 34px rgba(24, 217, 232, 0.28);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CARDS ROW
|
||||
============================================================ */
|
||||
.cards-row {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
gap: 76px;
|
||||
padding-top: 30px;
|
||||
perspective: 980px;
|
||||
perspective-origin: 50% 42%;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
/* Two nested wrappers per card */
|
||||
.card {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.card-pos {
|
||||
perspective: 980px;
|
||||
perspective-origin: 50% 45%;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform, opacity;
|
||||
/* initial x/scale/opacity set via gsap.set */
|
||||
}
|
||||
.card-tilt {
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
/* initial rotationY set via gsap.set */
|
||||
}
|
||||
.card-content {
|
||||
width: 720px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
position: relative;
|
||||
transform: translateZ(0) rotateX(0.01deg);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.card-left .card-tilt {
|
||||
transform-origin: 100% 50%;
|
||||
}
|
||||
.card-right .card-tilt {
|
||||
transform-origin: 0% 50%;
|
||||
}
|
||||
.card-label {
|
||||
margin-top: 26px;
|
||||
text-align: center;
|
||||
font-size: 54px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
transform: translateZ(34px);
|
||||
text-shadow: 0 0 30px rgba(24, 217, 232, 0.16);
|
||||
}
|
||||
.card-subtitle {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
transform: translateZ(26px);
|
||||
}
|
||||
|
||||
/* Left card: shadow falls right (positive x in box-shadow = right) */
|
||||
.card-left .card-image {
|
||||
box-shadow:
|
||||
30px 30px 60px rgba(0, 0, 0, 0.45),
|
||||
0 0 60px rgba(24, 217, 232, 0.2);
|
||||
}
|
||||
/* Right card: shadow falls left */
|
||||
.card-right .card-image {
|
||||
box-shadow:
|
||||
-30px 30px 60px rgba(0, 0, 0, 0.45),
|
||||
0 0 60px rgba(123, 234, 90, 0.2);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CARD IMAGE PLACEHOLDERS (mock UI)
|
||||
============================================================ */
|
||||
/* Left card — HTML Composition: gradient + timed clip grid */
|
||||
.card-left .card-image {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(12, 38, 42, 0.96) 0%,
|
||||
rgba(18, 66, 63, 0.9) 52%,
|
||||
rgba(32, 57, 38, 0.94) 100%
|
||||
);
|
||||
}
|
||||
.mock-templates {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 56px 66px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 22px;
|
||||
opacity: 0.22;
|
||||
}
|
||||
.mock-thumb {
|
||||
background: linear-gradient(135deg, rgba(24, 217, 232, 0.2), rgba(123, 234, 90, 0.08));
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
padding: 14px;
|
||||
transform: translateZ(18px);
|
||||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
.mock-thumb .label {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Right card — Render Pipeline: validation rows */
|
||||
.card-right .card-image {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(23, 45, 34, 0.96) 0%,
|
||||
rgba(17, 70, 73, 0.9) 50%,
|
||||
rgba(38, 57, 45, 0.94) 100%
|
||||
);
|
||||
}
|
||||
.mock-team {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 66px 72px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
opacity: 0.24;
|
||||
}
|
||||
.mock-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 15px 20px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transform: translateZ(18px);
|
||||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
.mock-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, var(--brand-green), var(--brand-cyan));
|
||||
}
|
||||
.mock-avatar.purple {
|
||||
background: linear-gradient(135deg, var(--brand-cyan), #38bdf8);
|
||||
}
|
||||
.mock-avatar.green {
|
||||
background: linear-gradient(135deg, var(--brand-green), var(--brand-cyan));
|
||||
}
|
||||
.mock-avatar.orange {
|
||||
background: linear-gradient(135deg, #facc15, var(--brand-green));
|
||||
}
|
||||
.mock-row .name {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.mock-row .status {
|
||||
margin-left: auto;
|
||||
font-size: 16px;
|
||||
color: var(--brand-green);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
FLOATING BADGES
|
||||
============================================================ */
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 22px;
|
||||
border-radius: 999px;
|
||||
background-color: var(--glass-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
will-change: transform, opacity;
|
||||
z-index: 50;
|
||||
}
|
||||
.badge-left {
|
||||
border: 1px solid rgba(24, 217, 232, 0.4);
|
||||
box-shadow: 0 0 30px rgba(24, 217, 232, 0.35);
|
||||
}
|
||||
.badge-right {
|
||||
border: 1px solid rgba(123, 234, 90, 0.4);
|
||||
box-shadow: 0 0 30px rgba(123, 234, 90, 0.35);
|
||||
}
|
||||
.badge-icon-wrap {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bg-dark);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.badge-left .badge-icon-wrap {
|
||||
background-color: var(--brand-cyan);
|
||||
}
|
||||
.badge-right .badge-icon-wrap {
|
||||
background-color: var(--brand-green);
|
||||
}
|
||||
.badge-label {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
AMBIENT GLOW + VIGNETTE
|
||||
============================================================ */
|
||||
.ambient-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 50%, var(--brand-cyan-glow) 0%, transparent 35%),
|
||||
radial-gradient(ellipse at 70% 50%, var(--brand-green-glow) 0%, transparent 35%);
|
||||
opacity: 0.13;
|
||||
pointer-events: none;
|
||||
}
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.45) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 400;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
class="stage clip"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
id="scene-stage"
|
||||
>
|
||||
<div class="bg"></div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="title" id="title">Build Video With <span class="accent">HyperFrames</span></div>
|
||||
|
||||
<!-- Cards row -->
|
||||
<div class="cards-row">
|
||||
<!-- Left card: HTML Composition -->
|
||||
<div class="card card-left" id="card-left">
|
||||
<div class="card-pos">
|
||||
<div class="card-tilt">
|
||||
<div class="card-content">
|
||||
<div class="card-image">
|
||||
<div class="mock-templates">
|
||||
<div class="mock-thumb"><span class="label">HTML</span></div>
|
||||
<div class="mock-thumb"><span class="label">CSS</span></div>
|
||||
<div class="mock-thumb"><span class="label">GSAP</span></div>
|
||||
<div class="mock-thumb"><span class="label">Audio</span></div>
|
||||
<div class="mock-thumb"><span class="label">Captions</span></div>
|
||||
<div class="mock-thumb"><span class="label">Assets</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">HTML Composition</div>
|
||||
<div class="card-subtitle">Timed DOM clips, media, and motion</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right card: Render Pipeline -->
|
||||
<div class="card card-right" id="card-right">
|
||||
<div class="card-pos">
|
||||
<div class="card-tilt">
|
||||
<div class="card-content">
|
||||
<div class="card-image">
|
||||
<div class="mock-team">
|
||||
<div class="mock-row">
|
||||
<div class="mock-avatar purple"></div>
|
||||
<span class="name">Register timeline</span>
|
||||
<span class="status">● seekable</span>
|
||||
</div>
|
||||
<div class="mock-row">
|
||||
<div class="mock-avatar green"></div>
|
||||
<span class="name">Validate layout</span>
|
||||
<span class="status">● clean</span>
|
||||
</div>
|
||||
<div class="mock-row">
|
||||
<div class="mock-avatar orange"></div>
|
||||
<span class="name">Render frames</span>
|
||||
<span class="status">● stable</span>
|
||||
</div>
|
||||
<div class="mock-row">
|
||||
<div class="mock-avatar"></div>
|
||||
<span class="name">Publish MP4</span>
|
||||
<span class="status">● ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-label">Render Pipeline</div>
|
||||
<div class="card-subtitle">Preview, check, render, publish</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating badges -->
|
||||
<div class="badge badge-left" id="badge-left">
|
||||
<div class="badge-icon-wrap">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" />
|
||||
<path d="M2 17L12 22L22 17" />
|
||||
<path d="M2 12L12 17L22 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="badge-label">Seekable Timeline</span>
|
||||
</div>
|
||||
|
||||
<div class="badge badge-right" id="badge-right">
|
||||
<div class="badge-icon-wrap">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M17 21V19C17 16.7909 15.2091 15 13 15H5C2.79086 15 1 16.7909 1 19V21" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21V19C22.9986 17.1771 21.765 15.5857 20 15.13" />
|
||||
<path
|
||||
d="M16 3.13C17.7699 3.58317 19.0078 5.17799 19.0078 7.005C19.0078 8.83201 17.7699 10.4268 16 10.88"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="badge-label">Render Ready</span>
|
||||
</div>
|
||||
|
||||
<div class="ambient-glow"></div>
|
||||
</div>
|
||||
|
||||
<div class="vignette"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
CONSTANTS
|
||||
================================================================ */
|
||||
const W = 1920,
|
||||
H = 1080;
|
||||
const TOTAL_DUR = 5.0;
|
||||
|
||||
const TIMING = {
|
||||
// Phase 1: title
|
||||
titleAt: 0.17,
|
||||
titleDur: 0.67,
|
||||
|
||||
// Phase 2: cards
|
||||
leftAt: 0.5,
|
||||
rightAt: 0.83,
|
||||
entryDur: 0.7,
|
||||
slideDist: 100,
|
||||
baseTilt: 18,
|
||||
|
||||
// Phase 3: badges
|
||||
badgeLeftAt: 1.67,
|
||||
badgeRightAt: 2.0,
|
||||
badgeEntryDur: 0.5,
|
||||
|
||||
// Continuous float
|
||||
floatYSpeed: 0.02 * 30, // = 0.6 rad/sec
|
||||
floatYAmp: 6,
|
||||
floatRSpeed: 0.015 * 30, // = 0.45 rad/sec
|
||||
floatRAmp: 1,
|
||||
badgeYSpeed: 0.025 * 30, // = 0.75 rad/sec
|
||||
badgeYAmp: 5,
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
BADGE POSITIONS — set via CSS left/top once (not tweened)
|
||||
================================================================ */
|
||||
const badgeLeftEl = document.getElementById("badge-left");
|
||||
const badgeRightEl = document.getElementById("badge-right");
|
||||
badgeLeftEl.style.left = W * 0.12 + "px"; // 230 px
|
||||
badgeLeftEl.style.top = H * 0.35 + "px"; // 378 px
|
||||
badgeLeftEl.style.position = "absolute";
|
||||
badgeRightEl.style.left = W * 0.75 + "px"; // 1440 px
|
||||
badgeRightEl.style.top = H * 0.38 + "px"; // 410 px
|
||||
badgeRightEl.style.position = "absolute";
|
||||
|
||||
/* ================================================================
|
||||
INITIAL STATES (via gsap.set, before the timeline runs)
|
||||
================================================================ */
|
||||
gsap.set("#title", { opacity: 0, y: -40 });
|
||||
|
||||
gsap.set("#card-left .card-pos", { x: -TIMING.slideDist, scale: 0.8, opacity: 0, y: 0 });
|
||||
gsap.set("#card-right .card-pos", { x: TIMING.slideDist, scale: 0.8, opacity: 0, y: 0 });
|
||||
gsap.set("#card-left .card-tilt", { rotationY: TIMING.baseTilt });
|
||||
gsap.set("#card-right .card-tilt", { rotationY: -TIMING.baseTilt });
|
||||
|
||||
gsap.set(["#badge-left", "#badge-right"], { scale: 0, opacity: 0, y: 0 });
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 1: Title slides down
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
"#title",
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: TIMING.titleDur,
|
||||
ease: "power3.out", // spring(stiffness:100, damping:16)
|
||||
},
|
||||
TIMING.titleAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 2: Cards enter from opposite sides
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
"#card-left .card-pos",
|
||||
{
|
||||
x: 0,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.entryDur,
|
||||
ease: "power3.out",
|
||||
},
|
||||
TIMING.leftAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#card-right .card-pos",
|
||||
{
|
||||
x: 0,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.entryDur,
|
||||
ease: "power3.out",
|
||||
},
|
||||
TIMING.rightAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 3: Badges pop in
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
"#badge-left",
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.badgeEntryDur,
|
||||
ease: "back.out(1.7)",
|
||||
},
|
||||
TIMING.badgeLeftAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#badge-right",
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.badgeEntryDur,
|
||||
ease: "back.out(1.7)",
|
||||
},
|
||||
TIMING.badgeRightAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
CONTINUOUS FLOATING — shared scene-ticker onUpdate
|
||||
Six gsap.set calls per frame, batched by the browser.
|
||||
---------------------------------------------------------------- */
|
||||
const leftPos = document.querySelector("#card-left .card-pos");
|
||||
const rightPos = document.querySelector("#card-right .card-pos");
|
||||
const leftTilt = document.querySelector("#card-left .card-tilt");
|
||||
const rightTilt = document.querySelector("#card-right .card-tilt");
|
||||
|
||||
tl.to(
|
||||
{ tick: 0 },
|
||||
{
|
||||
tick: 1,
|
||||
duration: TOTAL_DUR,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const t = tl.time();
|
||||
// Cards float in opposition (phase π apart).
|
||||
const lY = Math.sin(t * TIMING.floatYSpeed) * TIMING.floatYAmp;
|
||||
const lR = Math.sin(t * TIMING.floatRSpeed) * TIMING.floatRAmp;
|
||||
const rY = Math.sin(t * TIMING.floatYSpeed + Math.PI) * TIMING.floatYAmp;
|
||||
const rR = Math.sin(t * TIMING.floatRSpeed + Math.PI) * TIMING.floatRAmp;
|
||||
gsap.set(leftPos, { y: lY });
|
||||
gsap.set(rightPos, { y: rY });
|
||||
gsap.set(leftTilt, { rotationY: TIMING.baseTilt + lR });
|
||||
gsap.set(rightTilt, { rotationY: -TIMING.baseTilt + rR });
|
||||
|
||||
// Badges — small shared y oscillation.
|
||||
const bY = Math.sin(t * TIMING.badgeYSpeed) * TIMING.badgeYAmp;
|
||||
gsap.set(badgeLeftEl, { y: bY });
|
||||
gsap.set(badgeRightEl, { y: bY });
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,520 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 7 — HyperFrames Decrypt Pan Track</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (4 phases, 7 seconds total):
|
||||
0.0 – 0.7s Shot 1 static text fades in + rises ("HyperFrames renders")
|
||||
0.7 – 1.6s Hacker-flip decodes the accent word ("video")
|
||||
2.8 – 3.5s Horizontal pan to Shot 2 with parallax exit + scale-in
|
||||
3.7 – 6.1s Cursor-tracked typing: "HTML, CSS and JS become MP4"
|
||||
(NOT @the source/layout-utils, NOT a charWidthRatio constant)
|
||||
- Bar width pre-allocated from the full text — never tweened
|
||||
- Camera follows cursor by tweening the strip's `x`, with a piecewise
|
||||
Math.min(initialOffset, trackingOffset) for the two-phase camera
|
||||
- Hacker-flip glyph flicker via deterministic int hash, not Math.random
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: linear-gradient(
|
||||
135deg,
|
||||
#3a3a3a 0%,
|
||||
#17211f 30%,
|
||||
#0b2328 48%,
|
||||
#1f3518 70%,
|
||||
#343434 100%
|
||||
);
|
||||
--text-dark: #f8fafc;
|
||||
--text-highlight: #18d9e8;
|
||||
--text-highlight-2: #7bea5a;
|
||||
--search-bg: rgba(14, 24, 23, 0.82);
|
||||
--cursor: #7bea5a;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.viewport {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.strip {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
.shot {
|
||||
width: 1920px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SHOT 1 — "HyperFrames renders video"
|
||||
============================================================ */
|
||||
.shot1-content {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0; /* GSAP fromTo fades in */
|
||||
}
|
||||
.shot1-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4em;
|
||||
font-size: 130px;
|
||||
}
|
||||
.shot1-static {
|
||||
font-weight: 500;
|
||||
color: var(--text-dark);
|
||||
text-shadow: 0 0 36px rgba(24, 217, 232, 0.18);
|
||||
}
|
||||
.shot1-accent {
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
perspective: 800px; /* required for the per-glyph rotateX */
|
||||
}
|
||||
.flip-glyph {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
}
|
||||
.flip-glyph.space {
|
||||
min-width: 0.4em;
|
||||
}
|
||||
.flip-glyph .ghost {
|
||||
opacity: 0;
|
||||
}
|
||||
.flip-glyph .anim {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
color: var(--text-highlight);
|
||||
background: linear-gradient(135deg, var(--text-highlight) 0%, var(--text-highlight-2) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
filter: drop-shadow(0 0 24px rgba(24, 217, 232, 0.32))
|
||||
drop-shadow(0 0 36px rgba(123, 234, 90, 0.22));
|
||||
opacity: 0;
|
||||
transform: perspective(600px) rotateX(90deg);
|
||||
transform-origin: bottom;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SHOT 2 — Search bar with cursor-tracked typing
|
||||
============================================================ */
|
||||
.shot2-bar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
/* left set by JS once we know the bar's world coordinate */
|
||||
height: 240px;
|
||||
background: var(--search-bg);
|
||||
border: 1px solid rgba(123, 234, 90, 0.22);
|
||||
box-shadow:
|
||||
0 0 60px rgba(24, 217, 232, 0.2),
|
||||
0 0 96px rgba(123, 234, 90, 0.14),
|
||||
inset 0 0 32px rgba(24, 217, 232, 0.08);
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 120px;
|
||||
padding-right: 180px;
|
||||
opacity: 0; /* GSAP fromTo fades in */
|
||||
transform-origin: left center;
|
||||
}
|
||||
.search-text {
|
||||
font-size: 120px;
|
||||
font-weight: 400;
|
||||
color: var(--text-dark);
|
||||
text-shadow: 0 0 24px rgba(248, 250, 252, 0.16);
|
||||
white-space: pre;
|
||||
line-height: 1;
|
||||
}
|
||||
.search-cursor {
|
||||
font-size: 120px;
|
||||
color: var(--cursor);
|
||||
text-shadow: 0 0 26px rgba(123, 234, 90, 0.42);
|
||||
margin-left: 4px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Hidden probe used by measureNodeWidth */
|
||||
.measure-probe {
|
||||
position: absolute;
|
||||
left: -99999px;
|
||||
top: -99999px;
|
||||
visibility: hidden;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="viewport" data-layout-allow-overflow>
|
||||
<div class="strip" data-layout-allow-overflow>
|
||||
<!-- =====================================================
|
||||
SHOT 1 — "HyperFrames renders video"
|
||||
===================================================== -->
|
||||
<div
|
||||
id="shot-decode"
|
||||
class="shot shot1 clip"
|
||||
data-start="0"
|
||||
data-duration="3.5"
|
||||
data-track-index="1"
|
||||
>
|
||||
<div class="shot1-content">
|
||||
<div class="shot1-row">
|
||||
<span class="shot1-static">HyperFrames renders</span>
|
||||
<span class="shot1-accent" aria-label="video">
|
||||
<!-- .flip-glyph spans generated by JS -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =====================================================
|
||||
SHOT 2 — Search bar typing
|
||||
===================================================== -->
|
||||
<div
|
||||
id="shot-typing"
|
||||
class="shot shot2 clip"
|
||||
data-start="2.5"
|
||||
data-duration="4.5"
|
||||
data-track-index="2"
|
||||
>
|
||||
<div class="shot2-bar">
|
||||
<span class="search-text"></span><span class="search-cursor">_</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
CONSTANTS — all baked at setup time.
|
||||
================================================================ */
|
||||
const W = 1920,
|
||||
H = 1080;
|
||||
const COMPOSITION_DURATION = 7;
|
||||
const FPS_HASH = 60; // synthetic clock for the flicker hash
|
||||
|
||||
const TIMING = {
|
||||
// Phase 1: shot1 text fades in + rises
|
||||
shot1EntryStart: 0.0,
|
||||
shot1EntryDur: 0.67,
|
||||
|
||||
// Phase 2: hacker-flip "video"
|
||||
flipStart: 0.7,
|
||||
flipStagger: 0.066, // ~2 frames at 30fps per glyph
|
||||
flipDuration: 0.55,
|
||||
|
||||
// Phase 3: horizontal pan
|
||||
panStart: 2.83,
|
||||
panDuration: 0.67,
|
||||
|
||||
// Phase 4: cursor-tracked typing
|
||||
typingStart: 3.67, // panStart + panDuration + 0.17 buffer
|
||||
charRate: 0.083, // seconds per character (~2.5 frames at 30fps)
|
||||
};
|
||||
|
||||
const FULL_TEXT = "HTML, CSS and JS become MP4";
|
||||
const FONT_SIZE = 120;
|
||||
const PADDING_LEFT = 120;
|
||||
const PADDING_RIGHT = 180;
|
||||
const CURSOR_VIS_W = FONT_SIZE * 0.6; // visual cursor width approximation
|
||||
const CURSOR_TARGET = W * 0.7; // screen X where cursor locks
|
||||
const BAR_LEFT_MARGIN = 80; // initial left margin in Phase 4
|
||||
const PARALLAX_DIST = 400; // px Shot 1 moves extra during pan
|
||||
|
||||
/* ================================================================
|
||||
BUILD DOM — flip glyphs (Shot 1) generated synchronously.
|
||||
The accent text is "video" → 5 glyphs.
|
||||
================================================================ */
|
||||
const accentEl = document.querySelector(".shot1-accent");
|
||||
const ACCENT_WORD = "video";
|
||||
ACCENT_WORD.split("").forEach((char, index) => {
|
||||
const span = document.createElement("span");
|
||||
span.className = "flip-glyph" + (char === " " ? " space" : "");
|
||||
span.dataset.char = char;
|
||||
span.dataset.index = String(index);
|
||||
// Each glyph needs its own font-size to match the surrounding row.
|
||||
span.style.fontSize = "130px";
|
||||
const ghost = document.createElement("span");
|
||||
ghost.className = "ghost";
|
||||
ghost.textContent = char === " " ? " " : char;
|
||||
const anim = document.createElement("span");
|
||||
anim.className = "anim";
|
||||
anim.textContent = char === " " ? " " : char;
|
||||
span.append(ghost, anim);
|
||||
accentEl.appendChild(span);
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
TEXT MEASUREMENT — uses a hidden DOM probe so we capture
|
||||
real letter-spacing, kerning, and font-feature widths.
|
||||
Must run AFTER document.fonts.ready.
|
||||
================================================================ */
|
||||
function measureNodeWidth(text, font) {
|
||||
const probe = document.createElement("span");
|
||||
probe.className = "measure-probe";
|
||||
probe.style.font = font;
|
||||
probe.style.whiteSpace = "pre";
|
||||
probe.textContent = text;
|
||||
document.body.appendChild(probe);
|
||||
const width = probe.getBoundingClientRect().width;
|
||||
probe.remove();
|
||||
return width;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE BUILD — fires after fonts are ready so measurements
|
||||
use the real rendered metrics, not fallback fonts.
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Register early so HF can find it; tweens are added below.
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
document.fonts.ready.then(() => {
|
||||
const searchFont = `400 ${FONT_SIZE}px Inter, system-ui, sans-serif`;
|
||||
const fullTextWidth = measureNodeWidth(FULL_TEXT, searchFont);
|
||||
const barWidth = PADDING_LEFT + fullTextWidth + CURSOR_VIS_W + PADDING_RIGHT;
|
||||
|
||||
// Pre-allocate the bar's final width — no width tweens ever.
|
||||
const barEl = document.querySelector(".shot2-bar");
|
||||
barEl.style.width = barWidth + "px";
|
||||
|
||||
// Initial bar position in Shot 2's local space.
|
||||
// We position it at LEFT_MARGIN from the shot's left edge.
|
||||
barEl.style.left = BAR_LEFT_MARGIN + "px";
|
||||
barEl.style.transform = "translateY(-50%)"; // vertical center
|
||||
|
||||
// ============================================================
|
||||
// PHASE 1: Shot 1 text fade in + rise
|
||||
// ============================================================
|
||||
tl.fromTo(
|
||||
".shot1-content",
|
||||
{ opacity: 0, y: 30 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: TIMING.shot1EntryDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.shot1EntryStart,
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// PHASE 2: Hacker-flip "video"
|
||||
// ============================================================
|
||||
const CHAR_POOL = "abcdefghijklmnopqrstuvwxyz";
|
||||
const FLICKER = 3;
|
||||
const REVEAL_AT = 0.6;
|
||||
|
||||
// Deterministic 32-bit mix — replaces Math.random / seeded RNG.
|
||||
function pseudoHash(i, t) {
|
||||
return ((i * 374761393 + t * 668265263) >>> 0) % CHAR_POOL.length;
|
||||
}
|
||||
|
||||
document.querySelectorAll(".flip-glyph").forEach((glyph) => {
|
||||
const index = Number(glyph.dataset.index);
|
||||
const real = glyph.dataset.char === " " ? " " : glyph.dataset.char;
|
||||
const anim = glyph.querySelector(".anim");
|
||||
const start = TIMING.flipStart + index * TIMING.flipStagger;
|
||||
|
||||
tl.fromTo(
|
||||
anim,
|
||||
{ rotationX: 90, opacity: 0, "--p": 0 },
|
||||
{
|
||||
rotationX: 0,
|
||||
opacity: 1,
|
||||
"--p": 1,
|
||||
duration: TIMING.flipDuration,
|
||||
ease: "back.out(1.6)", // spring(stiffness:150, damping:14)
|
||||
onUpdate: function () {
|
||||
const p = Number(gsap.getProperty(anim, "--p"));
|
||||
if (p >= REVEAL_AT) {
|
||||
if (anim.textContent !== real) anim.textContent = real;
|
||||
} else {
|
||||
const localFrame = Math.floor((tl.time() - start) * FPS_HASH);
|
||||
const bucket = Math.max(0, Math.floor(localFrame / FLICKER));
|
||||
anim.textContent = CHAR_POOL[pseudoHash(index, bucket)];
|
||||
}
|
||||
},
|
||||
},
|
||||
start,
|
||||
);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// PHASE 3: Horizontal pan + parallax exit + Shot 2 entry
|
||||
// All three tweens at the same timeline position run in parallel.
|
||||
// ============================================================
|
||||
// (a) Camera pan — strip slides one full viewport left.
|
||||
tl.to(
|
||||
".strip",
|
||||
{
|
||||
x: -W,
|
||||
duration: TIMING.panDuration,
|
||||
ease: "power3.inOut", // cinematic slow-in-slow-out
|
||||
},
|
||||
TIMING.panStart,
|
||||
);
|
||||
|
||||
// (b) Shot 1 parallax exit — content moves EXTRA -PARALLAX_DIST.
|
||||
tl.to(
|
||||
".shot1-content",
|
||||
{
|
||||
x: -PARALLAX_DIST,
|
||||
duration: TIMING.panDuration,
|
||||
ease: "power3.inOut",
|
||||
},
|
||||
TIMING.panStart,
|
||||
);
|
||||
|
||||
// Shot 1 fades out partway through the pan so the eye lands on Shot 2.
|
||||
tl.to(
|
||||
".shot1-content",
|
||||
{
|
||||
opacity: 0,
|
||||
duration: TIMING.panDuration * 0.4,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.panStart,
|
||||
);
|
||||
|
||||
// (c) Shot 2 bar entry — fade + scale with mild overshoot ("landing").
|
||||
tl.fromTo(
|
||||
".shot2-bar",
|
||||
{ opacity: 0, scale: 0.8 },
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: TIMING.panDuration,
|
||||
ease: "back.out(1.2)",
|
||||
},
|
||||
TIMING.panStart,
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// PHASE 4: Cursor-tracked typing.
|
||||
//
|
||||
// The strip is now at x = -W (Shot 2 fully on screen). For the
|
||||
// cursor-track effect we'll *further* shift the strip by a piecewise
|
||||
// amount: hold initial offset while the empty bar's cursor is left
|
||||
// of CURSOR_TARGET, then follow once typing pushes it past.
|
||||
// ============================================================
|
||||
const searchTextEl = document.querySelector(".search-text");
|
||||
const stripEl = document.querySelector(".strip");
|
||||
|
||||
const CURSOR_WIDTH_HALF = CURSOR_VIS_W / 2;
|
||||
const STRIP_BASE_X = -W; // strip's x at the end of Phase 3
|
||||
|
||||
// Initial Phase-4 offset = 0 (no additional shift on top of -W).
|
||||
// i.e. bar already sits at BAR_LEFT_MARGIN inside Shot 2.
|
||||
const INITIAL_OFFSET = 0;
|
||||
|
||||
// Typing driver — a clock that runs from 0 to FULL_TEXT.length.
|
||||
const typingProxy = { progress: 0 };
|
||||
const typingDur = FULL_TEXT.length * TIMING.charRate;
|
||||
const typingEnd = TIMING.typingStart + typingDur;
|
||||
const postTypingHold = Math.max(0, COMPOSITION_DURATION - typingEnd);
|
||||
|
||||
tl.to(
|
||||
typingProxy,
|
||||
{
|
||||
progress: FULL_TEXT.length,
|
||||
duration: typingDur,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const charsTyped = Math.min(FULL_TEXT.length, Math.floor(typingProxy.progress));
|
||||
const visibleText = FULL_TEXT.slice(0, charsTyped);
|
||||
if (searchTextEl.textContent !== visibleText) {
|
||||
searchTextEl.textContent = visibleText;
|
||||
}
|
||||
|
||||
// Measure current visible width to compute cursor screen position.
|
||||
// Cheap because Inter's metrics are cached after the first call.
|
||||
const visibleW =
|
||||
visibleText.length === 0 ? 0 : measureNodeWidth(visibleText, searchFont);
|
||||
|
||||
// Cursor X in *Shot 2's* coordinate system:
|
||||
// shot left edge → bar left edge (BAR_LEFT_MARGIN)
|
||||
// → cursor within bar (PADDING_LEFT + visibleW + CURSOR_WIDTH_HALF)
|
||||
const cursorXInShot2 = BAR_LEFT_MARGIN + PADDING_LEFT + visibleW + CURSOR_WIDTH_HALF;
|
||||
|
||||
// For the cursor to land at CURSOR_TARGET on the *screen*, the
|
||||
// strip must be shifted by:
|
||||
// stripX = CURSOR_TARGET - cursorXInShot2 - W (the -W accounts
|
||||
// for Shot 2 being the SECOND shot in the strip)
|
||||
const trackingStripX = CURSOR_TARGET - cursorXInShot2 - W;
|
||||
|
||||
// Piecewise: hold STRIP_BASE_X + INITIAL_OFFSET until tracking
|
||||
// would pan FURTHER LEFT than the base, then follow.
|
||||
const finalStripX = Math.min(STRIP_BASE_X + INITIAL_OFFSET, trackingStripX);
|
||||
|
||||
gsap.set(stripEl, { x: finalStripX });
|
||||
},
|
||||
},
|
||||
TIMING.typingStart,
|
||||
);
|
||||
|
||||
if (postTypingHold > 0) {
|
||||
tl.to(
|
||||
".search-cursor",
|
||||
{
|
||||
opacity: 0.22,
|
||||
duration: postTypingHold / 2,
|
||||
repeat: 1,
|
||||
yoyo: true,
|
||||
ease: "none",
|
||||
},
|
||||
typingEnd,
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,468 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 10 — HyperFrames Morph Press Interact</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (4 phases, 5.5 seconds total):
|
||||
0.17 – 0.64s Hero "HyperFrames" + logo asset fade up (opacity + y rise)
|
||||
0.17 – 5.50s Logo asset rotates ±4° continuously (sine onUpdate)
|
||||
2.17 – 2.62s Hero shrinks (1 → 0.6) and fades (1 → 0)
|
||||
2.17 – 2.62s "Build video from HTML" CTA pill pops in with back.out(2) overshoot
|
||||
2.33 – 2.66s CTA text fades + lifts into place (after container is recognizable)
|
||||
2.83s Cursor hard-cuts in at off-screen bottom-right
|
||||
2.83 – 3.83s Cursor approaches via spring path to (W/2 + 120, H/2 + 50)
|
||||
3.83 – 3.98s Click DOWN: both cursor and CTA compress to scale 0.9 (sync)
|
||||
4.17 – 4.42s Click UP: both return to scale 1.0 (sync)
|
||||
DOM + GSAP opacity tweens; z-index keeps incoming above outgoing
|
||||
- Press synchronization uses single GSAP target array ["#cta", "#cursor"]
|
||||
to guarantee perfectly identical tween values
|
||||
- Cursor opacity uses a 0.001-second fromTo for hard-cut step change
|
||||
- Logo uses a local static image asset referenced with a plain URL
|
||||
- Breathing rotation only on the logo, not on the whole hero (avoids
|
||||
conflict with the morph exit scale tween)
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--hf-bg: #050711;
|
||||
--hf-bg-2: #0b1020;
|
||||
--hf-ink: #f8fafc;
|
||||
--hf-muted: #cbd5e1;
|
||||
--hf-purple: #7c3aed;
|
||||
--hf-purple-soft: #a78bfa;
|
||||
--hf-cyan: #18d9e8;
|
||||
--hf-green: #22c55e;
|
||||
--hf-glass: rgba(248, 250, 252, 0.08);
|
||||
--white: #ffffff;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 42%, rgba(24, 217, 232, 0.14), transparent 50%),
|
||||
radial-gradient(ellipse at 62% 48%, rgba(124, 58, 237, 0.2), transparent 56%),
|
||||
radial-gradient(ellipse at 42% 58%, rgba(34, 197, 94, 0.1), transparent 58%),
|
||||
linear-gradient(135deg, #050711 0%, #090b18 48%, #120717 100%);
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--hf-ink);
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
HERO LOCKUP (Phase 1 — exits during morph)
|
||||
============================================================ */
|
||||
.hero {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 54px;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.hero-text {
|
||||
font-size: 220px;
|
||||
font-weight: 800;
|
||||
color: var(--hf-ink);
|
||||
letter-spacing: 0;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
text-shadow:
|
||||
0 0 34px rgba(167, 139, 250, 0.18),
|
||||
0 0 72px rgba(24, 217, 232, 0.1);
|
||||
}
|
||||
.hero-text .brand-accent {
|
||||
color: var(--hf-purple-soft);
|
||||
text-shadow:
|
||||
0 0 34px rgba(124, 58, 237, 0.45),
|
||||
0 0 82px rgba(167, 139, 250, 0.22);
|
||||
}
|
||||
.hero-logo {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
margin-left: 8px;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero-logo .logo-mark {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 36px;
|
||||
box-shadow:
|
||||
0 0 64px rgba(24, 217, 232, 0.32),
|
||||
0 0 112px rgba(34, 197, 94, 0.18),
|
||||
0 0 150px rgba(124, 58, 237, 0.12);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CTA PILL (Phase 2 — pops in during morph)
|
||||
============================================================ */
|
||||
.cta {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.96) 0%,
|
||||
rgba(24, 217, 232, 0.9) 48%,
|
||||
rgba(34, 197, 94, 0.96) 100%
|
||||
);
|
||||
padding: 70px 200px;
|
||||
border-radius: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(24, 217, 232, 0.55);
|
||||
box-shadow:
|
||||
0 24px 60px rgba(0, 0, 0, 0.42),
|
||||
0 0 72px rgba(24, 217, 232, 0.24),
|
||||
0 0 120px rgba(124, 58, 237, 0.24),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.24),
|
||||
inset 0 0 34px var(--hf-glass);
|
||||
will-change: transform, opacity;
|
||||
/* initial scale 0 + opacity 0 set by gsap.set() */
|
||||
}
|
||||
.cta-text {
|
||||
color: var(--hf-bg);
|
||||
font-size: 110px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
text-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.3),
|
||||
0 0 22px rgba(255, 255, 255, 0.12);
|
||||
will-change: transform, opacity;
|
||||
/* initial opacity 0 + y 10 set by gsap.set() */
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CURSOR (Phase 3 — hard-cuts in)
|
||||
============================================================ */
|
||||
.cursor {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
will-change: transform, opacity;
|
||||
/* initial x/y/opacity set by gsap.set() */
|
||||
}
|
||||
.cursor .cursor-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="5.5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="stage clip" data-start="0" data-duration="5.5" data-track-index="1" id="stage">
|
||||
<!-- =====================================================
|
||||
HERO LOCKUP — "HyperFrames" + logo
|
||||
===================================================== -->
|
||||
<div
|
||||
class="hero clip"
|
||||
id="hero"
|
||||
data-start="0"
|
||||
data-duration="2.62"
|
||||
data-track-index="2"
|
||||
data-layout-allow-overflow
|
||||
aria-hidden="true"
|
||||
>
|
||||
<h1 class="hero-text">Hyper<span class="brand-accent">Frames</span></h1>
|
||||
<div class="hero-logo" id="hero-logo">
|
||||
<!-- Inline-SVG placeholder mark — swap for your logo image -->
|
||||
<svg class="logo-mark" viewBox="0 0 100 100" role="img" aria-label="HyperFrames">
|
||||
<defs>
|
||||
<linearGradient id="hfMark" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#8b7bff" />
|
||||
<stop offset="1" stop-color="#3ddc97" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="92" height="92" rx="22" fill="url(#hfMark)" />
|
||||
<text
|
||||
x="50"
|
||||
y="63"
|
||||
text-anchor="middle"
|
||||
font-family="Inter, system-ui, sans-serif"
|
||||
font-size="40"
|
||||
font-weight="800"
|
||||
fill="#fff"
|
||||
>
|
||||
HF
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =====================================================
|
||||
CTA PILL — "Build video from HTML"
|
||||
===================================================== -->
|
||||
<div class="cta" id="cta">
|
||||
<span class="cta-text">Build video from HTML</span>
|
||||
</div>
|
||||
|
||||
<!-- =====================================================
|
||||
CURSOR — inline SVG arrow with drop-shadow
|
||||
===================================================== -->
|
||||
<div class="cursor" id="cursor">
|
||||
<svg
|
||||
class="cursor-svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<filter id="cursorShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="2" dy="4" stdDeviation="3" flood-opacity="0.3" />
|
||||
</filter>
|
||||
</defs>
|
||||
<path
|
||||
d="M5.5 3.5L19 10.5L11.5 12.5L15.5 19.5L13.5 20.5L9.5 13.5L5.5 17.5V3.5Z"
|
||||
fill="#050711"
|
||||
stroke="#f8fafc"
|
||||
stroke-width="1.5"
|
||||
stroke-linejoin="round"
|
||||
filter="url(#cursorShadow)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
TIMING
|
||||
================================================================ */
|
||||
const W = 1920,
|
||||
H = 1080;
|
||||
const TOTAL_DUR = 5.5;
|
||||
|
||||
const TIMING = {
|
||||
introStart: 0.17, // ~5 frames at 30 fps
|
||||
introDur: 0.47, // hero enter spring settle
|
||||
|
||||
morphAt: 2.17, // ~65 frames
|
||||
morphExitDur: 0.5,
|
||||
morphFadeDur: 0.15, // 30% of exit
|
||||
morphEntDur: 0.45, // CTA pop-in
|
||||
|
||||
textRevealAt: 2.33, // morph + 0.17 s
|
||||
textRevealDur: 0.33,
|
||||
|
||||
cursorEnterAt: 2.83,
|
||||
cursorPathDur: 1.0,
|
||||
|
||||
clickDownAt: 3.83,
|
||||
clickDownDur: 0.15,
|
||||
clickUpAt: 4.17,
|
||||
clickUpDur: 0.25,
|
||||
|
||||
pressIntensity: 0.1,
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
INITIAL STATES (via gsap.set, before the timeline runs)
|
||||
================================================================ */
|
||||
// Hero: starts at opacity 0, y +40 (will fade up)
|
||||
gsap.set("#hero", { opacity: 0, y: 40 });
|
||||
|
||||
// CTA: starts invisible (scale 0, opacity 0) — morph pops it in
|
||||
gsap.set("#cta", { scale: 0, opacity: 0 });
|
||||
|
||||
// CTA text: starts at opacity 0 + y 10 — reveals after container is recognizable
|
||||
gsap.set(".cta-text", { opacity: 0, y: 10 });
|
||||
|
||||
// Cursor: starts off-screen bottom-right + invisible + scale 1 baseline
|
||||
gsap.set("#cursor", {
|
||||
x: W + 100,
|
||||
y: H + 200,
|
||||
scale: 1,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 1: Hero entrance + breathing rotation on logo
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
"#hero",
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: TIMING.introDur,
|
||||
ease: "power3.out", // spring(stiffness:120, damping:14)
|
||||
},
|
||||
TIMING.introStart,
|
||||
);
|
||||
|
||||
// Continuous breathing rotation on the logo. ±4° over ~6.3 s period.
|
||||
// For a 5.5 s composition this is just under one cycle — subtle but alive.
|
||||
// Use Form 2 (onUpdate reading tl.time) per sine-wave-loop rule.
|
||||
const logoEl = document.querySelector("#hero-logo");
|
||||
tl.to(
|
||||
{ tick: 0 },
|
||||
{
|
||||
tick: 1,
|
||||
duration: TOTAL_DUR,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const t = tl.time();
|
||||
gsap.set(logoEl, { rotation: Math.sin(t * 1.0) * 4 });
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 2: Scale-swap morph
|
||||
Three concurrent tween clusters from MORPH_AT.
|
||||
---------------------------------------------------------------- */
|
||||
// (1) Hero shrinks
|
||||
tl.to(
|
||||
"#hero",
|
||||
{
|
||||
scale: 0.6,
|
||||
duration: TIMING.morphExitDur,
|
||||
ease: "power3.out", // spring(stiffness:150, damping:18)
|
||||
},
|
||||
TIMING.morphAt,
|
||||
);
|
||||
|
||||
// (2) Hero fades fast (30% of shrink dur)
|
||||
tl.to(
|
||||
"#hero",
|
||||
{
|
||||
opacity: 0,
|
||||
duration: TIMING.morphFadeDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.morphAt,
|
||||
);
|
||||
|
||||
tl.set(
|
||||
"#hero",
|
||||
{
|
||||
x: -4000,
|
||||
},
|
||||
TIMING.morphAt + TIMING.morphFadeDur,
|
||||
);
|
||||
|
||||
// (3) CTA pops in with overshoot
|
||||
tl.to(
|
||||
"#cta",
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.morphEntDur,
|
||||
ease: "back.out(2)", // spring(stiffness:200, damping:15, mass:0.6)
|
||||
},
|
||||
TIMING.morphAt,
|
||||
);
|
||||
|
||||
// (4) CTA text reveals after container reaches recognizable scale
|
||||
tl.to(
|
||||
".cta-text",
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: TIMING.textRevealDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.textRevealAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 3: Cursor entry + motion path
|
||||
---------------------------------------------------------------- */
|
||||
// Hard-cut opacity (cursors don't fade in — they appear).
|
||||
// 0.001 s tween creates a step change that scrubs correctly.
|
||||
tl.fromTo(
|
||||
"#cursor",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.001, ease: "none" },
|
||||
TIMING.cursorEnterAt,
|
||||
);
|
||||
|
||||
// Spring-driven approach to a target slightly offset from center
|
||||
// (where a human would naturally aim — not dead-center).
|
||||
tl.to(
|
||||
"#cursor",
|
||||
{
|
||||
x: W / 2 + 120,
|
||||
y: H / 2 + 50,
|
||||
duration: TIMING.cursorPathDur,
|
||||
ease: "power2.out", // spring(stiffness:60, damping:20)
|
||||
},
|
||||
TIMING.cursorEnterAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 4: Physics-based press
|
||||
Both CTA and cursor compress together — single target array.
|
||||
---------------------------------------------------------------- */
|
||||
// Press DOWN — both elements compress to (1 - intensity)
|
||||
tl.to(
|
||||
["#cta", "#cursor"],
|
||||
{
|
||||
scale: 1 - TIMING.pressIntensity,
|
||||
duration: TIMING.clickDownDur,
|
||||
ease: "power3.out", // spring(stiffness:300, damping:20)
|
||||
},
|
||||
TIMING.clickDownAt,
|
||||
);
|
||||
|
||||
// RELEASE — back to scale 1.0
|
||||
tl.to(
|
||||
["#cta", "#cursor"],
|
||||
{
|
||||
scale: 1.0,
|
||||
duration: TIMING.clickUpDur,
|
||||
ease: "power2.out", // spring(stiffness:200, damping:15)
|
||||
},
|
||||
TIMING.clickUpAt,
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,759 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 04 — Contextual Product Showcase</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (4 phases, 9 seconds total):
|
||||
0.00 – 0.80s 3D-tilted page card scales 0.95 → 1.0
|
||||
Navbar / title / CTA fade in (staggered)
|
||||
0.12 – 2.80s Title keywords glow synced to ASR words:
|
||||
"1" "long" "video" "10" "viral" "clips"
|
||||
3.08 – 4.08s Page content scrolls up 280 px (programmatic scroll feel)
|
||||
Carousel section fades in + scales from 0.9
|
||||
3.58 – 8.84s Main video pops forward in 3D (translateZ 80 px) + scales up
|
||||
Radial spotlight dims the surroundings; decays at end
|
||||
(attack 0→1 then decay 1→REST_LEVEL). All visual effects (color,
|
||||
text-shadow, scale) derive from --glow via CSS calc()
|
||||
- Pop-target uses the same --glow pattern; translateZ + scale derived
|
||||
- Spotlight is a separate overlay with simple opacity tween
|
||||
- Video placeholders are colored divs (no asset files needed)
|
||||
- Tilt is static CSS, never tweened
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--page-bg: #0a0a0f;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a1a1aa;
|
||||
--accent: #edcb50;
|
||||
--input-bg: #1a1a1f;
|
||||
--green: #22c55e;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND
|
||||
============================================================ */
|
||||
.bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse 100% 60% at 50% -10%,
|
||||
rgba(100, 100, 150, 0.08) 0%,
|
||||
transparent 60%
|
||||
),
|
||||
var(--page-bg);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
3D PERSPECTIVE + TILTED CARD
|
||||
============================================================ */
|
||||
.perspective-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
perspective: 1200px;
|
||||
}
|
||||
.page-card {
|
||||
width: 92%;
|
||||
height: 88%;
|
||||
background-color: var(--page-bg);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
/* tilt + initial scale set by gsap.set() below so subsequent tweens preserve them */
|
||||
box-shadow:
|
||||
-30px 30px 60px rgba(0, 0, 0, 0.4),
|
||||
-15px 15px 30px rgba(0, 0, 0, 0.3),
|
||||
0 0 80px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
NAVBAR (sticks to top of card; NOT inside scroll content)
|
||||
============================================================ */
|
||||
.page-navbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 72px;
|
||||
padding: 0 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 100;
|
||||
opacity: 0; /* GSAP fades in */
|
||||
background: linear-gradient(to bottom, rgba(10, 10, 15, 0.95), rgba(10, 10, 15, 0.6));
|
||||
}
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.nav-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.nav-brand {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.nav-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
}
|
||||
.nav-item {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.nav-item .chev {
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.nav-tag {
|
||||
font-size: 10px;
|
||||
background: var(--green);
|
||||
color: #fff;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.nav-signin {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-signup {
|
||||
padding: 8px 16px;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
color: var(--page-bg);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SCROLL CONTENT — GSAP tweens its y
|
||||
============================================================ */
|
||||
.scroll-content {
|
||||
padding-top: 140px;
|
||||
padding-bottom: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
HERO SECTION
|
||||
============================================================ */
|
||||
.hero {
|
||||
text-align: center;
|
||||
max-width: 1200px;
|
||||
width: 90%;
|
||||
opacity: 0; /* GSAP fades in */
|
||||
}
|
||||
.hero-eyebrow {
|
||||
font-size: 14px;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hero-title {
|
||||
font-size: 64px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
.hero-sub {
|
||||
font-size: 20px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 24px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
KEYWORD GLOW — all derived from --glow CSS custom property
|
||||
============================================================ */
|
||||
.kw {
|
||||
--glow: 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
padding: 0 0.015em;
|
||||
color: rgb(
|
||||
calc(255 - var(--glow) * 18) calc(255 - var(--glow) * 30) calc(255 - var(--glow) * 150)
|
||||
);
|
||||
text-shadow:
|
||||
0 0 calc(var(--glow) * 10px) rgba(255, 235, 120, calc(var(--glow) * 0.95)),
|
||||
0 0 calc(var(--glow) * 24px) rgba(237, 203, 80, calc(var(--glow) * 0.78)),
|
||||
0 0 calc(var(--glow) * 44px) rgba(237, 203, 80, calc(var(--glow) * 0.38));
|
||||
transform: scale(calc(1 + var(--glow) * 0.035));
|
||||
}
|
||||
.kw::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -0.025em;
|
||||
right: -0.025em;
|
||||
top: 0.08em;
|
||||
bottom: 0.05em;
|
||||
z-index: -1;
|
||||
border-radius: 0.16em;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(237, 203, 80, 0.08),
|
||||
rgba(255, 222, 80, 0.72),
|
||||
rgba(237, 203, 80, 0.12)
|
||||
);
|
||||
opacity: calc(var(--glow) * 0.78);
|
||||
transform: scaleX(calc(0.72 + var(--glow) * 0.24)) skewX(-5deg);
|
||||
transform-origin: left center;
|
||||
box-shadow:
|
||||
0 0 calc(var(--glow) * 14px) rgba(237, 203, 80, calc(var(--glow) * 0.7)),
|
||||
0 0 calc(var(--glow) * 28px) rgba(237, 203, 80, calc(var(--glow) * 0.32));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CTA INPUT ROW
|
||||
============================================================ */
|
||||
.cta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 36px;
|
||||
opacity: 0; /* GSAP fades in */
|
||||
}
|
||||
.cta-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--input-bg);
|
||||
border-radius: 28px;
|
||||
padding: 6px 6px 6px 18px;
|
||||
gap: 12px;
|
||||
}
|
||||
.cta-link-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.cta-placeholder {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
min-width: 130px;
|
||||
}
|
||||
.cta-button {
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
color: var(--page-bg);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cta-or {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 14px;
|
||||
}
|
||||
.cta-upload {
|
||||
padding: 12px 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 22px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
VIDEO CAROUSEL (Phase 2 entry, Phase 4 pop-out)
|
||||
============================================================ */
|
||||
.carousel-wrap {
|
||||
opacity: 0; /* GSAP fades in */
|
||||
transform: scale(0.9); /* GSAP scales to 1 */
|
||||
transform-origin: center top;
|
||||
margin-top: 48px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
padding: 0 20px;
|
||||
perspective: 800px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.carousel-arrow {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: rgba(60, 60, 65, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* Main video — Phase 4 pop-out target */
|
||||
.carousel-main {
|
||||
--glow: 0;
|
||||
width: 720px;
|
||||
height: 420px;
|
||||
background: var(--input-bg);
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
transform-style: preserve-3d;
|
||||
transform: translateZ(calc(var(--glow) * 80px)) scale(calc(1 + var(--glow) * 0.15));
|
||||
box-shadow:
|
||||
0 0 calc(var(--glow) * 25px) rgba(237, 203, 80, calc(var(--glow) * 0.7)),
|
||||
0 0 calc(var(--glow) * 50px) rgba(237, 203, 80, calc(var(--glow) * 0.4)),
|
||||
0 calc(20px + var(--glow) * 40px) calc(60px + var(--glow) * 40px)
|
||||
rgba(0, 0, 0, calc(0.6 + var(--glow) * 0.2));
|
||||
border: 3px solid rgba(237, 203, 80, calc(var(--glow) * 0.8));
|
||||
z-index: 200;
|
||||
}
|
||||
.carousel-main-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
background: linear-gradient(135deg, #1a2540 0%, #3d2a4f 50%, #4f1d2e 100%);
|
||||
}
|
||||
.carousel-main-placeholder span {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 8px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Side panel */
|
||||
.carousel-side {
|
||||
width: 420px;
|
||||
height: 280px;
|
||||
background: var(--input-bg);
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.carousel-side-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #2a3550 0%, #4a3a5a 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.presets-stack {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 5;
|
||||
}
|
||||
.preset-label {
|
||||
padding: 8px 14px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.preset-tile {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SPOTLIGHT OVERLAY (above page card, dims surroundings)
|
||||
============================================================ */
|
||||
.spotlight {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse 850px 550px at 40% 60%,
|
||||
transparent 0%,
|
||||
transparent 50%,
|
||||
rgba(0, 0, 0, 0.65) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0; /* GSAP fades in during Phase 4 */
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
VIGNETTE
|
||||
============================================================ */
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.3) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 400;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="9"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="bg"></div>
|
||||
|
||||
<div class="perspective-wrap">
|
||||
<div
|
||||
class="page-card clip"
|
||||
data-start="0"
|
||||
data-duration="9"
|
||||
data-track-index="1"
|
||||
id="page-card"
|
||||
>
|
||||
<!-- Navbar (anchored to card top) -->
|
||||
<nav class="page-navbar" id="navbar">
|
||||
<div class="nav-left">
|
||||
<svg
|
||||
class="nav-logo"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M11.954 3.817c-4.49 0-8.132 3.644-8.152 8.146v12H0V12C0 5.373 5.352 0 11.954 0s11.953 5.373 11.953 12-5.351 12-11.953 12a12 12 0 0 1-1.718-.123v-3.876q.831.181 1.718.182c4.502 0 8.152-3.663 8.152-8.183s-3.65-8.183-8.152-8.183"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.118 24V11.995c0-3.79 3.062-6.857 6.836-6.857 3.773 0 6.836 3.068 6.836 6.857s-3.063 6.857-6.836 6.857c-.594 0-1.17-.076-1.718-.218V14.5c.488.337 1.08.534 1.718.534a3.037 3.037 0 0 0 3.034-3.04 3.037 3.037 0 0 0-3.034-3.04 3.037 3.037 0 0 0-3.034 3.008V24z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-brand">OpusClip</span>
|
||||
</div>
|
||||
<div class="nav-center">
|
||||
<span class="nav-item">Features<span class="chev">▾</span></span>
|
||||
<span class="nav-item">Solutions<span class="chev">▾</span></span>
|
||||
<span class="nav-item">Resources<span class="chev">▾</span></span>
|
||||
<span class="nav-item">Pricing</span>
|
||||
<span class="nav-item">For business<span class="chev">▾</span></span>
|
||||
<span class="nav-item"><span class="nav-tag">New</span>Agent Opus</span>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<span class="nav-signin">Sign in</span>
|
||||
<span class="nav-signup">Sign up - It's FREE</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Scrollable content -->
|
||||
<div class="scroll-content" id="scroll-content">
|
||||
<div class="hero" id="hero">
|
||||
<div class="hero-eyebrow">#1 AI VIDEO CLIPPING TOOL</div>
|
||||
<h1 class="hero-title">
|
||||
<span class="kw" data-glow-start="0.12" data-glow-end="0.28">1</span>
|
||||
<span class="kw" data-glow-start="0.52" data-glow-end="0.72">long</span>
|
||||
<span class="kw" data-glow-start="0.78" data-glow-end="1.44">video,</span>
|
||||
<span class="kw" data-glow-start="1.48" data-glow-end="1.80">10</span>
|
||||
<span class="kw" data-glow-start="1.94" data-glow-end="2.22">viral</span>
|
||||
<span class="kw" data-glow-start="2.28" data-glow-end="2.80">clips.</span>
|
||||
<br />Create 10x faster.
|
||||
</h1>
|
||||
<p class="hero-sub">
|
||||
OpusClip turns long videos into shorts, and publishes them to all social platforms
|
||||
in one click.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cta-row" id="cta-row">
|
||||
<div class="cta-input">
|
||||
<svg
|
||||
class="cta-link-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
<span class="cta-placeholder">Drop a video link</span>
|
||||
<div class="cta-button">Get free clips</div>
|
||||
</div>
|
||||
<span class="cta-or">or</span>
|
||||
<div class="cta-upload">Upload files</div>
|
||||
</div>
|
||||
|
||||
<!-- Video carousel section -->
|
||||
<div class="carousel-wrap" id="carousel">
|
||||
<div class="carousel-arrow">‹</div>
|
||||
|
||||
<div class="carousel-main pop-target" id="pop-target">
|
||||
<div class="carousel-main-placeholder">
|
||||
<span>Hero animation</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="carousel-arrow">›</div>
|
||||
|
||||
<div class="carousel-side">
|
||||
<div class="carousel-side-placeholder">Demo · reframe</div>
|
||||
<div class="presets-stack">
|
||||
<div class="preset-label">Presets</div>
|
||||
<div class="preset-tile"></div>
|
||||
<div class="preset-tile"></div>
|
||||
<div class="preset-tile"></div>
|
||||
<div class="preset-tile"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.scroll-content -->
|
||||
</div>
|
||||
<!-- /.page-card -->
|
||||
</div>
|
||||
<!-- /.perspective-wrap -->
|
||||
|
||||
<div class="spotlight" id="spotlight"></div>
|
||||
<div class="vignette"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
TIMING — all in local seconds (scene starts at 0).
|
||||
ASR timestamps are direct from the source, with SCENE_START = 16
|
||||
subtracted to get local time.
|
||||
================================================================ */
|
||||
const ASR = {
|
||||
// Phase 2 title keywords
|
||||
one: { start: 0.119, end: 0.279 },
|
||||
long: { start: 0.52, end: 0.719 },
|
||||
video: { start: 0.779, end: 1.44 },
|
||||
ten: { start: 1.479, end: 1.799 },
|
||||
viral: { start: 1.94, end: 2.219 },
|
||||
clips: { start: 2.279, end: 2.8 },
|
||||
};
|
||||
|
||||
const TIMING = {
|
||||
// Phase 1: entry
|
||||
cardEntryStart: 0.0,
|
||||
cardEntryDur: 0.8,
|
||||
navbarFadeAt: 0.0,
|
||||
navbarFadeDur: 0.6,
|
||||
titleFadeAt: 0.17,
|
||||
titleFadeDur: 0.7,
|
||||
ctaFadeAt: 0.5,
|
||||
ctaFadeDur: 0.7,
|
||||
|
||||
// Phase 2: keyword glows handled per-element via data attrs
|
||||
|
||||
// Phase 3: scroll + carousel
|
||||
phase2StartAt: 3.08,
|
||||
scrollDur: 1.0,
|
||||
scrollDistance: 280,
|
||||
carouselFadeDur: 0.5,
|
||||
|
||||
// Phase 4: pop-out + spotlight
|
||||
popStart: 3.58,
|
||||
popAttackDur: 0.67, // ramp 0 → 1
|
||||
popDecayDur: 0.33, // ramp 1 → 0.5 near end
|
||||
popEnd: 8.84,
|
||||
popRestLevel: 0.5,
|
||||
};
|
||||
|
||||
const KEYWORD_REST_LEVEL = 0.14;
|
||||
const KEYWORD_SUSTAIN = 0.5; // seconds after ASR end before decay completes
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
// Set the static 3D tilt + initial scale via GSAP so subsequent
|
||||
// scale tweens preserve the rotation aliases. (If we left
|
||||
// `transform: rotateY(...) rotateX(...) scale(0.95)` in CSS, a GSAP
|
||||
// tween touching only `scale` could overwrite the full matrix and
|
||||
// lose the rotation. Owning the transform state in GSAP avoids the
|
||||
// problem entirely.)
|
||||
gsap.set(".page-card", {
|
||||
rotationY: -8,
|
||||
rotationX: 3,
|
||||
scale: 0.95,
|
||||
});
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 1: Page card entry + navbar/title/CTA fade-in
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
".page-card",
|
||||
{ scale: 1.0, duration: TIMING.cardEntryDur, ease: "power2.out" },
|
||||
TIMING.cardEntryStart,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
"#navbar",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: TIMING.navbarFadeDur, ease: "power2.out" },
|
||||
TIMING.navbarFadeAt,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
"#hero",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: TIMING.titleFadeDur, ease: "power2.out" },
|
||||
TIMING.titleFadeAt,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
"#cta-row",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: TIMING.ctaFadeDur, ease: "power2.out" },
|
||||
TIMING.ctaFadeAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 2: Keyword glows — two tweens per word.
|
||||
Attack (0 → 1) + decay (1 → REST_LEVEL). GSAP holds REST forever.
|
||||
---------------------------------------------------------------- */
|
||||
document.querySelectorAll(".kw").forEach((kw) => {
|
||||
const start = Number(kw.dataset.glowStart);
|
||||
const end = Number(kw.dataset.glowEnd);
|
||||
const peak = start + (end - start) / 2;
|
||||
const restAt = end + KEYWORD_SUSTAIN;
|
||||
|
||||
tl.fromTo(
|
||||
kw,
|
||||
{ "--glow": 0 },
|
||||
{ "--glow": 1.18, duration: peak - start, ease: "power2.out" },
|
||||
start,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
kw,
|
||||
{ "--glow": KEYWORD_REST_LEVEL, duration: restAt - peak, ease: "power2.out" },
|
||||
peak,
|
||||
);
|
||||
});
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 3: Scroll + carousel entry
|
||||
---------------------------------------------------------------- */
|
||||
tl.fromTo(
|
||||
"#scroll-content",
|
||||
{ y: 0 },
|
||||
{ y: -TIMING.scrollDistance, duration: TIMING.scrollDur, ease: "power2.inOut" },
|
||||
TIMING.phase2StartAt,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
"#carousel",
|
||||
{ opacity: 0, scale: 0.9 },
|
||||
{ opacity: 1, scale: 1.0, duration: TIMING.carouselFadeDur, ease: "power2.out" },
|
||||
TIMING.phase2StartAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 4: Pop-out (driven by --glow on .carousel-main) + spotlight
|
||||
---------------------------------------------------------------- */
|
||||
// Attack the pop-out glow.
|
||||
tl.fromTo(
|
||||
"#pop-target",
|
||||
{ "--glow": 0 },
|
||||
{ "--glow": 1, duration: TIMING.popAttackDur, ease: "power2.out" },
|
||||
TIMING.popStart,
|
||||
);
|
||||
|
||||
// Decay at the end of the scene (mirrors original phase2End-10 / phase2End ramp).
|
||||
tl.to(
|
||||
"#pop-target",
|
||||
{ "--glow": TIMING.popRestLevel, duration: TIMING.popDecayDur, ease: "power2.out" },
|
||||
TIMING.popEnd - TIMING.popDecayDur,
|
||||
);
|
||||
|
||||
// Spotlight fades in alongside the pop attack, decays with the pop.
|
||||
tl.fromTo(
|
||||
"#spotlight",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.5, ease: "power2.out" },
|
||||
TIMING.popStart,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#spotlight",
|
||||
{ opacity: TIMING.popRestLevel, duration: TIMING.popDecayDur, ease: "power2.out" },
|
||||
TIMING.popEnd - TIMING.popDecayDur,
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,729 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 01 — Counting Icon Burst</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (4 phases, 3.5 seconds total):
|
||||
0.00 – 0.17s Background visible with dark overlay; nothing else
|
||||
0.17 – 0.57s Four enriched icons enter staggered, clustered at startOffset 0.4
|
||||
(clock 0.17s · scissors 0.30s · video 0.43s · play 0.57s)
|
||||
0.47 – 1.47s Counter "0 → 90" with growing font size (0.20W → 0.42W);
|
||||
icons expand outward from 40% to 100% position
|
||||
0.50 – 2.33s Camera focus phase: scale 0.92 → 1.0
|
||||
2.33 – 3.50s Camera push phase: scale 1.0 → 1.08
|
||||
1.27s Percent symbol pops in with own spring
|
||||
|
||||
Continuous motion (running from t=0, gated by icon visibility):
|
||||
Clock minute hand: linear rotation 420° over 3.5s
|
||||
Scissors: ±15° sine oscillation, period ~1.7s
|
||||
Cutting line: stroke-dashoffset drifts -200 over 3.5s
|
||||
Video record dot: opacity + scale phase-offset sine pulses
|
||||
Play triangle: scale pulse ±8%, period ~2.6s
|
||||
ease + duration (no shared driver needed, just identical timing)
|
||||
- Counter text + number scale are emitted as seek-safe timeline keyframes
|
||||
- Background: local Pexels MP4 plus dark overlay
|
||||
- Per-icon entry uses GSAP fromTo on a nested .icon-entry wrapper
|
||||
so it never overwrites the .icon-pos expansion tween
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #060812;
|
||||
--text-primary: #ffffff;
|
||||
--accent-yellow: #edcb50;
|
||||
--record-red: #ef4444;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-dark);
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND (static gradient)
|
||||
============================================================ */
|
||||
.bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 30%, rgba(80, 100, 200, 0.25), transparent 60%),
|
||||
radial-gradient(ellipse at 70% 70%, rgba(180, 80, 150, 0.2), transparent 60%),
|
||||
linear-gradient(135deg, #0a0d1f 0%, #1a0d20 100%);
|
||||
}
|
||||
.bg-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.66);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CAMERA WRAPPER (GSAP-managed scale phases)
|
||||
============================================================ */
|
||||
.camera {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
ICONS — two nested wrappers per icon.
|
||||
.icon-pos is positioned at TARGET; GSAP x/y shifts toward center.
|
||||
.icon-entry tweens scale/opacity/rotation.
|
||||
============================================================ */
|
||||
.icons-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.icon-pos {
|
||||
position: absolute;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
will-change: transform;
|
||||
}
|
||||
.icon-entry {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
will-change: transform, opacity;
|
||||
/* initial scale(0) + opacity(0) set by GSAP fromTo */
|
||||
}
|
||||
.icon-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* SVG inner elements — class hooks for GSAP tweens */
|
||||
.clock-hand-min {
|
||||
transform-box: view-box;
|
||||
}
|
||||
.scissor-upper,
|
||||
.scissor-lower {
|
||||
transform-origin: 12px 12px;
|
||||
}
|
||||
.play-tri {
|
||||
transform-origin: 12px 12px;
|
||||
}
|
||||
.record-dot {
|
||||
transform-origin: 19px 8px;
|
||||
}
|
||||
.play-ring {
|
||||
transform-origin: 12px 12px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
COUNTER (absolute center, 3D entry)
|
||||
============================================================ */
|
||||
.counter-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
perspective: 2000px;
|
||||
perspective-origin: center center;
|
||||
}
|
||||
.counter-3d {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform, opacity;
|
||||
opacity: 0; /* GSAP entry fades in */
|
||||
}
|
||||
.counter-number {
|
||||
font-size: 806px; /* W × 0.42 — final size, GSAP scales from 0.20W */
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.9;
|
||||
font-variant-numeric: tabular-nums; /* prevents 1→2 digit jitter */
|
||||
color: var(--text-primary);
|
||||
display: inline-block;
|
||||
transform-origin: center bottom;
|
||||
will-change: transform;
|
||||
}
|
||||
.counter-percent {
|
||||
font-size: 269px; /* W × 0.14 */
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--accent-yellow);
|
||||
margin-left: 16px;
|
||||
display: inline-block;
|
||||
will-change: transform, opacity;
|
||||
opacity: 0; /* GSAP entry tweens in later */
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
VIGNETTE
|
||||
============================================================ */
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.5) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 400;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="3.5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="bg"></div>
|
||||
<div class="bg-overlay"></div>
|
||||
|
||||
<div
|
||||
class="camera clip"
|
||||
data-start="0"
|
||||
data-duration="3.5"
|
||||
data-track-index="1"
|
||||
id="camera-stage"
|
||||
>
|
||||
<div class="icons-stage">
|
||||
<!-- Clock -->
|
||||
<div class="icon-pos clock-pos" id="clock-pos">
|
||||
<div class="icon-entry clock-entry">
|
||||
<svg
|
||||
class="icon-svg"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
class="clock-ring"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="9"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="56.5"
|
||||
stroke-dashoffset="56.5"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
y1="12"
|
||||
x2="12"
|
||||
y2="8"
|
||||
stroke="white"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
class="clock-hand-min"
|
||||
x1="12"
|
||||
y1="12"
|
||||
x2="12"
|
||||
y2="6"
|
||||
stroke="#edcb50"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="1.5" fill="#edcb50" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scissors -->
|
||||
<div class="icon-pos scissors-pos" id="scissors-pos">
|
||||
<div class="icon-entry scissors-entry">
|
||||
<svg
|
||||
class="icon-svg"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g class="scissor-upper">
|
||||
<path
|
||||
d="M12 10.5L4.5 3L6.5 3L14 10.5"
|
||||
stroke="white"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<circle cx="6" cy="6" r="2.5" fill="none" stroke="white" stroke-width="1.5" />
|
||||
</g>
|
||||
<g class="scissor-lower">
|
||||
<path
|
||||
d="M12 13.5L4.5 21L6.5 21L14 13.5"
|
||||
stroke="white"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<circle cx="6" cy="18" r="2.5" fill="none" stroke="white" stroke-width="1.5" />
|
||||
</g>
|
||||
<line
|
||||
class="cutting-line"
|
||||
x1="14"
|
||||
y1="12"
|
||||
x2="20"
|
||||
y2="12"
|
||||
stroke="#edcb50"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="4 2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video -->
|
||||
<div class="icon-pos video-pos" id="video-pos">
|
||||
<div class="icon-entry video-entry">
|
||||
<svg
|
||||
class="icon-svg"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect
|
||||
class="video-frame"
|
||||
x="3"
|
||||
y="5"
|
||||
width="14"
|
||||
height="14"
|
||||
rx="2"
|
||||
stroke="white"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke-dasharray="56"
|
||||
stroke-dashoffset="56"
|
||||
/>
|
||||
<path d="M8 9L13 12L8 15V9Z" fill="white" />
|
||||
<circle class="record-dot" cx="19" cy="8" r="2.5" fill="#ef4444" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Play -->
|
||||
<div class="icon-pos play-pos" id="play-pos">
|
||||
<div class="icon-entry play-entry">
|
||||
<svg
|
||||
class="icon-svg"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
class="play-ring"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="9"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="56.5"
|
||||
stroke-dashoffset="56.5"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="7" fill="rgba(255,255,255,0.05)" />
|
||||
<path class="play-tri" d="M10 8L16 12L10 16V8Z" fill="#edcb50" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.icons-stage -->
|
||||
|
||||
<div class="counter-stage">
|
||||
<div class="counter-3d" id="counter-3d">
|
||||
<span class="counter-number" id="counter-number">0</span
|
||||
><span class="counter-percent" id="counter-percent">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.camera -->
|
||||
|
||||
<div class="vignette"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
CONSTANTS
|
||||
================================================================ */
|
||||
const W = 1920,
|
||||
H = 1080;
|
||||
const ICON_SIZE = 180;
|
||||
const CENTER_X = W / 2 - ICON_SIZE / 2; // 870
|
||||
const CENTER_Y = H / 2 - ICON_SIZE / 2; // 450
|
||||
|
||||
const TIMING = {
|
||||
// Phase 2 — icon entries (staggered)
|
||||
clockEntryAt: 0.17,
|
||||
scissorsEntryAt: 0.3,
|
||||
videoEntryAt: 0.43,
|
||||
playEntryAt: 0.57,
|
||||
entryDur: 0.55,
|
||||
|
||||
// Phase 3 — count + expansion (shared start, dur, ease)
|
||||
countAt: 0.47,
|
||||
countDur: 1.0,
|
||||
startOffset: 0.4,
|
||||
|
||||
// Counter 3D entry (slightly earlier than count starts)
|
||||
counterEntryAt: 0.27,
|
||||
counterEntryDur: 0.7,
|
||||
|
||||
// Percent symbol entry
|
||||
percentEntryAt: 1.27,
|
||||
percentEntryDur: 0.55,
|
||||
|
||||
// Phase 4 — camera
|
||||
cameraFocusAt: 0.5,
|
||||
cameraFocusDur: 1.83,
|
||||
cameraPushAt: 2.33,
|
||||
cameraPushDur: 1.17,
|
||||
};
|
||||
|
||||
const ICONS = [
|
||||
{
|
||||
name: "clock",
|
||||
sel: "#clock-pos",
|
||||
targetX: W * 0.06,
|
||||
targetY: H * 0.18,
|
||||
delay: TIMING.clockEntryAt,
|
||||
entryRotation: -180,
|
||||
entryEase: "back.out(1.5)",
|
||||
},
|
||||
{
|
||||
name: "scissors",
|
||||
sel: "#scissors-pos",
|
||||
targetX: W * 0.85,
|
||||
targetY: H * 0.2,
|
||||
delay: TIMING.scissorsEntryAt,
|
||||
entryRotation: 0,
|
||||
entryEase: "back.out(1.5)",
|
||||
},
|
||||
{
|
||||
name: "video",
|
||||
sel: "#video-pos",
|
||||
targetX: W * 0.04,
|
||||
targetY: H * 0.68,
|
||||
delay: TIMING.videoEntryAt,
|
||||
entryRotation: 0,
|
||||
entryEase: "back.out(1.4)",
|
||||
},
|
||||
{
|
||||
name: "play",
|
||||
sel: "#play-pos",
|
||||
targetX: W * 0.88,
|
||||
targetY: H * 0.65,
|
||||
delay: TIMING.playEntryAt,
|
||||
entryRotation: 90,
|
||||
entryEase: "back.out(1.5)",
|
||||
},
|
||||
];
|
||||
|
||||
/* ================================================================
|
||||
SET ICON TARGET POSITIONS + INITIAL EXPANSION OFFSETS
|
||||
================================================================ */
|
||||
ICONS.forEach(({ sel, targetX, targetY }) => {
|
||||
const el = document.querySelector(sel);
|
||||
el.style.left = targetX + "px";
|
||||
el.style.top = targetY + "px";
|
||||
|
||||
// Initial offset for START_OFFSET = 0.4:
|
||||
// position = target + (center - target) * (1 - 0.4) = target + 0.6 * (center - target)
|
||||
const offsetX = (CENTER_X - targetX) * (1 - TIMING.startOffset);
|
||||
const offsetY = (CENTER_Y - targetY) * (1 - TIMING.startOffset);
|
||||
gsap.set(el, { x: offsetX, y: offsetY });
|
||||
});
|
||||
|
||||
/* Set initial entry state on each .icon-entry */
|
||||
ICONS.forEach(({ name, entryRotation }) => {
|
||||
gsap.set(`.${name}-entry`, {
|
||||
scale: 0,
|
||||
opacity: 0,
|
||||
rotation: entryRotation,
|
||||
});
|
||||
});
|
||||
|
||||
/* Set initial background and camera scale + subtle pan baseline */
|
||||
gsap.set(".bg", { scale: 1.05 });
|
||||
gsap.set(".camera", { scale: 0.92, x: 0, y: 2 });
|
||||
gsap.set("#counter-number", { scale: 0.2 / 0.42 });
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
tl.to(
|
||||
".bg",
|
||||
{
|
||||
scale: 1.07,
|
||||
x: 6,
|
||||
y: -4,
|
||||
duration: 3.5,
|
||||
ease: "sine.inOut",
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 2: Icon entries (staggered)
|
||||
---------------------------------------------------------------- */
|
||||
ICONS.forEach(({ name, delay, entryEase }) => {
|
||||
// Scale + opacity + rotation entry
|
||||
tl.to(
|
||||
`.${name}-entry`,
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 0.85,
|
||||
rotation: 0,
|
||||
duration: TIMING.entryDur,
|
||||
ease: entryEase,
|
||||
},
|
||||
delay,
|
||||
);
|
||||
});
|
||||
|
||||
// Stroke-draw the outline circles/rect on clock, video, and play during their entries
|
||||
tl.to(
|
||||
".clock-ring",
|
||||
{
|
||||
attr: { "stroke-dashoffset": 0 },
|
||||
duration: TIMING.entryDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.clockEntryAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".video-frame",
|
||||
{
|
||||
attr: { "stroke-dashoffset": 0 },
|
||||
duration: TIMING.entryDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.videoEntryAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".play-ring",
|
||||
{
|
||||
attr: { "stroke-dashoffset": 0 },
|
||||
duration: TIMING.entryDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.playEntryAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 3: Counter (with 3D entry) + expansion
|
||||
---------------------------------------------------------------- */
|
||||
// Counter 3D entry — translateZ + rotateY + scale + opacity, in parallel.
|
||||
tl.fromTo(
|
||||
"#counter-3d",
|
||||
{ rotationY: 10, scale: 0.7, z: -300, opacity: 0 },
|
||||
{
|
||||
rotationY: 0,
|
||||
scale: 1.0,
|
||||
z: 0,
|
||||
opacity: 1,
|
||||
duration: TIMING.counterEntryDur,
|
||||
ease: "power3.out",
|
||||
},
|
||||
TIMING.counterEntryAt,
|
||||
);
|
||||
|
||||
// Counter number + apparent size. Use discrete frame keyframes so
|
||||
// HyperFrames can seek directly to any sampled time.
|
||||
const counterEl = document.querySelector("#counter-number");
|
||||
const COUNT_STEPS = 30;
|
||||
const COUNT_START_SCALE = 0.2 / 0.42;
|
||||
|
||||
for (let i = 0; i <= COUNT_STEPS; i += 1) {
|
||||
const p = i / COUNT_STEPS;
|
||||
const eased = 1 - Math.pow(1 - p, 2.5);
|
||||
const number = Math.round(eased * 90);
|
||||
const scale = COUNT_START_SCALE + eased * (1 - COUNT_START_SCALE);
|
||||
|
||||
tl.set(
|
||||
counterEl,
|
||||
{
|
||||
textContent: String(number),
|
||||
scale,
|
||||
},
|
||||
TIMING.countAt + p * TIMING.countDur,
|
||||
);
|
||||
}
|
||||
|
||||
// Percent symbol enters later with its own spring.
|
||||
tl.fromTo(
|
||||
"#counter-percent",
|
||||
{ x: 60, rotation: 20, scale: 0.4, opacity: 0 },
|
||||
{
|
||||
x: 0,
|
||||
rotation: 0,
|
||||
scale: 1.0,
|
||||
opacity: 1,
|
||||
duration: TIMING.percentEntryDur,
|
||||
ease: "back.out(1.4)",
|
||||
},
|
||||
TIMING.percentEntryAt,
|
||||
);
|
||||
|
||||
// Icon expansion — per-icon, same start/dur/ease as counter for lock-step sync.
|
||||
ICONS.forEach(({ sel }) => {
|
||||
tl.to(
|
||||
sel,
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
duration: TIMING.countDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.countAt,
|
||||
);
|
||||
});
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 4: Multi-phase camera
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
".camera",
|
||||
{
|
||||
scale: 1.0,
|
||||
duration: TIMING.cameraFocusDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.cameraFocusAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".camera",
|
||||
{
|
||||
scale: 1.08,
|
||||
duration: TIMING.cameraPushDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.cameraPushAt,
|
||||
);
|
||||
|
||||
// Subtle deterministic pan from the the source camera micro-movement.
|
||||
tl.to(
|
||||
".camera",
|
||||
{
|
||||
x: 3,
|
||||
y: 1.6,
|
||||
duration: 3.5,
|
||||
ease: "sine.inOut",
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
CONTINUOUS INTERNAL SVG MOTION
|
||||
All four icons' inner animations are derived from tl.time() in
|
||||
a single shared onUpdate. Cheaper than 4-5 separate onUpdates,
|
||||
easier to inspect.
|
||||
---------------------------------------------------------------- */
|
||||
const SCISSOR_SPEED = 0.12 * 30; // = 3.6 rad/sec (sin freq)
|
||||
const SCISSOR_AMP = 15;
|
||||
const REC_OPACITY_SPEED = 0.15 * 30; // = 4.5 rad/sec
|
||||
const REC_SCALE_SPEED = 0.1 * 30; // = 3.0 rad/sec
|
||||
const REC_OPACITY_AMP = 0.3;
|
||||
const REC_OPACITY_BASE = 0.7;
|
||||
const REC_SCALE_AMP = 0.15;
|
||||
const PLAY_PULSE_SPEED = 0.08 * 30; // = 2.4 rad/sec
|
||||
const PLAY_PULSE_AMP = 0.08;
|
||||
const CUTTING_FLOW_SPEED = 0.5 * 30; // = 15 units/sec (for stroke-dashoffset)
|
||||
|
||||
const scissorUpper = document.querySelector(".scissor-upper");
|
||||
const scissorLower = document.querySelector(".scissor-lower");
|
||||
const recordDot = document.querySelector(".record-dot");
|
||||
const playTri = document.querySelector(".play-tri");
|
||||
const cuttingLine = document.querySelector(".cutting-line");
|
||||
|
||||
tl.to(
|
||||
{ tick: 0 },
|
||||
{
|
||||
tick: 1,
|
||||
duration: 3.5,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const t = tl.time();
|
||||
|
||||
// Scissors — symmetric oscillation, anchored to scissors delay
|
||||
const sciT = t - TIMING.scissorsEntryAt;
|
||||
const sciAngle = sciT > 0 ? Math.sin(sciT * SCISSOR_SPEED) * SCISSOR_AMP : 0;
|
||||
gsap.set(scissorUpper, { rotation: sciAngle });
|
||||
gsap.set(scissorLower, { rotation: -sciAngle });
|
||||
|
||||
// Cutting line — linear dash flow (negative = leftward drift)
|
||||
const cutT = t - TIMING.scissorsEntryAt;
|
||||
if (cutT > 0) {
|
||||
gsap.set(cuttingLine, {
|
||||
attr: { "stroke-dashoffset": -cutT * CUTTING_FLOW_SPEED },
|
||||
});
|
||||
}
|
||||
|
||||
// Record dot — phase-offset opacity and scale
|
||||
const recT = t - TIMING.videoEntryAt;
|
||||
if (recT > 0) {
|
||||
const recOpacity =
|
||||
Math.sin(recT * REC_OPACITY_SPEED) * REC_OPACITY_AMP + REC_OPACITY_BASE;
|
||||
const recScale = 1 + Math.sin(recT * REC_SCALE_SPEED) * REC_SCALE_AMP;
|
||||
gsap.set(recordDot, { opacity: recOpacity, scale: recScale });
|
||||
}
|
||||
|
||||
// Play triangle — gentle scale pulse
|
||||
const playT = t - TIMING.playEntryAt;
|
||||
if (playT > 0) {
|
||||
const playScale = 1 + Math.sin(playT * PLAY_PULSE_SPEED) * PLAY_PULSE_AMP;
|
||||
gsap.set(playTri, { scale: playScale });
|
||||
}
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
/* Clock minute hand — linear rotation, anchored to clock entry */
|
||||
tl.fromTo(
|
||||
".clock-hand-min",
|
||||
{ rotation: 0, svgOrigin: "12 12" },
|
||||
{
|
||||
rotation: 420, // 120 deg/sec × 3.5s, scaled for visible motion
|
||||
svgOrigin: "12 12",
|
||||
duration: 3.5 - TIMING.clockEntryAt,
|
||||
ease: "none",
|
||||
},
|
||||
TIMING.clockEntryAt,
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,352 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene — HyperFrames Messaging Multi-Phrase</title>
|
||||
|
||||
<!--
|
||||
Blueprint: messaging-multi-phrase (HyperFrames preview)
|
||||
HyperFrames-native version of the scene-9-multi-phrase concept.
|
||||
|
||||
Choreography (3 phrases, ~7.5 seconds total, content-driven duration):
|
||||
|
||||
Phrase 1 [0.00 – 3.86 s]
|
||||
"With an AI assistant " (21 chars × 0.083 s = 1.75 s)
|
||||
+ "you can trust" (13 chars × 0.083 s = 1.08 s, accent pink)
|
||||
+ hold 1.00 s
|
||||
→ total 1.75 + 1.08 + 1.00 ≈ 3.83 s (computed by JS)
|
||||
|
||||
Phrase 2 [3.86 – 5.27 s]
|
||||
"Instant " (8 chars × 0.083 s = 0.67 s)
|
||||
+ "insights" (8 chars × 0.083 s = 0.67 s, accent pink)
|
||||
+ hold 1.00 s
|
||||
→ total 0.67 + 0.67 + 1.00 ≈ 2.33 s, but starts at 3.86 → endTime ≈ 6.18 s
|
||||
|
||||
Phrase 3 [6.18 – 9.45 s]
|
||||
"Endless " (8 chars × 0.083 = 0.67 s)
|
||||
+ "possibilities" (13 chars × 0.083 = 1.08 s, accent pink)
|
||||
+ hold 2.00 s (longer — closing beat)
|
||||
→ total 0.67 + 1.08 + 2.00 ≈ 3.75 s, but starts at 6.18 → endTime ≈ 9.93 s
|
||||
|
||||
Continuous: cursor blink (square wave via `tl.time() % 1.0`)
|
||||
color, cursor blink. No per-phrase tweens.
|
||||
- No conditional DOM — phrase container exists from t=0 with empty text;
|
||||
onUpdate overwrites textContent in-place at each frame.
|
||||
- TIMELINE is a plain const computed once at script load (no useMemo).
|
||||
- Frames become seconds: charSpeed 2.5 frames → 0.083 s; hold 30 frames → 1.0 s.
|
||||
- Cursor blink: `(t % BLINK_CYCLE) < BLINK_CYCLE / 2 ? 1 : 0` inside the same
|
||||
onUpdate, not a separate component with `useCurrentFrame`.
|
||||
- textContent !== visible guard skips redundant DOM writes during hold windows.
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--text-primary: #ffffff;
|
||||
--text-accent: #32fff6;
|
||||
--text-accent-2: #a3ff7a;
|
||||
--bg: linear-gradient(
|
||||
135deg,
|
||||
#3a3a3a 0%,
|
||||
#17211f 30%,
|
||||
#0b2328 48%,
|
||||
#1f3518 70%,
|
||||
#343434 100%
|
||||
);
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
font-family:
|
||||
"Inter",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHRASE STAGE — one shared line with three inline children:
|
||||
.phrase-main primary-color portion
|
||||
.phrase-accent accent-color portion
|
||||
.phrase-cursor block cursor (color overridden per-frame)
|
||||
`white-space: pre` keeps the trailing space in textMain so the
|
||||
accent doesn't merge with the lead-in.
|
||||
============================================================ */
|
||||
.phrase-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 150px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
white-space: pre;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
.phrase-main {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.phrase-accent {
|
||||
color: var(--text-accent);
|
||||
text-shadow: 0 0 12px rgba(50, 255, 246, 0.28);
|
||||
}
|
||||
.phrase-cursor {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
width: 8px;
|
||||
height: 162px; /* ≈ 1.08 × fontSize */
|
||||
background: var(--text-primary);
|
||||
margin-left: 12px;
|
||||
vertical-align: middle;
|
||||
transform: translateY(10px); /* baseline alignment */
|
||||
will-change: opacity, background-color;
|
||||
box-shadow: 0 0 12px rgba(50, 255, 246, 0.36);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND TINT — subtle radial glow so pure black doesn't
|
||||
feel sterile. Drops to ~rgb(8,8,12) at corners.
|
||||
============================================================ */
|
||||
.bg-tint {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 42% 48%, rgba(24, 217, 232, 0.32), transparent 42%),
|
||||
radial-gradient(ellipse at 58% 52%, rgba(123, 234, 90, 0.26), transparent 44%),
|
||||
radial-gradient(ellipse at 50% 50%, rgba(8, 16, 18, 0.42), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="7.5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
id="multi-phrase-scene"
|
||||
class="stage clip"
|
||||
data-start="0"
|
||||
data-duration="7.5"
|
||||
data-track-index="1"
|
||||
>
|
||||
<div class="bg-tint"></div>
|
||||
<div class="phrase-stage">
|
||||
<span class="phrase-main"></span><span class="phrase-accent"></span
|
||||
><span class="phrase-cursor"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* ============================================================
|
||||
SCRIPT — content + per-phrase timing config.
|
||||
Change SCRIPT and the timeline reshapes itself.
|
||||
============================================================ */
|
||||
const SCRIPT = [
|
||||
{
|
||||
textMain: "Build video with ",
|
||||
textAccent: "HTML",
|
||||
charSpeed: 0.083, // seconds per character (= 2.5 frames @ 30 fps)
|
||||
hold: 1.0,
|
||||
},
|
||||
{
|
||||
textMain: "Seek ",
|
||||
textAccent: "any frame",
|
||||
charSpeed: 0.083,
|
||||
hold: 1.0,
|
||||
},
|
||||
{
|
||||
textMain: "Render to ",
|
||||
textAccent: "MP4",
|
||||
charSpeed: 0.083,
|
||||
hold: 2.0, // longer closing beat
|
||||
},
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
TIMELINE — flat reduce, computed once.
|
||||
Each entry gains absolute startTime + endTime in seconds.
|
||||
============================================================ */
|
||||
let acc = 0;
|
||||
const TIMELINE = SCRIPT.map((item) => {
|
||||
const totalChars = item.textMain.length + item.textAccent.length;
|
||||
const typingDuration = totalChars * item.charSpeed;
|
||||
const totalDuration = typingDuration + item.hold;
|
||||
const start = acc;
|
||||
const end = start + totalDuration;
|
||||
acc = end;
|
||||
return Object.assign({}, item, {
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
typingDuration,
|
||||
});
|
||||
});
|
||||
const TOTAL = TIMELINE[TIMELINE.length - 1].endTime;
|
||||
// TOTAL ≈ 9.93s with the SCRIPT above. The composition root caps at 7.5s
|
||||
// (data-duration), which is the actual render window — phrases 1+2 fit
|
||||
// comfortably; phrase 3 holds long enough that any time after ~6.18s is
|
||||
// already on the closing line. Adjust SCRIPT durations to fit your budget.
|
||||
|
||||
/* ============================================================
|
||||
COLORS & BLINK
|
||||
============================================================ */
|
||||
const MAIN_COLOR = "#FFFFFF";
|
||||
const ACCENT_COLOR = "#32FFF6";
|
||||
const BLINK_CYCLE = 1.0; // seconds — 0.5s on, 0.5s off (matches 30-frame square wave)
|
||||
|
||||
const mainEl = document.querySelector(".phrase-main");
|
||||
const accentEl = document.querySelector(".phrase-accent");
|
||||
const cursorEl = document.querySelector(".phrase-cursor");
|
||||
|
||||
/* ============================================================
|
||||
MASTER ENGINE — single onUpdate writes text + cursor every frame.
|
||||
============================================================ */
|
||||
// Cache the most-recently-active phrase index for seek-safe scans.
|
||||
let lastIdx = 0;
|
||||
|
||||
function findPhrase(t) {
|
||||
// Hot path: same phrase still active
|
||||
if (t >= TIMELINE[lastIdx].startTime && t < TIMELINE[lastIdx].endTime) {
|
||||
return TIMELINE[lastIdx];
|
||||
}
|
||||
// Cold path: linear scan (handles forward + backward seeks)
|
||||
for (let i = 0; i < TIMELINE.length; i++) {
|
||||
if (t >= TIMELINE[i].startTime && t < TIMELINE[i].endTime) {
|
||||
lastIdx = i;
|
||||
return TIMELINE[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderAt(t) {
|
||||
// ----- Cursor blink (square wave, runs always) -----
|
||||
cursorEl.style.opacity = t % BLINK_CYCLE < BLINK_CYCLE / 2 ? "1" : "0";
|
||||
|
||||
// ----- Find current phrase -----
|
||||
const phrase = findPhrase(t);
|
||||
if (!phrase) {
|
||||
if (mainEl.textContent !== "") mainEl.textContent = "";
|
||||
if (accentEl.textContent !== "") accentEl.textContent = "";
|
||||
cursorEl.style.background = MAIN_COLOR;
|
||||
return;
|
||||
}
|
||||
|
||||
// ----- Compute visible characters -----
|
||||
const activeT = t - phrase.startTime;
|
||||
const charIdx = Math.floor(activeT / phrase.charSpeed);
|
||||
const mainLen = phrase.textMain.length;
|
||||
|
||||
const visMain = phrase.textMain.slice(0, Math.min(charIdx, mainLen));
|
||||
const accentLen = Math.max(0, charIdx - mainLen);
|
||||
const visAccent = phrase.textAccent.slice(0, accentLen);
|
||||
|
||||
// ----- DOM writes (guarded) -----
|
||||
if (mainEl.textContent !== visMain) mainEl.textContent = visMain;
|
||||
if (accentEl.textContent !== visAccent) accentEl.textContent = visAccent;
|
||||
|
||||
// ----- Cursor color follows active segment -----
|
||||
const inAccent = visMain.length === mainLen && visAccent.length > 0;
|
||||
cursorEl.style.background = inAccent ? ACCENT_COLOR : MAIN_COLOR;
|
||||
}
|
||||
|
||||
tl.to(
|
||||
{ tick: 0 },
|
||||
{
|
||||
tick: 1,
|
||||
duration: 7.5, // matches data-duration; clips the timeline
|
||||
ease: "none",
|
||||
onUpdate: () => renderAt(tl.time()),
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
const pauseTimeline = tl.pause.bind(tl);
|
||||
tl.pause = (time, suppressEvents) => {
|
||||
const result = pauseTimeline(time, suppressEvents);
|
||||
if (typeof time === "number") renderAt(time);
|
||||
return result;
|
||||
};
|
||||
|
||||
function wrapSeekOwner(owner) {
|
||||
if (!owner || owner.__multiPhraseSeekWrapped) return;
|
||||
let currentSeek = owner.seek;
|
||||
Object.defineProperty(owner, "seek", {
|
||||
configurable: true,
|
||||
get: () => currentSeek,
|
||||
set: (value) => {
|
||||
if (typeof value !== "function") {
|
||||
currentSeek = value;
|
||||
return;
|
||||
}
|
||||
currentSeek = function (time) {
|
||||
const result = value.apply(this, arguments);
|
||||
if (typeof time === "number") renderAt(time);
|
||||
return result;
|
||||
};
|
||||
},
|
||||
});
|
||||
owner.seek = currentSeek;
|
||||
owner.__multiPhraseSeekWrapped = true;
|
||||
}
|
||||
|
||||
function installWindowSeekHook(key) {
|
||||
if (window[key]) {
|
||||
wrapSeekOwner(window[key]);
|
||||
return;
|
||||
}
|
||||
let pendingOwner;
|
||||
Object.defineProperty(window, key, {
|
||||
configurable: true,
|
||||
get: () => pendingOwner,
|
||||
set: (value) => {
|
||||
pendingOwner = value;
|
||||
wrapSeekOwner(value);
|
||||
},
|
||||
});
|
||||
}
|
||||
installWindowSeekHook("__player");
|
||||
installWindowSeekHook("__hf");
|
||||
|
||||
renderAt(0);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,779 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 07 — HyperFrames Video Kinetic Text Pivot</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (4 phases, 6.5 seconds total):
|
||||
0.17 – 0.87s "HyperFrames" badge fades in (top)
|
||||
0.17 – 0.87s Demo screen card enters centered (scale 0.6 → 1)
|
||||
2.20 – 3.00s Video slides left to 32% W (scale → 0.85); MP4 pops in on right
|
||||
with 5-layer green depth stack and bouncy back.out(1.6)
|
||||
3.00 – 3.86s MP4 breathes (±2% scale at ~1.9 Hz)
|
||||
3.86 – 4.46s Video and MP4 slide off left + fade; typing stage fades in center
|
||||
4.36 – 5.13s "HTML pages become video" types char-by-char (23 chars at 30 ch/s)
|
||||
Accent words "pages" and "video" in brand green
|
||||
4.83 – 5.43s Gradient pill (purple → green) scales in behind line 2 with glow halo
|
||||
5.13 – 5.63s "frame by frame." types char-by-char inside the pill
|
||||
0 – 6.50s Continuous: video float, cursor blink, stat breath
|
||||
(each gated by time window)
|
||||
so entry, continuous motion, and static tilt never overwrite each other
|
||||
- Conditional rendering ({frame > X && ...}) replaced by permanent DOM +
|
||||
opacity 0 + gates inside the scene-ticker onUpdate
|
||||
- Cursor blink derived from tl.time() via Math.floor(t * 2) % 2
|
||||
- Demo screen is a self-contained CSS scene (.video-scene); to showcase real
|
||||
footage, drop a local mp4 back in as a muted <video class="clip"> (see
|
||||
the note at the .video-content markup)
|
||||
- 3D depth layers built once at composition setup with document.createElement
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #0a0a18;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.65);
|
||||
--brand-purple: #7c3aed;
|
||||
--brand-purple-soft: #a78bfa;
|
||||
--brand-green: #22c55e;
|
||||
--brand-purple-glow: rgba(124, 58, 237, 1);
|
||||
--brand-green-glow: rgba(34, 197, 94, 1);
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-dark);
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND + AMBIENT GLOW
|
||||
============================================================ */
|
||||
.bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 50%, rgba(40, 30, 80, 0.5), transparent 65%),
|
||||
linear-gradient(135deg, #0a0a18 0%, #160a1f 100%);
|
||||
}
|
||||
.ambient-glow {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1536px;
|
||||
height: 864px;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(147, 51, 234, 0.13) 0%,
|
||||
rgba(34, 197, 94, 0.08) 30%,
|
||||
transparent 70%
|
||||
);
|
||||
filter: blur(80px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BADGE
|
||||
============================================================ */
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 42px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 32px;
|
||||
font-size: 112px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
text-shadow:
|
||||
0 0 32px rgba(248, 250, 252, 0.12),
|
||||
0 0 72px rgba(24, 217, 232, 0.1);
|
||||
will-change: transform, opacity;
|
||||
/* initial opacity 0 + scale 0.9 via gsap.set */
|
||||
}
|
||||
.badge .accent {
|
||||
color: var(--brand-purple-soft);
|
||||
background: linear-gradient(135deg, var(--brand-purple-soft) 0%, var(--brand-purple) 72%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
filter: drop-shadow(0 0 28px rgba(124, 58, 237, 0.34));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
VIDEO CARD — three nested wrappers
|
||||
============================================================ */
|
||||
.video-pos {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1040px;
|
||||
height: 585px; /* 16:9 ratio, intentionally large for the pivot beat */
|
||||
margin-left: -520px; /* so x: W/2 centers it */
|
||||
margin-top: -292.5px;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.video-float {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
.video-tilt {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(5deg) rotateY(15deg);
|
||||
}
|
||||
.video-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 30px 60px rgba(0, 0, 0, 0.5),
|
||||
0 0 80px rgba(147, 51, 234, 0.2),
|
||||
0 0 120px rgba(34, 197, 94, 0.15);
|
||||
position: relative;
|
||||
}
|
||||
.video-scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, rgba(255, 200, 100, 0.4), transparent 50%),
|
||||
linear-gradient(135deg, #1a3a5f 0%, #2a4570 50%, #3a2a5a 100%);
|
||||
}
|
||||
.video-scene::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 40%;
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #fbbf24, #f97316);
|
||||
opacity: 0.6;
|
||||
filter: blur(8px);
|
||||
}
|
||||
.video-caption {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 24px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.video-caption .accent {
|
||||
color: var(--brand-green);
|
||||
}
|
||||
.video-reflection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
MP4 STAT — three nested wrappers + depth stack
|
||||
============================================================ */
|
||||
.stat-pos {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.stat-breath {
|
||||
will-change: transform;
|
||||
}
|
||||
.stat-tilt {
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(5deg) rotateY(-15deg);
|
||||
}
|
||||
.depth-stack {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: 340px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1;
|
||||
}
|
||||
.depth-stack .depth-layer.front {
|
||||
position: relative;
|
||||
color: var(--brand-green);
|
||||
}
|
||||
.depth-stack .depth-layer.back {
|
||||
position: absolute;
|
||||
top: var(--top);
|
||||
left: var(--left);
|
||||
color: rgba(34, 197, 94, var(--alpha));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
TYPING STAGE — center, fades in during pivot
|
||||
============================================================ */
|
||||
.typing-stage {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.typing-tilt {
|
||||
perspective: 1200px;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateY(-15deg) rotateX(5deg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
}
|
||||
.line1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
.seg {
|
||||
font-size: 124px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
white-space: pre;
|
||||
line-height: 1;
|
||||
}
|
||||
.seg.accent,
|
||||
.seg.accent2 {
|
||||
color: var(--brand-green);
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 124px;
|
||||
background-color: var(--text-primary);
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
transform: translateY(4px);
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.cursor.green {
|
||||
background-color: var(--brand-green);
|
||||
}
|
||||
|
||||
.line2-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 64px;
|
||||
}
|
||||
.pill-bg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
width: calc(100% + 80px);
|
||||
height: calc(100% + 40px);
|
||||
border-radius: 80px;
|
||||
background: linear-gradient(90deg, var(--brand-purple) 0%, var(--brand-green) 100%);
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.pill-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% + 100px);
|
||||
height: calc(100% + 60px);
|
||||
border-radius: 80px;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
var(--brand-purple-glow) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
opacity: 0;
|
||||
filter: blur(30px);
|
||||
z-index: -2;
|
||||
will-change: opacity;
|
||||
}
|
||||
.line2-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.seg.line2 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
VIGNETTE
|
||||
============================================================ */
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.5) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 400;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="6.5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
class="stage clip"
|
||||
data-start="0"
|
||||
data-duration="6.5"
|
||||
data-track-index="1"
|
||||
id="scene-stage"
|
||||
>
|
||||
<div class="bg"></div>
|
||||
<div class="ambient-glow"></div>
|
||||
|
||||
<div class="badge" id="badge">Hyper<span class="accent">Frames</span></div>
|
||||
|
||||
<!-- Video card -->
|
||||
<div class="video-pos" id="video-pos" data-layout-allow-overflow>
|
||||
<div class="video-float" id="video-float">
|
||||
<div class="video-tilt">
|
||||
<div class="video-content">
|
||||
<div class="video-scene"></div>
|
||||
<!-- To showcase real footage, add a local mp4 on top of .video-scene:
|
||||
<video id="demo-video" class="clip" src="assets/demo.mp4"
|
||||
style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover"
|
||||
data-start="0.17" data-duration="3.69" data-track-index="2"
|
||||
data-media-start="0" muted playsinline></video> -->
|
||||
<div class="video-caption">
|
||||
HTML, CSS and JS become <span class="accent">video</span>
|
||||
</div>
|
||||
<div class="video-reflection"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MP4 stat -->
|
||||
<div class="stat-pos" id="stat-pos" data-layout-allow-overflow>
|
||||
<div class="stat-breath" id="stat-breath">
|
||||
<div class="stat-tilt">
|
||||
<div class="depth-stack" id="depth-stack-97" data-text="MP4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typing -->
|
||||
<div class="typing-stage" id="typing-stage">
|
||||
<div class="typing-tilt">
|
||||
<div class="line1">
|
||||
<span class="seg main">HTML </span><span class="seg accent">pages</span
|
||||
><span class="seg suffix"> become </span><span class="seg accent2">video</span
|
||||
><span class="cursor cursor1" id="cursor1"></span>
|
||||
</div>
|
||||
<div class="line2-wrap">
|
||||
<div class="pill-bg" id="pill-bg"></div>
|
||||
<div class="pill-glow" id="pill-glow"></div>
|
||||
<div class="line2-content">
|
||||
<span class="seg line2">frame by frame.</span
|
||||
><span class="cursor cursor2" id="cursor2"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vignette"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
CONSTANTS
|
||||
================================================================ */
|
||||
const W = 1920,
|
||||
H = 1080;
|
||||
const TOTAL_DUR = 6.5;
|
||||
|
||||
/* ASR-anchored timing (the source source frame counts converted to seconds at 30fps). */
|
||||
const TIMING = {
|
||||
// Phase 1: video + badge enter
|
||||
badgeAt: 0.17,
|
||||
badgeDur: 0.7,
|
||||
videoEntryAt: 0.17,
|
||||
videoEntryDur: 0.7,
|
||||
|
||||
// Phase 2: video slides, MP4 appears
|
||||
slideAt: 2.2,
|
||||
videoSlideDur: 0.8,
|
||||
videoSlideX: W * 0.29,
|
||||
videoSlideScale: 0.92,
|
||||
|
||||
statEntryDur: 0.6,
|
||||
statEntryX: W * 0.6,
|
||||
statBreathAt: 3.0, // entry settles by here, breath begins
|
||||
statBreathDur: 0.86, // until exit at 3.86
|
||||
|
||||
// Phase 3: pivot — both exit, typing begins
|
||||
pivotAt: 3.86,
|
||||
exitDur: 0.6,
|
||||
videoExitX: -W * 0.5,
|
||||
statExitX: -W * 0.7,
|
||||
typingStageDur: 0.5,
|
||||
|
||||
// Phase 3 typing
|
||||
typingStartAt: 4.36, // pivotAt + 0.5 s stage settle
|
||||
typeRate: 30, // chars/sec
|
||||
|
||||
// Phase 4: pill
|
||||
pillAt: 4.83, // typing reaches line2 at this time
|
||||
pillDur: 0.6,
|
||||
pillGlowDur: 0.8,
|
||||
};
|
||||
|
||||
/* Text segments and boundaries */
|
||||
const SEG = {
|
||||
main: "HTML ",
|
||||
accent: "pages",
|
||||
suffix: " become ",
|
||||
accent2: "video",
|
||||
line2: "frame by frame.",
|
||||
};
|
||||
const BOUNDS = {
|
||||
mainEnd: SEG.main.length, // 5
|
||||
accentEnd: SEG.main.length + SEG.accent.length, // 10
|
||||
suffixEnd: SEG.main.length + SEG.accent.length + SEG.suffix.length, // 18
|
||||
accent2End: SEG.main.length + SEG.accent.length + SEG.suffix.length + SEG.accent2.length, // 23
|
||||
line2End:
|
||||
SEG.main.length +
|
||||
SEG.accent.length +
|
||||
SEG.suffix.length +
|
||||
SEG.accent2.length +
|
||||
SEG.line2.length, // 38
|
||||
};
|
||||
const TYPING_DUR = (BOUNDS.line2End - 0) / TIMING.typeRate; // ≈ 1.3 s
|
||||
|
||||
/* ================================================================
|
||||
BUILD DOM — 5-layer depth stack for "MP4"
|
||||
================================================================ */
|
||||
const LAYER_COUNT = 5;
|
||||
const OFFSET_X = 1;
|
||||
const OFFSET_Y = 2;
|
||||
const depthStack = document.getElementById("depth-stack-97");
|
||||
const depthText = depthStack.dataset.text;
|
||||
for (let i = 0; i < LAYER_COUNT; i++) {
|
||||
const layer = document.createElement("div");
|
||||
layer.className = "depth-layer " + (i === LAYER_COUNT - 1 ? "front" : "back");
|
||||
layer.textContent = depthText;
|
||||
if (i < LAYER_COUNT - 1) {
|
||||
layer.setAttribute("aria-hidden", "true");
|
||||
const alpha = 0.62 + 0.08 * (LAYER_COUNT - 2 - i);
|
||||
layer.style.setProperty("--alpha", alpha);
|
||||
layer.style.setProperty("--top", i * OFFSET_Y + "px");
|
||||
layer.style.setProperty("--left", -i * OFFSET_X + "px");
|
||||
}
|
||||
depthStack.appendChild(layer);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
INITIAL STATES (via gsap.set, before the timeline runs)
|
||||
================================================================ */
|
||||
gsap.set("#badge", { opacity: 0, scale: 0.9 });
|
||||
|
||||
// Video starts centered (x = W/2, y = H/2 via the .video-pos margin trick)
|
||||
gsap.set("#video-pos", { x: W / 2, y: H / 2, scale: 0.6, opacity: 0 });
|
||||
|
||||
// Stat starts off-right + below + small + invisible
|
||||
gsap.set("#stat-pos", { x: TIMING.statEntryX, y: H * 0.42 + 60, scale: 0.3, opacity: 0 });
|
||||
|
||||
gsap.set("#typing-stage", { scale: 0.9, opacity: 0 });
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 1: Badge + Video entry
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
"#badge",
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: TIMING.badgeDur,
|
||||
ease: "power3.out",
|
||||
},
|
||||
TIMING.badgeAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#video-pos",
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.videoEntryDur,
|
||||
ease: "power3.out", // spring(stiffness:80, damping:15)
|
||||
},
|
||||
TIMING.videoEntryAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 2: Video slides left, MP4 appears on right
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
"#video-pos",
|
||||
{
|
||||
x: TIMING.videoSlideX,
|
||||
scale: TIMING.videoSlideScale,
|
||||
duration: TIMING.videoSlideDur,
|
||||
ease: "power3.out", // spring(stiffness:100, damping:18)
|
||||
},
|
||||
TIMING.slideAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#stat-pos",
|
||||
{
|
||||
y: H * 0.42, // rises to final y
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.statEntryDur,
|
||||
ease: "back.out(1.6)", // spring(stiffness:150, damping:12)
|
||||
},
|
||||
TIMING.slideAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 3: Pivot — video + stat exit; typing fades in
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
"#video-pos",
|
||||
{
|
||||
x: TIMING.videoExitX,
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
duration: TIMING.exitDur,
|
||||
ease: "power3.out", // spring(stiffness:120, damping:20)
|
||||
},
|
||||
TIMING.pivotAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#stat-pos",
|
||||
{
|
||||
x: TIMING.statExitX,
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
duration: TIMING.exitDur,
|
||||
ease: "power3.out",
|
||||
},
|
||||
TIMING.pivotAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#typing-stage",
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.typingStageDur,
|
||||
ease: "power3.out", // spring(stiffness:100, damping:15)
|
||||
},
|
||||
TIMING.pivotAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 3 (continued): Character-by-character typing
|
||||
---------------------------------------------------------------- */
|
||||
const segMain = document.querySelector(".seg.main");
|
||||
const segAccent = document.querySelector(".seg.accent");
|
||||
const segSuffix = document.querySelector(".seg.suffix");
|
||||
const segAccent2 = document.querySelector(".seg.accent2");
|
||||
const segLine2 = document.querySelector(".seg.line2");
|
||||
|
||||
const typeProxy = { idx: 0 };
|
||||
function typedIndexAt(t) {
|
||||
if (t < TIMING.typingStartAt) return 0;
|
||||
return Math.min(BOUNDS.line2End, Math.floor((t - TIMING.typingStartAt) * TIMING.typeRate));
|
||||
}
|
||||
|
||||
function renderTypedText(i) {
|
||||
const m = SEG.main.slice(0, Math.min(i, BOUNDS.mainEnd));
|
||||
const a = SEG.accent.slice(0, Math.max(0, Math.min(i - BOUNDS.mainEnd, SEG.accent.length)));
|
||||
const s = SEG.suffix.slice(
|
||||
0,
|
||||
Math.max(0, Math.min(i - BOUNDS.accentEnd, SEG.suffix.length)),
|
||||
);
|
||||
const a2 = SEG.accent2.slice(
|
||||
0,
|
||||
Math.max(0, Math.min(i - BOUNDS.suffixEnd, SEG.accent2.length)),
|
||||
);
|
||||
const l2 = SEG.line2.slice(
|
||||
0,
|
||||
Math.max(0, Math.min(i - BOUNDS.accent2End, SEG.line2.length)),
|
||||
);
|
||||
if (segMain.textContent !== m) segMain.textContent = m;
|
||||
if (segAccent.textContent !== a) segAccent.textContent = a;
|
||||
if (segSuffix.textContent !== s) segSuffix.textContent = s;
|
||||
if (segAccent2.textContent !== a2) segAccent2.textContent = a2;
|
||||
if (segLine2.textContent !== l2) segLine2.textContent = l2;
|
||||
}
|
||||
|
||||
tl.to(
|
||||
typeProxy,
|
||||
{
|
||||
idx: BOUNDS.line2End,
|
||||
duration: TYPING_DUR,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
renderTypedText(Math.floor(typeProxy.idx));
|
||||
},
|
||||
},
|
||||
TIMING.typingStartAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 4: Gradient pill behind line 2
|
||||
---------------------------------------------------------------- */
|
||||
gsap.set("#pill-bg", { scaleX: 0, scaleY: 0.5, opacity: 0 });
|
||||
gsap.set("#pill-glow", { opacity: 0 });
|
||||
|
||||
tl.to(
|
||||
"#pill-bg",
|
||||
{
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
opacity: 0.9,
|
||||
duration: TIMING.pillDur,
|
||||
ease: "power3.out", // spring(stiffness:80, damping:15)
|
||||
},
|
||||
TIMING.pillAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#pill-glow",
|
||||
{
|
||||
opacity: 0.5,
|
||||
duration: TIMING.pillGlowDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.pillAt,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".line2-content",
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.18,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.pillAt + 0.3,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
CONTINUOUS SCENE-TICKER — float, breath, cursor blink
|
||||
All gated by time windows so they only fire when visible.
|
||||
---------------------------------------------------------------- */
|
||||
const videoFloat = document.getElementById("video-float");
|
||||
const statBreathEl = document.getElementById("stat-breath");
|
||||
const cursor1El = document.getElementById("cursor1");
|
||||
const cursor2El = document.getElementById("cursor2");
|
||||
|
||||
tl.to(
|
||||
{ tick: 0 },
|
||||
{
|
||||
tick: 1,
|
||||
duration: TOTAL_DUR,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const t = tl.time();
|
||||
const idx = typedIndexAt(t);
|
||||
renderTypedText(idx);
|
||||
|
||||
// Video float — only while video is visible (Phase 1 + 2)
|
||||
if (t < TIMING.pivotAt) {
|
||||
const floatY = Math.sin(t * 0.9) * 6;
|
||||
gsap.set(videoFloat, { y: floatY });
|
||||
}
|
||||
|
||||
// Stat breath — only while MP4 is settled and not yet exiting
|
||||
if (t > TIMING.statBreathAt && t < TIMING.pivotAt) {
|
||||
const breath = 1 + Math.sin((t - TIMING.statBreathAt) * 1.2) * 0.02;
|
||||
gsap.set(statBreathEl, { scale: breath });
|
||||
}
|
||||
|
||||
// Cursor blink — 1 Hz (Math.floor(t * 2) % 2 === 0 → on for 0.5s, off for 0.5s)
|
||||
const blink = Math.floor(t * 2) % 2 === 0 ? 1 : 0;
|
||||
|
||||
// Cursor 1: visible while typing line 1 (idx < accent2End)
|
||||
// Determine cursor color: green when typing accent segments, white otherwise.
|
||||
if (t >= TIMING.typingStartAt && idx < BOUNDS.accent2End) {
|
||||
const inAccent = idx >= BOUNDS.mainEnd && idx < BOUNDS.accentEnd;
|
||||
const inAccent2 = idx >= BOUNDS.suffixEnd && idx < BOUNDS.accent2End;
|
||||
const isGreen = inAccent || inAccent2;
|
||||
cursor1El.style.opacity = blink;
|
||||
if (cursor1El._lastGreen !== isGreen) {
|
||||
cursor1El.classList.toggle("green", isGreen);
|
||||
cursor1El._lastGreen = isGreen;
|
||||
}
|
||||
} else {
|
||||
cursor1El.style.opacity = 0;
|
||||
}
|
||||
|
||||
// Cursor 2: visible while typing line 2 (idx ≥ accent2End && idx < line2End)
|
||||
if (idx >= BOUNDS.accent2End && idx < BOUNDS.line2End) {
|
||||
cursor2El.style.opacity = blink;
|
||||
} else {
|
||||
cursor2El.style.opacity = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,861 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 03 — HyperFrames Anchor Chain Reveal</title>
|
||||
|
||||
<!--
|
||||
HyperFrames-native version of the scene-03-anchor-chain-reveal concept.
|
||||
Same 5-phase choreography, GSAP-driven, seek-safe.
|
||||
|
||||
Audio (assumed accompanying narration):
|
||||
"Meet HyperFrames, the HTML video composition toolkit for building,
|
||||
previewing, and rendering programmable motion."
|
||||
|
||||
Sub-shots (local time, 8s total):
|
||||
0.0 – 1.2s "HyperFrames" Brand reveal (hacker-flip)
|
||||
1.2 – 4.0s "HTML VIDEO" Text swap + rolling ticker
|
||||
4.0 – 6.3s "60FPS" Counter + capability cloud
|
||||
6.3 – 8.0s "Built for Motion" Capability strip
|
||||
|
||||
Placeholders (self-contained — no asset files needed):
|
||||
inline-SVG "HF" mark — brand logo (swap for your image)
|
||||
CSS initials circles — user avatars (edit CREATOR_INITIALS)
|
||||
CSS text chips — partner brand logos (edit --brand-name)
|
||||
Replace with real assets for production.
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
/* Palette — direct port of the the source COLORS constants */
|
||||
--text-primary: #ffffff;
|
||||
--text-muted: rgba(255, 255, 255, 0.45);
|
||||
--brand-green: #00e676;
|
||||
--bg-grad-from: #0a1a2e;
|
||||
--bg-grad-to: #1a0a2e;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND (port of <FrostedGlassBackground />)
|
||||
============================================================ */
|
||||
.bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 20%, rgba(0, 230, 118, 0.18), transparent 50%),
|
||||
radial-gradient(ellipse at 70% 80%, rgba(120, 60, 220, 0.18), transparent 50%),
|
||||
linear-gradient(135deg, var(--bg-grad-from), var(--bg-grad-to));
|
||||
}
|
||||
|
||||
/* Camera wrapper — port of <CameraSystem /> */
|
||||
.camera {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASES 1–3 (BrandReveal): logo + decode + swap + recenter
|
||||
============================================================ */
|
||||
.brand-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50; /* above cloud SVG (z:1), below logo (z:100) */
|
||||
}
|
||||
.anchor-shift {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 35px;
|
||||
/* GSAP tweens .x on this element for Phase-2 recenter */
|
||||
}
|
||||
.anchor-logo {
|
||||
position: relative;
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100; /* highest — sits above lines + avatars */
|
||||
opacity: 1;
|
||||
/* initial scale(0) set by GSAP fromTo — no CSS transform here */
|
||||
}
|
||||
.anchor-logo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 24px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
.anchor-text {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.phase1-text {
|
||||
display: flex;
|
||||
perspective: 800px; /* required for the per-glyph rotateX */
|
||||
white-space: nowrap;
|
||||
}
|
||||
.flip-glyph {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: 163px; /* 1920 × 0.085 — matches the source brandFontSize */
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.flip-glyph .ghost {
|
||||
opacity: 0;
|
||||
}
|
||||
.flip-glyph .anim {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
color: var(--text-primary);
|
||||
opacity: 0;
|
||||
transform: perspective(600px) rotateX(90deg);
|
||||
transform-origin: bottom;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.flip-glyph.space {
|
||||
min-width: 0.35em;
|
||||
}
|
||||
|
||||
.phase2-claim {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
.claim-rank {
|
||||
color: var(--brand-green);
|
||||
font-size: 163px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.claim-ai,
|
||||
.claim-video {
|
||||
color: var(--text-primary);
|
||||
font-size: 163px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Vertical ticker — port of <RollingWord /> */
|
||||
.ticker-window {
|
||||
height: 204px; /* fontSize × 1.25 = 163 × 1.25 ≈ 204 */
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.ticker-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
will-change: transform;
|
||||
}
|
||||
.ticker-item {
|
||||
height: 204px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-primary);
|
||||
font-weight: 900;
|
||||
font-size: 163px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASE 4 (CreatorCloud): counter + avatar cloud + lines
|
||||
============================================================ */
|
||||
.cloud-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10; /* below brand-stage's logo (z:100), below counter */
|
||||
opacity: 0; /* GSAP fades it in */
|
||||
}
|
||||
.counter-pin {
|
||||
position: absolute;
|
||||
top: 3%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
z-index: 60; /* above brand-stage so it overlays cleanly */
|
||||
}
|
||||
.count {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 900;
|
||||
font-size: 115px; /* 1920 × 0.06 */
|
||||
color: var(--brand-green);
|
||||
display: inline-block;
|
||||
min-width: 200px;
|
||||
text-align: right;
|
||||
}
|
||||
.count-suffix {
|
||||
font-weight: 900;
|
||||
font-size: 115px;
|
||||
color: var(--brand-green);
|
||||
opacity: 0; /* GSAP fades in after count */
|
||||
}
|
||||
.cloud-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.cloud-avatars {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.cloud-avatar {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
opacity: 0;
|
||||
/* initial scale(0) set by GSAP fromTo — no CSS transform here */
|
||||
background: linear-gradient(135deg, #6b8cff, #a6b3ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASE 5 (BrandShowcase): label + scrolling brand strip
|
||||
============================================================ */
|
||||
.brand-strip {
|
||||
position: absolute;
|
||||
bottom: 8%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0; /* GSAP fades + translateY in via fromTo */
|
||||
z-index: 70;
|
||||
}
|
||||
.brand-label {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
font-size: 27px; /* 1920 × 0.014 */
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.brand-strip-window {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.brand-strip-track {
|
||||
display: flex;
|
||||
gap: 110px;
|
||||
}
|
||||
.brand-logo {
|
||||
width: 170px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
/* initial scale(0.5) set by GSAP fromTo — no CSS transform here */
|
||||
}
|
||||
.brand-logo::before {
|
||||
content: var(--brand-name);
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
white-space: nowrap;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
.brand-nvidia {
|
||||
--brand-name: "NVIDIA";
|
||||
}
|
||||
.brand-visa {
|
||||
--brand-name: "VISA";
|
||||
}
|
||||
.brand-zoominfo {
|
||||
--brand-name: "ZOOMINFO";
|
||||
}
|
||||
.brand-github {
|
||||
--brand-name: "GITHUB";
|
||||
}
|
||||
|
||||
/* Vignette overlay */
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.45) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 200;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="bg"></div>
|
||||
|
||||
<div class="camera">
|
||||
<!-- =====================================================
|
||||
PHASES 1–3: BrandReveal (logo + hacker-flip + swap)
|
||||
===================================================== -->
|
||||
<div
|
||||
id="phase-anchor"
|
||||
class="brand-stage clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
>
|
||||
<div class="anchor-shift" data-layout-allow-overflow>
|
||||
<!-- Anchor logo — persists across all phases -->
|
||||
<div class="anchor-logo">
|
||||
<!-- Inline-SVG placeholder mark — swap for your logo image -->
|
||||
<svg
|
||||
class="logo-mark"
|
||||
viewBox="0 0 100 100"
|
||||
width="100%"
|
||||
height="100%"
|
||||
role="img"
|
||||
aria-label="hyperframes logo"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="hfMark" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#8b7bff" />
|
||||
<stop offset="1" stop-color="#3ddc97" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="92" height="92" rx="22" fill="url(#hfMark)" />
|
||||
<text
|
||||
x="50"
|
||||
y="63"
|
||||
text-anchor="middle"
|
||||
font-family="Inter, system-ui, sans-serif"
|
||||
font-size="40"
|
||||
font-weight="800"
|
||||
fill="#fff"
|
||||
>
|
||||
HF
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Swappable text zone -->
|
||||
<div class="anchor-text">
|
||||
<!-- Phase 1: 'HyperFrames' decoded character by character -->
|
||||
<div class="phase1-text" aria-label="HyperFrames">
|
||||
<!-- Spans generated by the script below -->
|
||||
</div>
|
||||
|
||||
<!-- Phase 2: 'HTML video render/ship' claim -->
|
||||
<div class="phase2-claim">
|
||||
<span class="claim-rank">HTML</span>
|
||||
<span class="claim-ai">Video</span>
|
||||
<div class="claim-ticker">
|
||||
<div class="ticker-window" data-layout-allow-overflow>
|
||||
<div class="ticker-stack">
|
||||
<div class="ticker-item">render</div>
|
||||
<div class="ticker-item">ship</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =====================================================
|
||||
PHASE 4: CreatorCloud (counter + avatar ring + lines)
|
||||
===================================================== -->
|
||||
<div
|
||||
id="phase-cloud"
|
||||
class="cloud-stage clip"
|
||||
data-start="3.6"
|
||||
data-duration="4.4"
|
||||
data-track-index="2"
|
||||
>
|
||||
<div class="counter-pin">
|
||||
<span class="count">60</span><span class="count-suffix">FPS</span>
|
||||
</div>
|
||||
<svg class="cloud-lines" width="1920" height="1080">
|
||||
<defs>
|
||||
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#00e676" stop-opacity="0.8" />
|
||||
<stop offset="100%" stop-color="#00e676" stop-opacity="0.2" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- <line> elements generated by JS -->
|
||||
</svg>
|
||||
<div class="cloud-avatars">
|
||||
<!-- .cloud-avatar elements generated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =====================================================
|
||||
PHASE 5: BrandShowcase (label + scrolling logos)
|
||||
===================================================== -->
|
||||
<div
|
||||
id="phase-brands"
|
||||
class="brand-strip clip"
|
||||
data-start="6.3"
|
||||
data-duration="1.7"
|
||||
data-track-index="3"
|
||||
>
|
||||
<div class="brand-label">Trusted by Leading Brands</div>
|
||||
<div class="brand-strip-window" data-layout-allow-overflow>
|
||||
<div class="brand-strip-track">
|
||||
<div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
|
||||
<div class="brand-logo brand-visa" aria-label="Visa"></div>
|
||||
<div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
|
||||
<div class="brand-logo brand-github" aria-label="GitHub"></div>
|
||||
<div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
|
||||
<div class="brand-logo brand-visa" aria-label="Visa"></div>
|
||||
<div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
|
||||
<div class="brand-logo brand-github" aria-label="GitHub"></div>
|
||||
<div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
|
||||
<div class="brand-logo brand-visa" aria-label="Visa"></div>
|
||||
<div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
|
||||
<div class="brand-logo brand-github" aria-label="GitHub"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.camera -->
|
||||
|
||||
<div class="vignette"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
CONSTANTS — all derived once, never recomputed at tween time.
|
||||
Composition is 1920 × 1080, total duration 8.0s.
|
||||
================================================================ */
|
||||
const W = 1920,
|
||||
H = 1080;
|
||||
const FPS_HASH = 60; // synthetic clock for the hacker-flip flicker hash only
|
||||
|
||||
// Self-contained placeholder data — swap for real asset paths in production.
|
||||
const CREATOR_INITIALS = ["JC", "MK", "AR", "TS", "LP", "DV", "NW", "SB", "KH", "EM"];
|
||||
|
||||
const TIMING = {
|
||||
// Phase 1 — logo pop + HyperFrames decode
|
||||
logoPop: 0.32,
|
||||
flipStart: 0.4, // logoPop + ~5/60
|
||||
flipStagger: 0.033, // 2 frames at 60fps per glyph
|
||||
flipDuration: 0.55,
|
||||
|
||||
// Phase 2 — slide-out + claim slide-in.
|
||||
swapTrigger: 1.38,
|
||||
swapDuration: 0.55,
|
||||
claimFadeIn: 1.65, // swapTrigger + ~8/60
|
||||
claimFadeDur: 0.27,
|
||||
|
||||
// Rolling ticker — render → ship.
|
||||
tickerTrigger: 2.35,
|
||||
tickerDuration: 0.45,
|
||||
|
||||
// Phase 3 — logo recenters (anchored ~0.25s before "trusted")
|
||||
recenterTrigger: 3.87,
|
||||
recenterDuration: 0.9,
|
||||
|
||||
// Phase 4 — cloud + counter.
|
||||
cloudFadeIn: 4.12,
|
||||
cloudFadeDur: 0.4,
|
||||
avatarEntryStart: 4.3,
|
||||
avatarStagger: 0.1,
|
||||
avatarEntryDur: 0.55,
|
||||
linesDelay: 0.2, // gap after last avatar
|
||||
linesDur: 0.45,
|
||||
lineStagger: 0.033,
|
||||
|
||||
counterStart: 4.92,
|
||||
counterDuration: 0.85,
|
||||
suffixDelay: 0.05,
|
||||
|
||||
// Phase 5 — brand logos
|
||||
brandStripStart: 6.5,
|
||||
brandStripFadeDur: 0.35,
|
||||
brandLogoStagger: 0.1,
|
||||
brandScrollDur: 1.4, // until end (8.0 - 6.5 - 0.1 buffer)
|
||||
};
|
||||
|
||||
// Layout constants — shared center keeps logo, avatars, and lines aligned.
|
||||
const RECENTER_OFFSET = -W * 0.12; // = -230.4 px — container shift in Phase 2
|
||||
const CENTER_OFFSET = 800; // Phase 3: logo center aligns with cloud center.
|
||||
|
||||
// Cloud geometry — logo final position and line origins derive from this center.
|
||||
const CLOUD_CENTER_X = W / 2;
|
||||
const CLOUD_CENTER_Y = H * 0.47; // = 507.6 px
|
||||
const VERTICAL_ADJUST = CLOUD_CENTER_Y - H / 2;
|
||||
const CLOUD_RADIUS_X = W * 0.25; // = 480 px
|
||||
const CLOUD_RADIUS_Y = H * 0.22; // = 237.6 px
|
||||
const AVATAR_COUNT = 10;
|
||||
|
||||
/* ================================================================
|
||||
BUILD DOM — flip glyphs, avatars, connection lines.
|
||||
Runs synchronously before the timeline registration.
|
||||
================================================================ */
|
||||
|
||||
// ---- Hacker-flip glyphs for "HyperFrames" -------------------------
|
||||
const phase1 = document.querySelector(".phase1-text");
|
||||
const decodeText = "HyperFrames";
|
||||
decodeText.split("").forEach((char, index) => {
|
||||
const span = document.createElement("span");
|
||||
span.className = "flip-glyph" + (char === " " ? " space" : "");
|
||||
span.dataset.char = char;
|
||||
span.dataset.index = String(index);
|
||||
const ghost = document.createElement("span");
|
||||
ghost.className = "ghost";
|
||||
ghost.textContent = char === " " ? " " : char;
|
||||
const anim = document.createElement("span");
|
||||
anim.className = "anim";
|
||||
anim.textContent = char === " " ? " " : char;
|
||||
span.append(ghost, anim);
|
||||
phase1.appendChild(span);
|
||||
});
|
||||
|
||||
// ---- Avatar cloud + connection lines ------------------------------
|
||||
const cloudLines = document.querySelector(".cloud-lines");
|
||||
const cloudAvatars = document.querySelector(".cloud-avatars");
|
||||
const avatarPositions = [];
|
||||
const avatars = CREATOR_INITIALS.slice(0, AVATAR_COUNT);
|
||||
|
||||
for (let i = 0; i < avatars.length; i++) {
|
||||
const angle = (i / avatars.length) * Math.PI * 2 - Math.PI / 2;
|
||||
const x = CLOUD_CENTER_X + Math.cos(angle) * CLOUD_RADIUS_X;
|
||||
const y = CLOUD_CENTER_Y + Math.sin(angle) * CLOUD_RADIUS_Y;
|
||||
avatarPositions.push({ x, y });
|
||||
|
||||
const size = 90 + (i % 3) * 15;
|
||||
const av = document.createElement("div");
|
||||
av.className = "cloud-avatar";
|
||||
av.style.left = x - size / 2 + "px";
|
||||
av.style.top = y - size / 2 + "px";
|
||||
av.style.width = size + "px";
|
||||
av.style.height = size + "px";
|
||||
av.dataset.index = String(i);
|
||||
// Initials placeholder — deterministic hue from index (no asset files).
|
||||
av.textContent = avatars[i];
|
||||
av.style.background = `linear-gradient(135deg, hsl(${(220 + i * 36) % 360}, 70%, 62%), hsl(${(260 + i * 36) % 360}, 70%, 46%))`;
|
||||
cloudAvatars.appendChild(av);
|
||||
|
||||
// SVG line — both endpoints start at center; will tween x2/y2 outward.
|
||||
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
||||
line.setAttribute("x1", CLOUD_CENTER_X);
|
||||
line.setAttribute("y1", CLOUD_CENTER_Y);
|
||||
line.setAttribute("x2", CLOUD_CENTER_X);
|
||||
line.setAttribute("y2", CLOUD_CENTER_Y);
|
||||
line.setAttribute("stroke", "#00e676");
|
||||
line.setAttribute("stroke-width", "2");
|
||||
line.setAttribute("stroke-dasharray", "4 4");
|
||||
line.setAttribute("stroke-opacity", "0");
|
||||
cloudLines.appendChild(line);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Camera: gentle initial zoom + drift. Finite, computed from duration.
|
||||
// ----------------------------------------------------------------
|
||||
tl.fromTo(".camera", { scale: 0.95 }, { scale: 1.0, duration: 0.95, ease: "power2.out" }, 0);
|
||||
|
||||
// Subtle drift across the whole 8s. Finite yoyo cycles, not infinite.
|
||||
// sin(frame * 0.003) cycle = 2π / 0.003 frames = 2094 frames ≈ 34.9s at 60fps.
|
||||
// Over 8s we see roughly a quarter cycle — model as a single slow yoyo.
|
||||
tl.fromTo(
|
||||
".camera",
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 3, y: 2, duration: 4, ease: "sine.inOut", yoyo: true, repeat: 1 },
|
||||
0,
|
||||
);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PHASE 1: Logo pop + HyperFrames decode
|
||||
// ----------------------------------------------------------------
|
||||
tl.fromTo(
|
||||
".anchor-logo",
|
||||
{ scale: 0 },
|
||||
{
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
ease: "back.out(1.4)", // spring(stiffness:180, damping:12)
|
||||
},
|
||||
TIMING.logoPop,
|
||||
);
|
||||
|
||||
// Hacker-flip per glyph. Each glyph has its own tween with onUpdate
|
||||
// that derives the visible character from time.
|
||||
const CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&";
|
||||
const FLICKER = 3; // frames between glyph reshuffles
|
||||
const REVEAL_AT = 0.6; // progress threshold to swap random → real
|
||||
|
||||
function pseudoHash(i, t) {
|
||||
// Cheap deterministic 32-bit mix — no Math.random, no Date.now
|
||||
return ((i * 374761393 + t * 668265263) >>> 0) % CHAR_POOL.length;
|
||||
}
|
||||
|
||||
document.querySelectorAll(".flip-glyph").forEach((glyph) => {
|
||||
const index = Number(glyph.dataset.index);
|
||||
const real = glyph.dataset.char === " " ? " " : glyph.dataset.char;
|
||||
const anim = glyph.querySelector(".anim");
|
||||
const start = TIMING.flipStart + index * TIMING.flipStagger;
|
||||
|
||||
tl.fromTo(
|
||||
anim,
|
||||
{ rotationX: 90, opacity: 0, "--p": 0 },
|
||||
{
|
||||
rotationX: 0,
|
||||
opacity: 1,
|
||||
"--p": 1,
|
||||
duration: TIMING.flipDuration,
|
||||
ease: "back.out(1.6)", // spring(stiffness:150, damping:14)
|
||||
onUpdate: function () {
|
||||
const p = Number(gsap.getProperty(anim, "--p"));
|
||||
if (p >= REVEAL_AT) {
|
||||
if (anim.textContent !== real) anim.textContent = real;
|
||||
} else {
|
||||
const localFrame = Math.floor((tl.time() - start) * FPS_HASH);
|
||||
const bucket = Math.max(0, Math.floor(localFrame / FLICKER));
|
||||
anim.textContent = CHAR_POOL[pseudoHash(index, bucket)];
|
||||
}
|
||||
},
|
||||
},
|
||||
start,
|
||||
);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PHASE 2: Swap — old text slides out, container shifts left,
|
||||
// claim fades in at the old text's origin.
|
||||
// ----------------------------------------------------------------
|
||||
tl.to(
|
||||
".phase1-text",
|
||||
{
|
||||
x: 200,
|
||||
opacity: 0,
|
||||
duration: TIMING.swapDuration * 0.5,
|
||||
ease: "power3.out",
|
||||
},
|
||||
TIMING.swapTrigger,
|
||||
);
|
||||
tl.set(".phase1-text .anim", { opacity: 0 }, TIMING.swapTrigger + TIMING.swapDuration * 0.5);
|
||||
|
||||
tl.to(
|
||||
".anchor-shift",
|
||||
{
|
||||
x: RECENTER_OFFSET,
|
||||
duration: TIMING.swapDuration,
|
||||
ease: "power3.out", // spring(stiffness:80, damping:18)
|
||||
},
|
||||
TIMING.swapTrigger,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".phase2-claim",
|
||||
{
|
||||
opacity: 1,
|
||||
duration: TIMING.claimFadeDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.claimFadeIn,
|
||||
);
|
||||
|
||||
// Rolling ticker — render → ship.
|
||||
tl.to(
|
||||
".ticker-stack",
|
||||
{
|
||||
y: "-=204", // one itemHeight
|
||||
duration: TIMING.tickerDuration,
|
||||
ease: "back.out(1.4)", // spring(stiffness:120, damping:14)
|
||||
},
|
||||
TIMING.tickerTrigger,
|
||||
);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PHASE 3: Logo recenters + text fades out.
|
||||
// ----------------------------------------------------------------
|
||||
tl.to(
|
||||
".anchor-text",
|
||||
{
|
||||
opacity: 0,
|
||||
duration: 0.3,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.recenterTrigger,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".anchor-logo",
|
||||
{
|
||||
x: CENTER_OFFSET,
|
||||
y: VERTICAL_ADJUST,
|
||||
duration: TIMING.recenterDuration,
|
||||
ease: "power2.out", // spring(stiffness:45, damping:22) — gentle
|
||||
},
|
||||
TIMING.recenterTrigger,
|
||||
);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PHASE 4: Avatar cloud + counter.
|
||||
// ----------------------------------------------------------------
|
||||
tl.to(
|
||||
".cloud-stage",
|
||||
{
|
||||
opacity: 1,
|
||||
duration: TIMING.cloudFadeDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.cloudFadeIn,
|
||||
);
|
||||
|
||||
// Avatars cascade in.
|
||||
tl.fromTo(
|
||||
".cloud-avatar",
|
||||
{ scale: 0, opacity: 0 },
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: TIMING.avatarEntryDur,
|
||||
ease: "back.out(1.7)",
|
||||
stagger: { each: TIMING.avatarStagger, from: "start" },
|
||||
},
|
||||
TIMING.avatarEntryStart,
|
||||
);
|
||||
|
||||
// Connection lines draw outward from center after all avatars settle.
|
||||
const lastAvatarEnd =
|
||||
TIMING.avatarEntryStart + TIMING.avatarStagger * (AVATAR_COUNT - 1) + TIMING.avatarEntryDur;
|
||||
const linesStart = lastAvatarEnd + TIMING.linesDelay;
|
||||
|
||||
document.querySelectorAll(".cloud-lines line").forEach((line, i) => {
|
||||
const { x, y } = avatarPositions[i];
|
||||
tl.to(
|
||||
line,
|
||||
{
|
||||
attr: { x2: x, y2: y },
|
||||
strokeOpacity: 0.6,
|
||||
duration: TIMING.linesDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
linesStart + i * TIMING.lineStagger,
|
||||
);
|
||||
});
|
||||
|
||||
const countEl = document.querySelector(".count");
|
||||
const suffixEl = document.querySelector(".count-suffix");
|
||||
|
||||
// Counter pulse — text stays deterministic under timeline seeking.
|
||||
tl.fromTo(
|
||||
countEl,
|
||||
{
|
||||
scale: 0.88,
|
||||
},
|
||||
{
|
||||
scale: 1,
|
||||
duration: TIMING.counterDuration,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.counterStart,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
suffixEl,
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.3,
|
||||
ease: "back.out(1.6)",
|
||||
},
|
||||
TIMING.counterStart + TIMING.counterDuration + TIMING.suffixDelay,
|
||||
);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PHASE 5: Brand strip — label + scrolling partner logos.
|
||||
// ----------------------------------------------------------------
|
||||
tl.fromTo(
|
||||
".brand-strip",
|
||||
{ opacity: 0, y: 40 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: TIMING.brandStripFadeDur,
|
||||
ease: "power2.out",
|
||||
},
|
||||
TIMING.brandStripStart,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
".brand-logo",
|
||||
{ scale: 0.5, opacity: 0 },
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 0.7,
|
||||
duration: 0.5,
|
||||
ease: "back.out(1.4)",
|
||||
stagger: { each: TIMING.brandLogoStagger, from: "start" },
|
||||
},
|
||||
TIMING.brandStripStart,
|
||||
);
|
||||
|
||||
// Finite horizontal scroll. Distance derived from remaining time × speed.
|
||||
// Speed in original: 0.8 px/frame at 30fps = 24 px/s. Over 1.4s ≈ 33.6 px.
|
||||
// For a more visible scroll, multiply by 4 → ~135 px.
|
||||
tl.to(
|
||||
".brand-strip-track",
|
||||
{
|
||||
x: -135,
|
||||
duration: TIMING.brandScrollDur,
|
||||
ease: "none",
|
||||
},
|
||||
TIMING.brandStripStart,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,347 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene 5 — HyperFrames Displace Reveal</title>
|
||||
|
||||
<!--
|
||||
HyperFrames composition.tsx.
|
||||
|
||||
Choreography (4 phases, 7.5 seconds total):
|
||||
0.00 – 0.67s Typewriter reveals "HyperFrames turns" (3 → 17 chars)
|
||||
1.67 – 2.22s Ticker scrolls: HTML → motion
|
||||
3.33 – 3.88s Ticker scrolls: motion → video
|
||||
4.60 – 5.45s Logo enters from offscreen-right with rotation + scale impact
|
||||
Text group pushed left + fades (40-50% of hero duration)
|
||||
6.60 – 7.50s Logo breathes (dual-frequency sine onUpdate, multiplicative on 1.3 scale)
|
||||
tuned to 40-50% of hero duration so the impact reads as causal
|
||||
- Breathing uses Form 2 (onUpdate) so it multiplies onto the 1.3 final scale,
|
||||
not overwrites it (Form 1 yoyo would undo the impact)
|
||||
- Two breathing periods (1.0s scale, 1.33s rotation) for organic feel
|
||||
- Logo uses a local static image asset referenced with a plain URL
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: linear-gradient(
|
||||
135deg,
|
||||
#3a3a3a 0%,
|
||||
#17211f 30%,
|
||||
#0b2328 48%,
|
||||
#1f3518 70%,
|
||||
#343434 100%
|
||||
);
|
||||
--text: #f8fafc;
|
||||
--accent: #18d9e8;
|
||||
--accent-2: #7bea5a;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Text group — typewriter + ticker side by side, displaced as a unit */
|
||||
.text-group {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.typewriter {
|
||||
height: 168px; /* = ITEM_HEIGHT (FONT_SIZE × 1.2) */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text);
|
||||
font-weight: 400;
|
||||
font-size: 140px;
|
||||
line-height: 1;
|
||||
white-space: pre;
|
||||
text-shadow: 0 0 36px rgba(24, 217, 232, 0.18);
|
||||
}
|
||||
|
||||
/* Vertical ticker */
|
||||
.ticker-window {
|
||||
height: 168px;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.ticker-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
will-change: transform;
|
||||
}
|
||||
.ticker-item {
|
||||
height: 168px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--accent);
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 700;
|
||||
font-size: 140px;
|
||||
padding-left: 20px;
|
||||
line-height: 1;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Hero logo — enters from offscreen right */
|
||||
.hero {
|
||||
position: absolute;
|
||||
width: 440px;
|
||||
height: 440px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20; /* above text during overlap */
|
||||
opacity: 0; /* fromTo will animate */
|
||||
}
|
||||
.hero .logo-mark {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 52px;
|
||||
box-shadow:
|
||||
0 0 70px rgba(24, 217, 232, 0.28),
|
||||
0 0 120px rgba(123, 234, 90, 0.18);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="7.5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
class="stage clip"
|
||||
data-start="0"
|
||||
data-duration="7.5"
|
||||
data-track-index="1"
|
||||
id="displace-stage"
|
||||
>
|
||||
<!-- Text group: typewriter + ticker -->
|
||||
<div class="text-group" id="text-group">
|
||||
<div class="typewriter">
|
||||
<span class="typewriter-text">Hyp</span>
|
||||
</div>
|
||||
<div class="ticker-window">
|
||||
<div class="ticker-stack">
|
||||
<div class="ticker-item">HTML</div>
|
||||
<div class="ticker-item">motion</div>
|
||||
<div class="ticker-item">video</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero logo asset -->
|
||||
<div class="hero" id="hero-logo">
|
||||
<!-- Inline-SVG placeholder mark — swap for your logo image -->
|
||||
<svg class="logo-mark" viewBox="0 0 100 100" role="img" aria-label="HyperFrames">
|
||||
<defs>
|
||||
<linearGradient id="hfMark" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#8b7bff" />
|
||||
<stop offset="1" stop-color="#3ddc97" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="92" height="92" rx="22" fill="url(#hfMark)" />
|
||||
<text
|
||||
x="50"
|
||||
y="63"
|
||||
text-anchor="middle"
|
||||
font-family="Inter, system-ui, sans-serif"
|
||||
font-size="40"
|
||||
font-weight="800"
|
||||
fill="#fff"
|
||||
>
|
||||
HF
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ================================================================
|
||||
CONSTANTS
|
||||
================================================================ */
|
||||
const TOTAL_DURATION = 7.5;
|
||||
const FONT_SIZE = 140;
|
||||
const ITEM_HEIGHT = FONT_SIZE * 1.2; // = 168 px
|
||||
|
||||
const TIMING = {
|
||||
// Phase 1: typewriter
|
||||
typeStart: 0.0,
|
||||
typeDur: 0.67,
|
||||
typeStartLen: 3, // pre-render the first 3 chars
|
||||
|
||||
// Phase 2: ticker steps
|
||||
ticker1At: 1.67, // HTML → motion
|
||||
ticker2At: 3.33, // motion → video
|
||||
stepDur: 0.55,
|
||||
|
||||
// Phase 3: reactive displacement
|
||||
displaceAt: 4.6,
|
||||
heroDur: 0.85, // matches spring(mass:1.5) settle
|
||||
offscreenX: 800, // hero starts here
|
||||
pushDist: -150, // text pushed THIS direction
|
||||
|
||||
// Phase 4: breathing
|
||||
idleStart: 6.6, // displaceAt + heroDur + ~1.15s buffer
|
||||
};
|
||||
|
||||
const FULL_TEXT = "HyperFrames turns";
|
||||
|
||||
/* ================================================================
|
||||
TIMELINE
|
||||
================================================================ */
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["main"] = tl;
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 1: Typewriter (smooth slice)
|
||||
---------------------------------------------------------------- */
|
||||
const typewriterEl = document.querySelector(".typewriter-text");
|
||||
const typeProxy = { progress: TIMING.typeStartLen };
|
||||
|
||||
tl.to(
|
||||
typeProxy,
|
||||
{
|
||||
progress: FULL_TEXT.length,
|
||||
duration: TIMING.typeDur,
|
||||
ease: "none",
|
||||
onUpdate: () => {
|
||||
const len = Math.floor(typeProxy.progress);
|
||||
const next = FULL_TEXT.slice(0, len);
|
||||
if (typewriterEl.textContent !== next) typewriterEl.textContent = next;
|
||||
},
|
||||
},
|
||||
TIMING.typeStart,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 2: Vertical ticker (2 steps: HTML → motion → video)
|
||||
---------------------------------------------------------------- */
|
||||
tl.to(
|
||||
".ticker-stack",
|
||||
{
|
||||
y: `-=${ITEM_HEIGHT}`,
|
||||
duration: TIMING.stepDur,
|
||||
ease: "back.out(1.4)", // spring(stiffness:120, damping:14)
|
||||
},
|
||||
TIMING.ticker1At,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
".ticker-stack",
|
||||
{
|
||||
y: `-=${ITEM_HEIGHT}`,
|
||||
duration: TIMING.stepDur,
|
||||
ease: "back.out(1.4)",
|
||||
},
|
||||
TIMING.ticker2At,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 3: Reactive displacement — three concurrent tweens.
|
||||
---------------------------------------------------------------- */
|
||||
// (1) Hero enters with rotation + scale impact, lands at scale 1.3.
|
||||
tl.fromTo(
|
||||
".hero",
|
||||
{ x: TIMING.offscreenX, scale: 0.5, rotation: -45, opacity: 0 },
|
||||
{
|
||||
x: 0,
|
||||
scale: 1.3,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
duration: TIMING.heroDur,
|
||||
ease: "power2.out", // spring(stiffness:100, damping:20, mass:1.5)
|
||||
},
|
||||
TIMING.displaceAt,
|
||||
);
|
||||
|
||||
// (2) Text pushed left. Completes at 50% of hero duration → immediate impact.
|
||||
tl.to(
|
||||
".text-group",
|
||||
{ x: TIMING.pushDist, duration: TIMING.heroDur * 0.5, ease: "power2.out" },
|
||||
TIMING.displaceAt,
|
||||
);
|
||||
|
||||
// (3) Text fades. Completes at 40% → fades slightly before push lands.
|
||||
tl.to(
|
||||
".text-group",
|
||||
{ opacity: 0, duration: TIMING.heroDur * 0.4, ease: "power2.out" },
|
||||
TIMING.displaceAt,
|
||||
);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
PHASE 4: Breathing — onUpdate so it MULTIPLIES onto the 1.3 scale.
|
||||
Dual frequencies (1.0s scale, 1.33s rotation) for organic motion.
|
||||
---------------------------------------------------------------- */
|
||||
const heroEl = document.querySelector(".hero");
|
||||
const HERO_FINAL_SCALE = 1.3;
|
||||
const HERO_FINAL_ROTATION = 0;
|
||||
const SCALE_PERIOD = 1.0; // seconds per scale cycle
|
||||
const ROTATE_PERIOD = 1.333; // seconds per rotation cycle — not a simple ratio of SCALE_PERIOD
|
||||
const SCALE_AMP = 0.05;
|
||||
const ROTATE_AMP = 3;
|
||||
|
||||
const breathDur = TOTAL_DURATION - TIMING.idleStart;
|
||||
|
||||
tl.to(
|
||||
{ tick: 0 },
|
||||
{
|
||||
tick: 1,
|
||||
duration: breathDur,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
const idleTime = Math.max(0, tl.time() - TIMING.idleStart);
|
||||
const omegaS = (idleTime / SCALE_PERIOD) * Math.PI * 2;
|
||||
const omegaR = (idleTime / ROTATE_PERIOD) * Math.PI * 2;
|
||||
gsap.set(heroEl, {
|
||||
scale: HERO_FINAL_SCALE * (1 + Math.sin(omegaS) * SCALE_AMP),
|
||||
rotation: HERO_FINAL_ROTATION + Math.sin(omegaR) * ROTATE_AMP,
|
||||
});
|
||||
},
|
||||
},
|
||||
TIMING.idleStart,
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,606 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Scene — HyperFrames Workflow Approve & Press</title>
|
||||
|
||||
<!--
|
||||
Blueprint: workflow-approve-press
|
||||
HyperFrames composition.tsx.
|
||||
Same four-phase choreography, GSAP-driven, seek-safe.
|
||||
|
||||
Audio (assumed accompanying narration):
|
||||
"This is AI that renders with you, not just for you.
|
||||
Preview the result, render to MP4, then confirm."
|
||||
|
||||
Sub-phases (local time, 5.5s total):
|
||||
0.17 – 0.72s Headline "AI edits WITH you" slides down
|
||||
0.50 – 1.10s Center editor mockup scales in
|
||||
0.67 – 2.17s Step indicators stagger-enter on the left (3D-tilted)
|
||||
2.00 – 3.33s Step state machine: 1→complete, 2→active … 2→complete, 3→active
|
||||
3.52 – 4.02s Confirm MP4 button (3D-tilted right) bouncy entry after render step is active
|
||||
4.22 – 4.72s Button depression (linear, no overshoot)
|
||||
4.72 – 5.22s Render confirmed: step 3 completes, color shifts to success green + checkmark pop
|
||||
|
||||
Asset placeholders:
|
||||
./assets/editor-demo.mp4 — center product demo video. Replace with a
|
||||
real export of the AI-editor flow. The
|
||||
composition falls back to a CSS-mockup of
|
||||
an editor timeline so it renders without
|
||||
network or media files.
|
||||
-->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
/* Palette — port of the the source COLORS / TYPOGRAPHY constants */
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #cbd5e1;
|
||||
--text-muted: #94a3b8;
|
||||
--brand-purple: #a78bfa;
|
||||
--brand-purple-glow: rgba(167, 139, 250, 0.6);
|
||||
--brand-green: #22c55e;
|
||||
--brand-green-glow: rgba(34, 197, 94, 0.6);
|
||||
--panel-stroke: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND — port of <FrostedGlassBackground />
|
||||
============================================================ */
|
||||
.bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 25% 30%, rgba(167, 139, 250, 0.2), transparent 55%),
|
||||
radial-gradient(ellipse at 80% 70%, rgba(34, 197, 94, 0.12), transparent 55%),
|
||||
linear-gradient(135deg, #1a1530 0%, #0a0815 70%);
|
||||
}
|
||||
|
||||
/* Ambient warm glow on the right (button side) */
|
||||
.ambient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at 70% 50%,
|
||||
var(--brand-purple-glow) 0%,
|
||||
transparent 40%
|
||||
);
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Vignette */
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.4) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASE 1 — HEADLINE
|
||||
============================================================ */
|
||||
.headline-wrap {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0; /* GSAP fades in */
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.headline {
|
||||
font-size: 96px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.headline .accent {
|
||||
color: var(--brand-purple);
|
||||
text-shadow: 0 0 30px var(--brand-purple-glow);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASE 2 — CENTER DEMO
|
||||
(Falls back to a CSS-mockup editor when ./assets/editor-demo.mp4
|
||||
is missing. The mockup is purely decorative — no animation
|
||||
inside it; the eye reads it as a paused product screenshot.)
|
||||
============================================================ */
|
||||
.demo-wrap {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 60%;
|
||||
opacity: 0;
|
||||
}
|
||||
.demo-frame {
|
||||
width: 1000px;
|
||||
height: 600px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--panel-stroke);
|
||||
box-shadow:
|
||||
0 0 40px var(--brand-purple-glow),
|
||||
0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
}
|
||||
.demo-frame video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* CSS-mockup fallback for the editor demo */
|
||||
.demo-mockup {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-rows: 56px 1fr 100px;
|
||||
background: linear-gradient(180deg, #1f1b3a 0%, #15112a 100%);
|
||||
}
|
||||
.mockup-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 20px;
|
||||
background: #15112a;
|
||||
border-bottom: 1px solid var(--panel-stroke);
|
||||
}
|
||||
.mockup-toolbar .dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.mockup-toolbar .dot.r {
|
||||
background: #ef4444;
|
||||
}
|
||||
.mockup-toolbar .dot.y {
|
||||
background: #eab308;
|
||||
}
|
||||
.mockup-toolbar .dot.g {
|
||||
background: #22c55e;
|
||||
}
|
||||
.mockup-toolbar .title {
|
||||
margin-left: 18px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.mockup-canvas {
|
||||
position: relative;
|
||||
padding: 30px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 40px),
|
||||
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 40px);
|
||||
}
|
||||
.mockup-canvas .preview-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, rgba(167, 139, 250, 0.35), rgba(34, 197, 94, 0.2));
|
||||
border: 1px solid var(--panel-stroke);
|
||||
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.mockup-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 20px;
|
||||
background: #15112a;
|
||||
border-top: 1px solid var(--panel-stroke);
|
||||
}
|
||||
.mockup-timeline .track {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.mockup-timeline .clip {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.mockup-timeline .t1 .a {
|
||||
flex: 3;
|
||||
background: var(--brand-purple);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.mockup-timeline .t1 .b {
|
||||
flex: 2;
|
||||
background: var(--brand-purple);
|
||||
opacity: 0.55;
|
||||
}
|
||||
.mockup-timeline .t1 .c {
|
||||
flex: 4;
|
||||
background: var(--brand-purple);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.mockup-timeline .t2 .a {
|
||||
flex: 5;
|
||||
background: var(--brand-green);
|
||||
opacity: 0.75;
|
||||
}
|
||||
.mockup-timeline .t2 .b {
|
||||
flex: 2;
|
||||
background: var(--brand-green);
|
||||
opacity: 0.45;
|
||||
}
|
||||
.mockup-timeline .t2 .c {
|
||||
flex: 2;
|
||||
background: var(--brand-green);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASE 3 — LEFT FLANK: STEP INDICATORS (3D-TILTED)
|
||||
============================================================ */
|
||||
.steps-flank {
|
||||
position: absolute;
|
||||
left: 100px;
|
||||
top: 55%;
|
||||
transform: translateY(-50%) perspective(800px) rotateY(15deg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
opacity: 0; /* GSAP fades in */
|
||||
}
|
||||
.step-circle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--text-muted);
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
transition: none; /* Steps SNAP between states, no CSS transition */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.step-num {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.step-check {
|
||||
display: none;
|
||||
}
|
||||
.step-label {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* State machine — toggled via tl.set({ attr: data-state }) */
|
||||
.step[data-state="active"] .step-circle {
|
||||
border-color: var(--brand-purple);
|
||||
box-shadow: 0 0 15px var(--brand-purple-glow);
|
||||
}
|
||||
.step[data-state="active"] .step-num {
|
||||
color: var(--brand-purple);
|
||||
}
|
||||
.step[data-state="active"] .step-label {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.step[data-state="complete"] .step-circle {
|
||||
border-color: var(--brand-green);
|
||||
background-color: var(--brand-green);
|
||||
}
|
||||
.step[data-state="complete"] .step-num {
|
||||
display: none;
|
||||
}
|
||||
.step[data-state="complete"] .step-check {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PHASE 4 — RIGHT FLANK: CONFIRM BUTTON (3D-TILTED)
|
||||
============================================================ */
|
||||
.button-flank {
|
||||
position: absolute;
|
||||
right: 70px;
|
||||
top: 55%;
|
||||
transform: translateY(-50%) perspective(800px) rotateY(-15deg);
|
||||
}
|
||||
.btn-press {
|
||||
opacity: 0;
|
||||
transform-origin: center center;
|
||||
}
|
||||
.btn {
|
||||
--btn-glow-blur: 20px;
|
||||
padding: 20px 50px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--brand-purple);
|
||||
box-shadow: 0 0 var(--btn-glow-blur) var(--brand-purple-glow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--text-primary);
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-check {
|
||||
display: inline-flex;
|
||||
transform-origin: center center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="interactive-workflow"
|
||||
data-start="0"
|
||||
data-duration="5.5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
style="position: relative; width: 1920px; height: 1080px; overflow: hidden"
|
||||
>
|
||||
<!-- Background layers -->
|
||||
<div class="bg"></div>
|
||||
|
||||
<!-- PHASE 1: Headline -->
|
||||
<div class="headline-wrap">
|
||||
<div class="headline">
|
||||
HyperFrames builds
|
||||
<span class="accent">WITH</span>
|
||||
you
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHASE 2: Center demo (video with CSS-mockup fallback) -->
|
||||
<div class="demo-wrap">
|
||||
<div class="demo-frame">
|
||||
<!--
|
||||
Real asset path (replace for production).
|
||||
If missing, the CSS mockup below shows through the transparent
|
||||
poster region. The video stays muted per HyperFrames rules.
|
||||
-->
|
||||
<!-- Decorative CSS-mockup of an editor — paused screenshot feel. -->
|
||||
<div class="demo-mockup">
|
||||
<div class="mockup-toolbar">
|
||||
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
||||
<span class="title">HyperFrames — Project: Launch Promo</span>
|
||||
</div>
|
||||
<div class="mockup-canvas">
|
||||
<div class="preview-box">Preview · 1920 × 1080</div>
|
||||
</div>
|
||||
<div class="mockup-timeline">
|
||||
<div class="track t1">
|
||||
<div class="clip a"></div>
|
||||
<div class="clip b"></div>
|
||||
<div class="clip c"></div>
|
||||
</div>
|
||||
<div class="track t2">
|
||||
<div class="clip a"></div>
|
||||
<div class="clip b"></div>
|
||||
<div class="clip c"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHASE 3: Left-flank step indicators (3D-tilted +15°) -->
|
||||
<div class="steps-flank">
|
||||
<div class="step step-1" data-step="1" data-state="pending">
|
||||
<div class="step-circle">
|
||||
<span class="step-num">1</span>
|
||||
<svg class="step-check" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 6L9 17L4 12" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-label">Compose HTML Scene</span>
|
||||
</div>
|
||||
|
||||
<div class="step step-2" data-step="2" data-state="pending">
|
||||
<div class="step-circle">
|
||||
<span class="step-num">2</span>
|
||||
<svg class="step-check" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 6L9 17L4 12" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-label">Seek & Preview</span>
|
||||
</div>
|
||||
|
||||
<div class="step step-3" data-step="3" data-state="pending">
|
||||
<div class="step-circle">
|
||||
<span class="step-num">3</span>
|
||||
<svg class="step-check" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 6L9 17L4 12" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="step-label">Render to MP4</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHASE 4: Right-flank confirm button (3D-tilted -15°) -->
|
||||
<div class="button-flank">
|
||||
<div class="btn-press">
|
||||
<div class="btn">
|
||||
<span class="btn-check">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M20 6L9 17L4 12"
|
||||
stroke="#fff"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="btn-label">Confirm MP4</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ambient + vignette overlays (pointer-events: none) -->
|
||||
<div class="ambient"></div>
|
||||
<div class="vignette"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// ── Phase boundaries (seconds) — match the blueprint ────────────
|
||||
const HEADLINE_START = 0.17;
|
||||
const HEADLINE_END = 0.72;
|
||||
const VIDEO_START = 0.5;
|
||||
const STEPS_START = 0.67;
|
||||
const STEP_STAGGER = 0.5;
|
||||
const STEP_ACTIVE_T2 = 2.0; // step 1 → complete, step 2 → active
|
||||
const STEP_ACTIVE_T3 = 3.33; // step 2 → complete, step 3 → active
|
||||
const BUTTON_ENTER = 3.52; // after "Render to MP4" becomes active
|
||||
const PRESS_FRAME = 4.22;
|
||||
const PRESS_DURATION = 0.5;
|
||||
const CHECK_POP = PRESS_FRAME + PRESS_DURATION;
|
||||
const SCENE_END = 5.5; // matches data-duration on the root
|
||||
|
||||
// ── Phase 1: Headline slides down from top ─────────────────────
|
||||
gsap.set(".demo-wrap", { xPercent: -50, yPercent: -50, scale: 0.8, opacity: 0 });
|
||||
gsap.set(".step", { x: -30, opacity: 0 });
|
||||
gsap.set(".btn-press", { scale: 0, opacity: 0 });
|
||||
gsap.set(".btn-check", { scale: 0 });
|
||||
|
||||
tl.fromTo(
|
||||
".headline-wrap",
|
||||
{ y: -40, opacity: 0 },
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: HEADLINE_END - HEADLINE_START,
|
||||
ease: "back.out(1.2)", // ≈ the source SPRING_CONFIGS.entrance
|
||||
},
|
||||
HEADLINE_START,
|
||||
);
|
||||
|
||||
// ── Phase 2: Center demo scales in ─────────────────────────────
|
||||
// HyperFrames syncs the <video> element's currentTime to the seek
|
||||
// position automatically — no .play() call needed (and one here
|
||||
// would break deterministic rendering).
|
||||
tl.to(".demo-wrap", { scale: 1, opacity: 1, duration: 0.6, ease: "power3.out" }, VIDEO_START);
|
||||
|
||||
// ── Phase 3a: Stagger entry of the step indicators ─────────────
|
||||
tl.to(
|
||||
".step",
|
||||
{
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
duration: 0.4,
|
||||
ease: "power3.out",
|
||||
stagger: { each: STEP_STAGGER, from: "start" },
|
||||
},
|
||||
STEPS_START,
|
||||
);
|
||||
|
||||
// ── Phase 3b: Step state machine (snap-toggled here) ──
|
||||
// Step 1 enters in the "active" state because narration starts on it.
|
||||
tl.set(".step-1", { attr: { "data-state": "active" } }, STEPS_START);
|
||||
|
||||
// First transition — step 1 finishes, step 2 takes over.
|
||||
tl.set(".step-1", { attr: { "data-state": "complete" } }, STEP_ACTIVE_T2);
|
||||
tl.set(".step-2", { attr: { "data-state": "active" } }, STEP_ACTIVE_T2);
|
||||
|
||||
// Second transition — step 2 finishes, step 3 takes over.
|
||||
tl.set(".step-2", { attr: { "data-state": "complete" } }, STEP_ACTIVE_T3);
|
||||
tl.set(".step-3", { attr: { "data-state": "active" } }, STEP_ACTIVE_T3);
|
||||
|
||||
// ── Phase 4a: Button bouncy entry ──────────────────────────────
|
||||
tl.to(
|
||||
".btn-press",
|
||||
{ scale: 1, opacity: 1, duration: 0.5, ease: "back.out(1.4)", overwrite: "auto" },
|
||||
BUTTON_ENTER,
|
||||
);
|
||||
|
||||
// ── Phase 4b: Press (linear depression → linear return) ────────
|
||||
// Two adjacent tweens on .btn-press — end value of (1) = start value of (2).
|
||||
tl.to(".btn-press", { scale: 0.95, duration: 0.1, ease: "power1.out" }, PRESS_FRAME);
|
||||
tl.to(
|
||||
".btn-press",
|
||||
{ scale: 1.0, duration: PRESS_DURATION - 0.1, ease: "power1.in" },
|
||||
PRESS_FRAME + 0.1,
|
||||
);
|
||||
|
||||
// ── Phase 4c: Color shift + label swap (at press end) ──────────
|
||||
tl.to(
|
||||
".btn",
|
||||
{
|
||||
backgroundColor: "#15803d",
|
||||
boxShadow: "0 0 25px rgba(21, 128, 61, 0.68)",
|
||||
duration: 0.3,
|
||||
ease: "power2.out",
|
||||
},
|
||||
CHECK_POP,
|
||||
);
|
||||
tl.set(".btn-label", { textContent: "Approved!" }, CHECK_POP);
|
||||
tl.set(".step-3", { attr: { "data-state": "complete" } }, CHECK_POP);
|
||||
|
||||
// ── Phase 4d: Checkmark pops into view ─────────────────────────
|
||||
// back.out(1.6) ≈ the source spring({ stiffness: 200, damping: 15 }).
|
||||
tl.to(".btn-check", { scale: 1, duration: 0.5, ease: "back.out(1.6)" }, CHECK_POP);
|
||||
|
||||
// ── Ambient pulsing glow on the button (Math.sin) ──
|
||||
// Source: `glowIntensity = 20 + Math.sin(frame * 0.1) * 10` — i.e. blur
|
||||
// oscillates 10 ↔ 30 around 20, with angular freq 0.1 rad/frame.
|
||||
// At 60fps that's 6 rad/s, so full period ≈ 2π / 6 ≈ 1.05s.
|
||||
// Finite yoyo replaces the continuous sine; `box-shadow` reads the
|
||||
// tweened CSS variable so the blur is actually animatable.
|
||||
const PULSE_PERIOD = 1.05;
|
||||
const PULSE_HALVES = Math.max(2, Math.floor((SCENE_END - BUTTON_ENTER) / (PULSE_PERIOD / 2)));
|
||||
tl.fromTo(
|
||||
".btn",
|
||||
{ "--btn-glow-blur": "10px" }, // source amplitude: 20 ± 10
|
||||
{
|
||||
"--btn-glow-blur": "30px",
|
||||
duration: PULSE_PERIOD / 2,
|
||||
ease: "sine.inOut",
|
||||
yoyo: true,
|
||||
repeat: PULSE_HALVES - 1,
|
||||
},
|
||||
BUTTON_ENTER,
|
||||
);
|
||||
|
||||
window.__timelines["interactive-workflow"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user