Install
That writes one file:compositions/beat-freeze-cut.html.
Add it to your video
It runs for 6 seconds at 1920×1080. Paste this into your composition:index.html
<div
data-composition-id="beat-freeze-cut"
data-composition-src="compositions/beat-freeze-cut.html"
data-start="0"
data-duration="6"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
data-start. Put it on a different timeline row with
data-track-index. See data attributes for the rest.
Change the colors
Set these CSS variables on the block:--bg-color— Background. Defaults to#0B0D10.--accent-color— Accent. Defaults to#00E5C7.
Source
beat-freeze-cut.html
beat-freeze-cut.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Beat Freeze Cut</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600;700;800;900&display=block"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 1920px;
height: 1080px;
margin: 0;
overflow: hidden;
background: #0b0d10;
}
/*
EDIT / STUDIO PARAMS
────────────────────
Studio color params (CSS custom properties on the root):
--bg-color default #0B0D10
--accent-color default #00E5C7
Numeric / string params (beat times, labels, intensity) are NOT in the
formal registry params schema today (only color keys are used by
existing blocks). Edit the CONFIG object in the script below instead.
*/
#bfc-root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
--bg-color: #0b0d10;
--accent-color: #00e5c7;
--bfc-ink: #f4f6f8;
--bfc-muted: #a0a8b0;
--bfc-panel: #14181f;
--bfc-panel-2: #1a2029;
--bfc-line: rgba(255, 255, 255, 0.08);
font-family:
"Inter",
system-ui,
-apple-system,
sans-serif;
color: var(--bfc-ink);
}
#bfc-bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 80% 60% at 50% 40%, rgba(0, 229, 199, 0.07) 0%, transparent 55%),
radial-gradient(
ellipse 50% 40% at 80% 80%,
rgba(255, 255, 255, 0.03) 0%,
transparent 50%
),
var(--bg-color);
}
#bfc-grid {
position: absolute;
inset: 0;
opacity: 0.22;
background-image:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 80px 80px;
mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000 20%, transparent 75%);
pointer-events: none;
}
#bfc-stage {
position: absolute;
inset: 0;
transform-origin: 50% 48%;
will-change: transform, filter;
}
#bfc-shot-a,
#bfc-shot-b {
position: absolute;
inset: 0;
overflow: hidden;
}
#bfc-shot-b {
visibility: hidden;
opacity: 0;
}
/* ── Shot A: single product card hero ── */
#bfc-a-crop {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transform-origin: 50% 50%;
}
#bfc-a-card {
position: relative;
width: 680px;
height: 720px;
border-radius: 36px;
background:
linear-gradient(160deg, rgba(255, 255, 255, 0.06) 0%, transparent 40%),
linear-gradient(180deg, var(--bfc-panel-2) 0%, var(--bfc-panel) 100%);
border: 1px solid var(--bfc-line);
box-shadow:
0 40px 120px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(0, 229, 199, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
overflow: hidden;
transform-origin: 50% 50%;
}
#bfc-a-card-glow {
position: absolute;
left: 50%;
top: 28%;
width: 420px;
height: 420px;
margin-left: -210px;
margin-top: -210px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0, 229, 199, 0.28) 0%, transparent 68%);
opacity: 0.85;
pointer-events: none;
}
#bfc-a-wave {
position: absolute;
left: 64px;
right: 64px;
top: 120px;
height: 220px;
}
#bfc-a-wave svg {
width: 100%;
height: 100%;
display: block;
}
#bfc-a-wave-path {
fill: none;
stroke: var(--accent-color);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.95;
}
#bfc-a-wave-fill {
fill: var(--accent-color);
opacity: 0.12;
}
#bfc-a-bars {
position: absolute;
left: 80px;
right: 80px;
bottom: 160px;
height: 96px;
display: flex;
align-items: flex-end;
gap: 10px;
}
.bfc-bar {
flex: 1;
border-radius: 6px 6px 2px 2px;
background: linear-gradient(180deg, var(--accent-color) 0%, rgba(0, 229, 199, 0.25) 100%);
transform-origin: 50% 100%;
height: 40%;
}
#bfc-a-meta {
position: absolute;
left: 64px;
right: 64px;
bottom: 56px;
display: flex;
align-items: center;
justify-content: space-between;
}
#bfc-a-kicker {
font-size: 18px;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--bfc-muted);
}
#bfc-a-pill {
font-size: 15px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #04120f;
background: var(--accent-color);
padding: 8px 16px;
border-radius: 999px;
}
/* ── Shot B: split layout, higher info density ── */
#bfc-b-layout {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 48px;
padding: 100px 110px;
align-items: center;
}
#bfc-b-left {
display: flex;
flex-direction: column;
gap: 28px;
transform-origin: 0% 50%;
}
#bfc-b-eyebrow {
font-size: 20px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent-color);
}
#bfc-b-title {
font-size: 128px;
font-weight: 900;
line-height: 0.92;
letter-spacing: -0.04em;
text-transform: uppercase;
color: var(--bfc-ink);
}
#bfc-b-sub {
font-size: 28px;
font-weight: 500;
color: var(--bfc-muted);
max-width: 520px;
line-height: 1.35;
}
#bfc-b-right {
display: flex;
flex-direction: column;
gap: 18px;
}
.bfc-b-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 28px 32px;
border-radius: 20px;
background: linear-gradient(135deg, var(--bfc-panel-2), var(--bfc-panel));
border: 1px solid var(--bfc-line);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35);
}
.bfc-b-card-label {
font-size: 18px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--bfc-muted);
}
.bfc-b-card-value {
font-size: 42px;
font-weight: 800;
letter-spacing: -0.03em;
color: var(--bfc-ink);
}
.bfc-b-card-value.accent {
color: var(--accent-color);
}
#bfc-b-accent-bar {
width: 120px;
height: 6px;
border-radius: 999px;
background: var(--accent-color);
opacity: 0.9;
}
/* ── Intro / transient typography ── */
#bfc-intro {
position: absolute;
left: 0;
right: 0;
top: 72px;
display: flex;
justify-content: center;
pointer-events: none;
z-index: 20;
opacity: 0;
visibility: hidden;
}
#bfc-intro-label {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--bfc-muted);
padding: 10px 18px;
border: 1px solid var(--bfc-line);
border-radius: 999px;
background: rgba(11, 13, 16, 0.55);
backdrop-filter: blur(8px);
}
#bfc-hit-type {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 30;
opacity: 0;
visibility: hidden;
}
#bfc-hit-type-text {
font-size: 160px;
font-weight: 900;
letter-spacing: -0.04em;
text-transform: uppercase;
color: var(--bfc-ink);
text-shadow:
0 0 40px rgba(0, 229, 199, 0.35),
0 8px 40px rgba(0, 0, 0, 0.55);
-webkit-text-stroke: 2px rgba(0, 0, 0, 0.35);
paint-order: stroke fill;
}
/* ── Freeze frame treatment ── */
#bfc-freeze {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 25;
opacity: 0;
visibility: hidden;
}
#bfc-freeze-outline {
position: absolute;
inset: 48px;
border: 3px solid var(--accent-color);
border-radius: 8px;
box-shadow:
0 0 0 1px rgba(0, 229, 199, 0.25),
inset 0 0 0 1px rgba(0, 229, 199, 0.15),
0 0 48px rgba(0, 229, 199, 0.2);
opacity: 0;
}
#bfc-freeze-flash {
position: absolute;
inset: 0;
background: #ffffff;
opacity: 0;
mix-blend-mode: screen;
}
#bfc-freeze-contour {
position: absolute;
inset: 0;
background:
linear-gradient(90deg, transparent 0%, rgba(0, 229, 199, 0.08) 48%, transparent 52%),
radial-gradient(ellipse 40% 55% at 50% 48%, transparent 40%, rgba(0, 229, 199, 0.18) 100%);
opacity: 0;
mix-blend-mode: screen;
}
#bfc-freeze-badge {
position: absolute;
top: 80px;
right: 96px;
font-size: 16px;
font-weight: 800;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #04120f;
background: var(--accent-color);
padding: 10px 18px;
border-radius: 6px;
opacity: 0;
}
/* ── Hard-cut connector (directional stretch / blur) ── */
#bfc-cut {
position: absolute;
inset: -8% -20%;
pointer-events: none;
z-index: 40;
opacity: 0;
visibility: hidden;
background: linear-gradient(
90deg,
transparent 0%,
rgba(0, 229, 199, 0.12) 35%,
rgba(255, 255, 255, 0.55) 50%,
rgba(0, 229, 199, 0.12) 65%,
transparent 100%
);
filter: blur(18px);
transform-origin: 50% 50%;
}
#bfc-vignette {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 15;
background: radial-gradient(
ellipse 75% 70% at 50% 50%,
transparent 45%,
rgba(0, 0, 0, 0.55) 100%
);
opacity: 0.85;
}
#bfc-driver {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
</style>
</head>
<body>
<div
id="bfc-root"
data-composition-id="beat-freeze-cut"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<div id="bfc-bg" aria-hidden="true"></div>
<div id="bfc-grid" aria-hidden="true"></div>
<div id="bfc-stage">
<!-- Shot A -->
<div id="bfc-shot-a">
<div id="bfc-a-crop" data-layout-allow-overflow>
<div id="bfc-a-card" data-layout-allow-overflow>
<div id="bfc-a-card-glow" aria-hidden="true"></div>
<div id="bfc-a-wave" aria-hidden="true">
<svg viewBox="0 0 592 220" preserveAspectRatio="none">
<path
id="bfc-a-wave-fill"
d="M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80 L592,220 L0,220 Z"
/>
<path
id="bfc-a-wave-path"
d="M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80"
/>
</svg>
</div>
<div id="bfc-a-bars" aria-hidden="true">
<div class="bfc-bar" id="bfc-bar-0" style="height: 34%"></div>
<div class="bfc-bar" id="bfc-bar-1" style="height: 58%"></div>
<div class="bfc-bar" id="bfc-bar-2" style="height: 42%"></div>
<div class="bfc-bar" id="bfc-bar-3" style="height: 78%"></div>
<div class="bfc-bar" id="bfc-bar-4" style="height: 52%"></div>
<div class="bfc-bar" id="bfc-bar-5" style="height: 88%"></div>
<div class="bfc-bar" id="bfc-bar-6" style="height: 46%"></div>
<div class="bfc-bar" id="bfc-bar-7" style="height: 64%"></div>
<div class="bfc-bar" id="bfc-bar-8" style="height: 36%"></div>
<div class="bfc-bar" id="bfc-bar-9" style="height: 72%"></div>
<div class="bfc-bar" id="bfc-bar-10" style="height: 48%"></div>
<div class="bfc-bar" id="bfc-bar-11" style="height: 92%"></div>
</div>
<div id="bfc-a-meta">
<span id="bfc-a-kicker">MUSIC PROMO</span>
<span id="bfc-a-pill">LIVE</span>
</div>
</div>
</div>
</div>
<!-- Shot B -->
<div id="bfc-shot-b">
<div id="bfc-b-layout">
<div id="bfc-b-left">
<div id="bfc-b-eyebrow">Next shot</div>
<div id="bfc-b-title">HARD<br />CUT</div>
<div id="bfc-b-accent-bar" aria-hidden="true"></div>
<div id="bfc-b-sub">
Beat-locked freeze, then cut. Built for music-led promos and montages.
</div>
</div>
<div id="bfc-b-right">
<div class="bfc-b-card" id="bfc-b-card-0">
<span class="bfc-b-card-label">Ramp</span>
<span class="bfc-b-card-value accent">1.35→2.2</span>
</div>
<div class="bfc-b-card" id="bfc-b-card-1">
<span class="bfc-b-card-label">Freeze</span>
<span class="bfc-b-card-value">0.8s</span>
</div>
<div class="bfc-b-card" id="bfc-b-card-2">
<span class="bfc-b-card-label">Cut</span>
<span class="bfc-b-card-value accent">3.00</span>
</div>
</div>
</div>
</div>
</div>
<div id="bfc-vignette" aria-hidden="true"></div>
<div id="bfc-intro" aria-hidden="true">
<span id="bfc-intro-label">ON THE BEAT</span>
</div>
<div id="bfc-hit-type" aria-hidden="true">
<span id="bfc-hit-type-text">DROP</span>
</div>
<div id="bfc-freeze" aria-hidden="true">
<div id="bfc-freeze-flash"></div>
<div id="bfc-freeze-contour"></div>
<div id="bfc-freeze-outline"></div>
<div id="bfc-freeze-badge">FREEZE</div>
</div>
<div id="bfc-cut" aria-hidden="true"></div>
<div id="bfc-driver" class="clip" data-start="0" data-duration="6" data-track-index="0"></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
// ── EDIT ME (not Studio params — see HTML comment) ──────────────
var CONFIG = {
primaryLabel: "DROP",
secondaryLabel: "ON THE BEAT",
beat1: 0.7,
beat2: 2.2,
freezeTime: 2.2,
intensity: 1.0,
};
// ────────────────────────────────────────────────────────────────
var DURATION = 6;
var intensity = Math.max(0.6, Math.min(1.4, Number(CONFIG.intensity) || 1));
var beat1 = Number(CONFIG.beat1) || 0.7;
var beat2 = Number(CONFIG.beat2) || 2.2;
var freezeAt = Number(CONFIG.freezeTime) || beat2;
var cutAt = 3.0;
var beat3 = 4.7;
var holdAt = 5.2;
var root = document.getElementById("bfc-root");
var stage = document.getElementById("bfc-stage");
var shotA = document.getElementById("bfc-shot-a");
var shotB = document.getElementById("bfc-shot-b");
var aCrop = document.getElementById("bfc-a-crop");
var aCard = document.getElementById("bfc-a-card");
var intro = document.getElementById("bfc-intro");
var introLabel = document.getElementById("bfc-intro-label");
var hitType = document.getElementById("bfc-hit-type");
var hitText = document.getElementById("bfc-hit-type-text");
var freeze = document.getElementById("bfc-freeze");
var freezeOutline = document.getElementById("bfc-freeze-outline");
var freezeFlash = document.getElementById("bfc-freeze-flash");
var freezeContour = document.getElementById("bfc-freeze-contour");
var freezeBadge = document.getElementById("bfc-freeze-badge");
var cutFx = document.getElementById("bfc-cut");
var bLeft = document.getElementById("bfc-b-left");
var bCards = [
document.getElementById("bfc-b-card-0"),
document.getElementById("bfc-b-card-1"),
document.getElementById("bfc-b-card-2"),
];
var bars = [];
for (var bi = 0; bi < 12; bi++) {
bars.push(document.getElementById("bfc-bar-" + bi));
}
// Apply editable labels
if (hitText) hitText.textContent = CONFIG.primaryLabel || "DROP";
if (introLabel) introLabel.textContent = CONFIG.secondaryLabel || "ON THE BEAT";
// Accent color from CSS var → freeze/glow that use hardcoded rgba fallbacks
// stay coherent because stroke/fill already use var(--accent-color).
var tl = gsap.timeline({ paused: true });
// Initial states (no CSS transform conflicts on animated props)
gsap.set(stage, { scale: 1, x: 0, y: 0, filter: "blur(0px) brightness(1)" });
gsap.set(aCrop, { scale: 1, x: 0, y: 0 });
// Card visible at t=0 so catalog snapshots are never a black frame
gsap.set(aCard, { scale: 0.9, y: 20, opacity: 1 });
gsap.set(shotA, { opacity: 1, visibility: "visible" });
gsap.set(shotB, { opacity: 0, visibility: "hidden" });
gsap.set(intro, { opacity: 0, visibility: "hidden", y: -12 });
gsap.set(hitType, { opacity: 0, visibility: "hidden", scale: 1.15 });
gsap.set(freeze, { opacity: 0, visibility: "hidden" });
gsap.set([freezeOutline, freezeFlash, freezeContour, freezeBadge], { opacity: 0 });
gsap.set(cutFx, { opacity: 0, visibility: "hidden", scaleX: 0.4, x: -200 });
gsap.set(bLeft, { opacity: 0, x: -48 });
gsap.set(bCards, { opacity: 0, x: 40 });
gsap.set(bars, { scaleY: 0.55 });
// Full duration anchor (seek-safe length)
tl.to({}, { duration: DURATION, ease: "none" }, 0);
// ── 0.00–0.70: intro push-in + secondary type ──
tl.to(aCard, { scale: 1, y: 0, duration: 0.55, ease: "power3.out" }, 0);
tl.to(stage, { scale: 1 + 0.04 * intensity, duration: 0.7, ease: "power1.out" }, 0);
tl.to(bars, { scaleY: 1, duration: 0.5, ease: "power2.out", stagger: 0.02 }, 0.12);
tl.set(intro, { visibility: "visible" }, 0.12);
tl.to(intro, { opacity: 1, y: 0, duration: 0.28, ease: "power2.out" }, 0.12);
tl.to(intro, { opacity: 0, y: -8, duration: 0.18, ease: "power2.in" }, 0.52);
tl.set(intro, { visibility: "hidden", opacity: 0 }, 0.7);
// ── 0.70–1.35: beat 1 — hard reframe ──
tl.to(
aCrop,
{
scale: 1 + 0.18 * intensity,
x: -80 * intensity,
y: 24 * intensity,
duration: 0.22,
ease: "power4.out",
},
beat1,
);
tl.to(
stage,
{
scale: 1.08 * intensity > 1.2 ? 1.2 : 1 + 0.08 * intensity,
duration: 0.28,
ease: "expo.out",
},
beat1,
);
tl.to(
bars,
{
scaleY: function (i) {
// Deterministic bar pattern (no Math.random)
var pattern = [0.55, 1.15, 0.7, 1.35, 0.9, 1.4, 0.65, 1.1, 0.5, 1.25, 0.8, 1.45];
return pattern[i % pattern.length];
},
duration: 0.18,
ease: "power3.out",
stagger: 0.01,
},
beat1,
);
// ── 1.35–2.20: speed ramp into freeze ──
var rampStart = 1.35;
var rampDur = Math.max(0.2, freezeAt - rampStart);
tl.to(
aCrop,
{
scale: 1.28 * (0.9 + 0.1 * intensity),
x: 28 * intensity,
y: -12 * intensity,
duration: rampDur,
ease: "power3.in",
},
rampStart,
);
tl.to(
stage,
{
scale: 1.12,
filter: "blur(0px) brightness(1.08)",
duration: rampDur,
ease: "power2.in",
},
rampStart,
);
tl.to(
bars,
{
scaleY: 1.55,
duration: rampDur * 0.85,
ease: "power2.in",
stagger: { each: 0.012, from: "center" },
},
rampStart,
);
// ── 2.20–3.00: freeze hit (snap-on for seek/snapshot stability) ──
tl.set(freeze, { visibility: "visible", opacity: 1 }, freezeAt);
tl.set(freezeOutline, { opacity: 1 }, freezeAt);
tl.set(freezeContour, { opacity: 0.85 }, freezeAt);
tl.set(freezeBadge, { opacity: 1 }, freezeAt);
tl.set(hitType, { visibility: "visible", opacity: 1, scale: 1 }, freezeAt);
// Hold crop/stage where ramp ended
tl.to(aCrop, { duration: 0.01, ease: "none" }, freezeAt);
tl.to(
stage,
{
scale: 1.12,
filter: "blur(0px) brightness(1.15)",
duration: 0.05,
ease: "power4.out",
},
freezeAt,
);
// Single exposure flash (not a strobe sequence)
tl.fromTo(
freezeFlash,
{ opacity: 0.55 * intensity },
{ opacity: 0, duration: 0.28, ease: "power2.out" },
freezeAt,
);
tl.to(
hitType,
{ opacity: 0, scale: 0.96, duration: 0.2, ease: "power2.in" },
freezeAt + 0.55,
);
tl.set(hitType, { visibility: "hidden", opacity: 0 }, freezeAt + 0.78);
// Soft settle of freeze dressing before cut
tl.to(
[freezeOutline, freezeContour, freezeBadge],
{ opacity: 0.75, duration: 0.35, ease: "sine.out" },
freezeAt + 0.35,
);
// ── 3.00–3.35: hard cut connector ──
tl.set(cutFx, { visibility: "visible" }, cutAt);
tl.fromTo(
cutFx,
{ opacity: 0, scaleX: 0.35, x: -280 },
{
opacity: 0.95,
scaleX: 1.6,
x: 120,
duration: 0.12,
ease: "power4.in",
},
cutAt,
);
tl.to(
stage,
{
filter: "blur(14px) brightness(1.35)",
scaleX: 1.35,
scaleY: 0.92,
duration: 0.12,
ease: "power3.in",
},
cutAt,
);
// Switch shots mid-cut — shot B content lands immediately (no black gap)
var switchAt = cutAt + 0.14;
tl.set(shotA, { opacity: 0, visibility: "hidden" }, switchAt);
tl.set(shotB, { opacity: 1, visibility: "visible" }, switchAt);
tl.set(hitType, { opacity: 0, visibility: "hidden" }, switchAt);
tl.set(
[freeze, freezeOutline, freezeFlash, freezeContour, freezeBadge],
{ opacity: 0, visibility: "hidden" },
switchAt,
);
tl.set(freeze, { visibility: "hidden" }, switchAt);
// Shot B content visible on switch (offset settles after)
tl.set(bLeft, { opacity: 1, x: -24 }, switchAt);
tl.set(bCards, { opacity: 1, x: 24 }, switchAt);
tl.to(
stage,
{
filter: "blur(0px) brightness(1)",
scale: 1,
scaleX: 1,
scaleY: 1,
x: 0,
y: 0,
duration: 0.18,
ease: "power3.out",
},
switchAt,
);
tl.to(cutFx, { opacity: 0, x: 360, duration: 0.16, ease: "power2.out" }, cutAt + 0.16);
tl.set(cutFx, { visibility: "hidden", opacity: 0 }, cutAt + 0.34);
// ── after switch: shot B settle (no scale overlap with recover) ──
tl.to(bLeft, { x: 0, duration: 0.4, ease: "power3.out" }, switchAt);
tl.to(
bCards,
{
x: 0,
duration: 0.38,
ease: "power3.out",
stagger: 0.06,
},
switchAt + 0.04,
);
tl.to(stage, { scale: 1.02, duration: 1.17, ease: "sine.out" }, switchAt + 0.18);
// ── 4.70–5.20: final beat punch-in ──
tl.to(
stage,
{
scale: 1.08 * (0.9 + 0.1 * intensity),
duration: 0.14,
ease: "power4.out",
},
beat3,
);
tl.to(
bCards[1],
{
scale: 1.04,
duration: 0.12,
ease: "power3.out",
},
beat3,
);
tl.to(
bCards[1],
{
scale: 1,
duration: 0.22,
ease: "power2.out",
},
beat3 + 0.14,
);
// Micro freeze feel: brief hold of scale
tl.to(
stage,
{ scale: 1.08 * (0.9 + 0.1 * intensity), duration: 0.2, ease: "none" },
beat3 + 0.14,
);
// ── 5.20–6.00: hold readable end card (≥0.8s) ──
tl.to(stage, { scale: 1.03, duration: 0.35, ease: "power2.out" }, holdAt);
// Ensure transient layers stay dead through end
tl.set(intro, { opacity: 0, visibility: "hidden" }, holdAt);
tl.set(hitType, { opacity: 0, visibility: "hidden" }, holdAt);
tl.set(freeze, { opacity: 0, visibility: "hidden" }, holdAt);
tl.set(cutFx, { opacity: 0, visibility: "hidden" }, holdAt);
window.__timelines["beat-freeze-cut"] = tl;
})();
</script>
</body>
</html>
transition music beat-driven freeze-frame.