mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-13 15:49:53 +00:00
595 lines
24 KiB
HTML
595 lines
24 KiB
HTML
<!doctype html>
|
||
<html
|
||
lang="en"
|
||
data-composition-variables='[
|
||
{"id":"bgColor", "type":"color", "label":"Stage background (theme 0)", "default":"#0a84ff"},
|
||
{"id":"anchors", "type":"string", "label":"Left anchor column (comma list = rows)","default":"studio,contre,courant"},
|
||
{"id":"theme", "type":"number", "label":"Base theme index", "default":0},
|
||
{"id":"showText","type":"boolean", "label":"Show text (false = blocks only)", "default":true},
|
||
{"id":"program", "type":"string", "label":"Program JSON (empty = built-in group_6 score)", "default":""}
|
||
]'
|
||
>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=1920, height=1080" />
|
||
<title>Split-Anchor Word-Slot · beat-synced kinetic typography — group_6 reference impl</title>
|
||
<script src="../../motion-primitives/assets/gsap.min.js"></script>
|
||
<style>
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
html,
|
||
body {
|
||
background: #0a84ff;
|
||
overflow: hidden;
|
||
font-family: "Helvetica Neue", "Arial Black", Arial, sans-serif;
|
||
}
|
||
#root {
|
||
position: relative;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
background: #0a84ff;
|
||
}
|
||
/* #frame = the CENTERING wrapper. CSS positions it slightly ABOVE center (the source
|
||
sits high-center); GSAP never touches #frame, so the CSS transform here is safe. */
|
||
#frame {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 43%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
/* #lockup = the thing GSAP jitters/tilts (shake) — NO CSS transform, so GSAP owns
|
||
x/y/rotation cleanly. Holds the base tilt the whole brutalist lockup leans at. */
|
||
#lockup {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
will-change: transform;
|
||
}
|
||
/* anchors LEFT-aligned (ragged right) so the slot box clears the shorter words and
|
||
never hides the held text. */
|
||
#anchors {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
.anchor {
|
||
font-weight: 900;
|
||
line-height: 0.96;
|
||
letter-spacing: -0.045em;
|
||
text-transform: lowercase;
|
||
white-space: nowrap;
|
||
will-change: transform, opacity;
|
||
}
|
||
/* the inner .t span carries the faux-EXTENDED width (scaleX) for the ANCHORS — they
|
||
have no background, so nothing overflows. GSAP animates the outer element; the
|
||
span's scaleX is never touched by GSAP, so the two never fight (asset-free
|
||
Druk-Wide / Extended-Black look from a system font). */
|
||
.t {
|
||
display: inline-block;
|
||
transform: scaleX(1.12);
|
||
transform-origin: left center;
|
||
}
|
||
/* slot TAPES carry their extend via the LINE's GSAP scaleX instead, so the tape
|
||
BACKGROUND stretches WITH the text and the word never overflows the strip. */
|
||
.sline .t {
|
||
transform: scaleX(1);
|
||
}
|
||
/* #slotbox = a TRANSPARENT container sitting just RIGHT of the anchors (small positive
|
||
margin → it never covers the held words). Holds the 3 stacked cards + the word tapes. */
|
||
/* small NEGATIVE margin = the source's tight collage TUCK: the tape kisses / lightly
|
||
presses over the tail of the longest anchor without hiding the glyph (the tape's own
|
||
text starts after its left padding, so words never collide). Tune this for press amount. */
|
||
#slotbox {
|
||
position: relative;
|
||
margin-left: -0.12em;
|
||
will-change: transform, opacity;
|
||
backface-visibility: hidden;
|
||
transform-origin: 50% 42%;
|
||
}
|
||
/* THREE stacked, offset, torn CARDS = the brutalist collage. Transparent during word
|
||
phases; `bg` turns them black for the empty box + the expand zoom (they scale as one
|
||
with #slotbox, staying staggered). Siblings — NOT parent-clipped — so the offsets show. */
|
||
.card {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: transparent;
|
||
will-change: transform;
|
||
}
|
||
#card1 {
|
||
z-index: -1;
|
||
clip-path: polygon(0% 4%, 97% 0%, 100% 92%, 3% 100%);
|
||
}
|
||
#card2 {
|
||
z-index: -2;
|
||
transform: translate(22px, 17px) rotate(-2.8deg);
|
||
clip-path: polygon(0% 2%, 100% 0%, 98% 100%, 2% 96%);
|
||
}
|
||
#card3 {
|
||
z-index: -3;
|
||
transform: translate(-15px, 10px) rotate(2.1deg);
|
||
clip-path: polygon(0% 6%, 96% 3%, 100% 95%, 4% 100%);
|
||
}
|
||
.lines {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: grid;
|
||
}
|
||
.sgroup {
|
||
grid-area: 1 / 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 3px;
|
||
}
|
||
/* each .sline is its OWN torn TAPE — sized to its word, its GSAP scaleX stretches
|
||
bg+text TOGETHER (no overflow), its own micro-rotation + stair offset + shadow. */
|
||
.sline {
|
||
font-weight: 900;
|
||
line-height: 0.96;
|
||
letter-spacing: -0.045em;
|
||
white-space: nowrap;
|
||
text-align: left;
|
||
background: #f4f4f4;
|
||
padding: 0.03em 0.22em 0.08em 0.16em;
|
||
clip-path: polygon(0% 6%, 98% 0%, 100% 90%, 3% 100%);
|
||
box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.16);
|
||
transform-origin: 0% 50%; /* GSAP scaleX stretches RIGHTward only — never back over the anchors */
|
||
will-change: transform, opacity;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- duration 5.41s = the full group_6 span (lockup → 2 slot cycles → bg flip + shake → expand wipe) -->
|
||
<div
|
||
data-hf-id="hf-g6rt"
|
||
id="root"
|
||
data-composition-id="group-six-recreate"
|
||
data-width="1920"
|
||
data-height="1080"
|
||
data-start="0"
|
||
data-duration="5.41"
|
||
data-root="true"
|
||
>
|
||
<div data-hf-id="hf-g6frame" id="frame">
|
||
<div data-hf-id="hf-g6lock" id="lockup">
|
||
<div data-hf-id="hf-g6anch" id="anchors"></div>
|
||
<div data-hf-id="hf-g6box" id="slotbox">
|
||
<div data-hf-id="hf-g6c1" id="card1" class="card"></div>
|
||
<div data-hf-id="hf-g6c2" id="card2" class="card"></div>
|
||
<div data-hf-id="hf-g6c3" id="card3" class="card"></div>
|
||
<div class="lines" id="lines"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// SPLIT-ANCHOR WORD-SLOT · ONE base lockup, FIVE beat-synced operators
|
||
// ────────────────────────────────────────────────────────────────────
|
||
// BASE : a kinetic-typography LOCKUP — a HELD left "anchor" column of R rows
|
||
// (fixed words) beside a torn-paper word-SLOT box on the right. The
|
||
// whole lockup LEANS at a base tilt (brutalist/acid attitude). R is
|
||
// data (the `anchors` comma-list). Asset-free: text + colored boxes.
|
||
// Reversed from group_6 + tuned against a Gemini side-by-side pass.
|
||
//
|
||
// OP lockup : pop the R anchor words in (punchy scale, faux motion-blur squash)
|
||
// + reveal the empty slot box. The held stage. (source f3–f12)
|
||
// OP slot : one word-GROUP cycle — lines WHIP in from the right (expo.out +
|
||
// scaleX stretch = fake motion blur), hold, then arc out down-left.
|
||
// own color per line. repeatable. (source f24–f44, f49–f66)
|
||
// OP bg : HARD-CUT flip the whole scene to another theme on a beat (bg +
|
||
// anchor ink + box fill + slot ink invert, 0ms). 0/1/2× = changes
|
||
// none / once / twice. (source f83 blue→green)
|
||
// OP shake : global per-beat JOLT of the lockup (x/y + rotation around the base
|
||
// tilt) with a back-ease recoil — one punch per beat. (source f52–f100)
|
||
// OP expand : EXIT wipe — fade text, then the slot box ZOOMS (expo.in, blur) from
|
||
// its own centre until it swallows the frame. (source f106–f117)
|
||
//
|
||
// ARCHITECTURE: every operator writes GSAP tweens at ABSOLUTE audio seconds onto
|
||
// ONE paused timeline → order-independent, may overlap (shake runs under slot/bg).
|
||
// A data PROGRAM lists which ops run on which beats; §7 dispatches it. Compose by
|
||
// editing DATA (the PROGRAM / `program` var / program.json), never operator code.
|
||
// ════════════════════════════════════════════════════════════════════
|
||
|
||
// ── 1. Content variables ──
|
||
var defaults = {
|
||
bgColor: "#0a84ff",
|
||
anchors: "studio,contre,courant",
|
||
theme: 0,
|
||
showText: true,
|
||
program: "",
|
||
};
|
||
var vars = Object.assign(
|
||
{},
|
||
defaults,
|
||
window.__hyperframes && window.__hyperframes.getVariables
|
||
? window.__hyperframes.getVariables()
|
||
: {},
|
||
);
|
||
var ANCHORS = String(vars.anchors)
|
||
.split(",")
|
||
.map(function (s) {
|
||
return s.trim();
|
||
})
|
||
.filter(function (s) {
|
||
return s.length;
|
||
});
|
||
if (!ANCHORS.length) ANCHORS = ["studio", "contre", "courant"];
|
||
var BASE_THEME = Math.max(0, parseInt(vars.theme, 10) || 0);
|
||
var SHOW_TEXT = vars.showText !== false && vars.showText !== "false";
|
||
|
||
// base attitude: the whole lockup leans; the box leans extra the other way.
|
||
var BASE_TILT = -3.5; // deg, the lockup's resting lean
|
||
var BOX_TILT = 2.6; // deg, the slot box's extra lean vs the anchors
|
||
|
||
// ── 2. THEME LIBRARY — brighter electric/fluoro to match the source ──
|
||
var THEMES = [
|
||
{ bg: "#0a84ff", ink: "#ffffff", box: "#f4f4f4", boxInk: "#0a84ff" }, // 0 electric blue
|
||
{ bg: "#5cf03a", ink: "#0a0a0a", box: "#0a0a0a", boxInk: "#5cf03a" }, // 1 lime/acid green (invert)
|
||
{ bg: "#0a0a0a", ink: "#ffffff", box: "#ffffff", boxInk: "#0a0a0a" }, // 2 ink
|
||
{ bg: "#ffe400", ink: "#0a0a0a", box: "#0a0a0a", boxInk: "#ffe400" }, // 3 amber
|
||
];
|
||
var ACCENTS = ["#0a84ff", "#39e024", "#ff3bd0", "#ffe400", "#ffffff", "#0a0a0a"];
|
||
function theme(i) {
|
||
return THEMES[((i % THEMES.length) + THEMES.length) % THEMES.length];
|
||
}
|
||
function accent(i) {
|
||
return ACCENTS[((i % ACCENTS.length) + ACCENTS.length) % ACCENTS.length];
|
||
}
|
||
|
||
// ── 3. THE PROGRAM (data) — see program.json ──
|
||
// group_6: ~92 BPM · downbeats 2.322(f56)/4.923(f118) · hihat fill 2.16–4.18s.
|
||
var DEFAULT_PROGRAM = [
|
||
{ op: "lockup", enter: [0.125, 0.333, 0.5], box: 0.292, theme: 0 },
|
||
{
|
||
op: "slot",
|
||
words: ["I bring", "ideas", "to life"],
|
||
enter: [1.0, 1.167, 1.292],
|
||
exit: [1.667, 1.75, 1.833],
|
||
},
|
||
{
|
||
op: "slot",
|
||
words: ["you", "wanna", "see?"],
|
||
enter: [2.042, 2.208, 2.375],
|
||
exit: [2.75, 2.75, 2.75],
|
||
colors: [0, 1, 1],
|
||
},
|
||
{
|
||
op: "shake",
|
||
amp: 26,
|
||
rot: 2.4,
|
||
settle: 0.16,
|
||
beats: [
|
||
2.16, 2.3, 2.44, 2.62, 2.76, 2.88, 3.06, 3.27, 3.41, 3.53, 3.71, 3.85, 4.09, 4.18,
|
||
],
|
||
},
|
||
{ op: "bg", at: 3.458, theme: 1 },
|
||
{ op: "expand", fade: 4.18, at: 4.4, to: 4.82 },
|
||
];
|
||
var DUR = 5.41;
|
||
|
||
var PROGRAM = DEFAULT_PROGRAM;
|
||
if (vars.program && String(vars.program).trim()) {
|
||
try {
|
||
var parsed = typeof vars.program === "string" ? JSON.parse(vars.program) : vars.program;
|
||
if (parsed && parsed.length) PROGRAM = parsed;
|
||
} catch (e) {
|
||
PROGRAM = DEFAULT_PROGRAM;
|
||
}
|
||
}
|
||
|
||
// ── 4. Build the LOCKUP ──────────────────────────────────────────────────
|
||
var R = ANCHORS.length;
|
||
var slotSteps = PROGRAM.filter(function (s) {
|
||
return s.op === "slot";
|
||
});
|
||
var maxSlot = slotSteps.reduce(function (m, s) {
|
||
return Math.max(m, (s.words || []).length);
|
||
}, 0);
|
||
var maxRows = Math.max(R, maxSlot, 1);
|
||
var fontPx = maxRows <= 3 ? 116 : maxRows === 4 ? 92 : 74;
|
||
var rowH = Math.round(fontPx * 0.99);
|
||
|
||
var root = document.getElementById("root");
|
||
var lockupEl = document.getElementById("lockup");
|
||
var anchorsBox = document.getElementById("anchors");
|
||
var slotbox = document.getElementById("slotbox");
|
||
var card1 = document.getElementById("card1");
|
||
var card2 = document.getElementById("card2");
|
||
var card3 = document.getElementById("card3");
|
||
var cards = [card1, card2, card3];
|
||
var linesBox = document.getElementById("lines");
|
||
|
||
var TH0 = theme(BASE_THEME);
|
||
document.documentElement.style.background = vars.bgColor || TH0.bg;
|
||
document.body.style.background = vars.bgColor || TH0.bg;
|
||
root.style.background = vars.bgColor || TH0.bg;
|
||
|
||
function mkWord(text, px, h) {
|
||
// word node with an inner faux-extended span
|
||
var d = document.createElement("div");
|
||
d.style.fontSize = px + "px";
|
||
d.style.height = h + "px";
|
||
var sp = document.createElement("span");
|
||
sp.className = "t";
|
||
sp.textContent = SHOW_TEXT ? text : "";
|
||
d.appendChild(sp);
|
||
return { row: d, span: sp };
|
||
}
|
||
|
||
// left anchor column (held words). The faux-extend scaleX renders the glyphs WIDER
|
||
// than their layout box (origin-left → overflow to the right); pin each row's width
|
||
// to the SCALED width so the column — and the slot box to its right — accounts for
|
||
// it and never overlaps the held words.
|
||
var anchorEls = [];
|
||
for (var a = 0; a < R; a++) {
|
||
var w = mkWord(ANCHORS[a], fontPx, rowH);
|
||
w.row.className = "anchor";
|
||
w.span.style.color = TH0.ink;
|
||
anchorsBox.appendChild(w.row);
|
||
w.row.style.width = Math.ceil(w.span.getBoundingClientRect().width + 2) + "px";
|
||
anchorEls.push(w.row);
|
||
}
|
||
|
||
// the slot box is a TRANSPARENT backing during word phases (so only the per-line
|
||
// tapes show on the bg — no dead space). `bg` turns it into a solid torn card for
|
||
// the empty state + the expand zoom. ONE .sgroup per slot step, stacked in one grid
|
||
// cell. Each .sline is its own TAPE (own width/rotation/offset). No runtime text edit.
|
||
slotbox.style.background = "transparent";
|
||
cards.forEach(function (c) {
|
||
c.style.background = "transparent";
|
||
});
|
||
var groups = []; // [{ lines:[rowEls], spans:[spanEls] }]
|
||
slotSteps.forEach(function (step) {
|
||
var g = document.createElement("div");
|
||
g.className = "sgroup";
|
||
var lines = [],
|
||
spans = [];
|
||
(step.words || []).forEach(function (word, li) {
|
||
var lw = mkWord(word, fontPx, rowH);
|
||
lw.row.className = "sline";
|
||
lw.row.style.background = TH0.box; // each line = its own tape
|
||
lw.row.style.marginLeft = [0, 16, 8, 24][li % 4] + "px"; // rightward stair offset (collage)
|
||
lw.span.style.color = TH0.boxInk;
|
||
g.appendChild(lw.row);
|
||
lines.push(lw.row);
|
||
spans.push(lw.span);
|
||
});
|
||
linesBox.appendChild(g);
|
||
groups.push({ lines: lines, spans: spans });
|
||
step.__g = groups.length - 1; // remember which group this step drives
|
||
});
|
||
var allSlotLines = groups.reduce(function (acc, g) {
|
||
return acc.concat(g.lines);
|
||
}, []);
|
||
var allSlotSpans = groups.reduce(function (acc, g) {
|
||
return acc.concat(g.spans);
|
||
}, []);
|
||
|
||
// ── 5. Timeline + helpers ────────────────────────────────────────────────
|
||
window.__timelines = window.__timelines || {};
|
||
var tl = gsap.timeline({ paused: true });
|
||
|
||
function beatAt(i, n, beats) {
|
||
if (!beats || !beats.length) return 0;
|
||
return beats[Math.min(beats.length - 1, Math.floor((i * beats.length) / n))];
|
||
}
|
||
function jolt(k) {
|
||
// deterministic per-beat shake offset (no Math.random)
|
||
return { x: ((k * 37) % 11) - 5, y: ((k * 53) % 9) - 4, r: ((k * 29) % 7) - 3 };
|
||
}
|
||
function lineRot(i) {
|
||
return [-2.0, 1.6, -1.0, 2.2][i % 4];
|
||
} // per-line lean
|
||
|
||
// rest pose: lockup leans at BASE_TILT, box leans extra, all content hidden.
|
||
gsap.set(lockupEl, { rotation: BASE_TILT });
|
||
gsap.set(slotbox, { rotation: BOX_TILT });
|
||
gsap.set(anchorEls, { autoAlpha: 0, y: 18, scale: 0.9, scaleX: 0.9 });
|
||
gsap.set(slotbox, { autoAlpha: 0, scale: 0.86 });
|
||
allSlotLines.forEach(function (el, i) {
|
||
gsap.set(el, { autoAlpha: 0, rotation: lineRot(i) });
|
||
});
|
||
|
||
// ── 6. THE FIVE OPERATORS ────────────────────────────────────────────────
|
||
|
||
// lockup — punchy anchor pop-in (scale + faux-blur squash) + box reveal.
|
||
function lockup(P) {
|
||
var th = theme(P.theme != null ? P.theme : BASE_THEME);
|
||
tl.set(lockupEl, { rotation: BASE_TILT }, 0);
|
||
anchorEls.forEach(function (el, i) {
|
||
tl.set(el.firstChild, { color: th.ink }, 0);
|
||
tl.fromTo(
|
||
el,
|
||
{ autoAlpha: 0, y: 26, scale: 0.84, scaleX: 0.7 },
|
||
{
|
||
autoAlpha: 1,
|
||
y: 0,
|
||
scale: 1,
|
||
scaleX: 1,
|
||
duration: 0.2,
|
||
ease: "expo.out",
|
||
overwrite: "auto",
|
||
immediateRender: false,
|
||
},
|
||
beatAt(i, R, P.enter),
|
||
);
|
||
});
|
||
var boxAt = P.box != null ? P.box : beatAt(R - 1, R, P.enter);
|
||
tl.fromTo(
|
||
slotbox,
|
||
{ autoAlpha: 0, scale: 0.6, rotation: BOX_TILT - 6 },
|
||
{
|
||
autoAlpha: 1,
|
||
scale: 1,
|
||
rotation: BOX_TILT,
|
||
duration: 0.22,
|
||
ease: "back.out(2.2)",
|
||
overwrite: "auto",
|
||
immediateRender: false,
|
||
},
|
||
boxAt,
|
||
);
|
||
}
|
||
|
||
// slot — word-GROUP cycle: WHIP in from the right (expo.out + scaleX stretch),
|
||
// hold, arc out down-left. each line its own colour + lean.
|
||
function slot(P) {
|
||
var g = groups[P.__g != null ? P.__g : 0];
|
||
if (!g) return;
|
||
var th = theme(P.theme != null ? P.theme : BASE_THEME);
|
||
var n = g.lines.length;
|
||
g.lines.forEach(function (el, i) {
|
||
var col = P.colors && P.colors[i] != null ? accent(P.colors[i]) : th.boxInk;
|
||
var tIn = beatAt(i, n, P.enter);
|
||
tl.set(g.spans[i], { color: col }, tIn);
|
||
tl.fromTo(
|
||
el,
|
||
{ autoAlpha: 0, x: 150, scaleX: 1.7, rotation: lineRot(i) },
|
||
{
|
||
autoAlpha: 1,
|
||
x: 0,
|
||
scaleX: 1.12,
|
||
rotation: lineRot(i),
|
||
duration: 0.24,
|
||
ease: "expo.out",
|
||
overwrite: "auto",
|
||
immediateRender: false,
|
||
}, // rest scaleX 1.12 = faux-extend (bg stretches with text)
|
||
tIn,
|
||
);
|
||
});
|
||
if (!P.exit || !P.exit.length) return;
|
||
g.lines.forEach(function (el, i) {
|
||
tl.to(
|
||
el,
|
||
{
|
||
autoAlpha: 0,
|
||
x: -120,
|
||
y: 46,
|
||
scaleX: 1.4,
|
||
duration: 0.16,
|
||
ease: "power3.in",
|
||
overwrite: "auto",
|
||
},
|
||
beatAt(i, n, P.exit),
|
||
);
|
||
});
|
||
}
|
||
|
||
// bg — hard-cut scene flip (0ms invert). 0/1/2 of these = bg changes 0/1/2×.
|
||
function bg(P) {
|
||
var th = theme(P.theme != null ? P.theme : 0);
|
||
tl.set(root, { backgroundColor: th.bg }, P.at);
|
||
tl.set(
|
||
anchorEls.map(function (e) {
|
||
return e.firstChild;
|
||
}),
|
||
{ color: th.ink },
|
||
P.at,
|
||
);
|
||
tl.set(cards, { backgroundColor: th.box }, P.at); // 3 stacked torn cards = the EMPTY box + zoom target
|
||
tl.set(allSlotLines, { backgroundColor: th.box }, P.at); // recolor any visible tapes
|
||
tl.set(allSlotSpans, { color: th.boxInk }, P.at);
|
||
}
|
||
|
||
// shake — per-beat JOLT around the base tilt, with a back-ease recoil.
|
||
function shake(P) {
|
||
var amp = P.amp != null ? P.amp : 22;
|
||
var rot = P.rot != null ? P.rot : 2.2;
|
||
var settle = P.settle != null ? P.settle : 0.15;
|
||
(P.beats || []).forEach(function (t, k) {
|
||
var j = jolt(k);
|
||
tl.fromTo(
|
||
lockupEl,
|
||
{ x: (j.x * amp) / 5, y: (j.y * amp) / 4, rotation: BASE_TILT + (j.r * rot) / 3 },
|
||
{
|
||
x: 0,
|
||
y: 0,
|
||
rotation: BASE_TILT,
|
||
duration: settle,
|
||
ease: "back.out(3)",
|
||
overwrite: "auto",
|
||
immediateRender: false,
|
||
},
|
||
t,
|
||
);
|
||
});
|
||
}
|
||
|
||
// expand — EXIT wipe: fade text, then the box ZOOMS (expo.in + blur) to swallow
|
||
// the frame. keeps the lockup lean; clip-path off so the scaled box fully covers.
|
||
function expand(P) {
|
||
var fadeAt = P.fade != null ? P.fade : P.at;
|
||
tl.to(
|
||
anchorEls,
|
||
{ autoAlpha: 0, duration: 0.16, ease: "power2.in", overwrite: "auto" },
|
||
fadeAt,
|
||
);
|
||
tl.to(
|
||
allSlotLines,
|
||
{ autoAlpha: 0, duration: 0.14, ease: "power2.in", overwrite: "auto" },
|
||
fadeAt,
|
||
);
|
||
var startAt = P.at,
|
||
endAt = P.to != null ? P.to : P.at + 0.46;
|
||
tl.set(lockupEl, { x: 0, y: 0 }, startAt); // clear shake residual (keep the lean)
|
||
if (P.theme != null) tl.set(cards, { backgroundColor: theme(P.theme).box }, startAt);
|
||
var mid = Math.max(startAt + 0.04, endAt - 0.12);
|
||
// stage A — the TORN mass grows but stays SUB-frame (≈half-screen, decelerating),
|
||
// so the eye reads "the box is swelling" across most of the window (matches the
|
||
// source, whose mass is only ~half-frame as late as f113). A big rest box covers
|
||
// the frame at a low scale, so cap stage A low and let stage B do the swallow.
|
||
tl.fromTo(
|
||
slotbox,
|
||
{ scale: 1, filter: "blur(0px)" },
|
||
{
|
||
scale: 2.2,
|
||
filter: "blur(4px)",
|
||
duration: mid - startAt,
|
||
ease: "power2.out",
|
||
overwrite: "auto",
|
||
immediateRender: false,
|
||
},
|
||
startAt,
|
||
);
|
||
// stage B — SWALLOW the frame in the last beats (accelerating), then flatten to solid.
|
||
tl.to(
|
||
slotbox,
|
||
{
|
||
scale: 34,
|
||
filter: "blur(12px)",
|
||
duration: endAt - mid,
|
||
ease: "power2.in",
|
||
overwrite: "auto",
|
||
},
|
||
mid,
|
||
);
|
||
tl.set(card1, { clipPath: "none" }, endAt - 0.03); // card1 (inset 0) flattens to a solid full-frame black-out
|
||
}
|
||
|
||
var OPS = { lockup: lockup, slot: slot, bg: bg, shake: shake, expand: expand };
|
||
|
||
// ── 7. RUN THE PROGRAM ───────────────────────────────────────────────────
|
||
PROGRAM.forEach(function (step) {
|
||
var fn = step && OPS[step.op];
|
||
if (fn) fn(step);
|
||
});
|
||
|
||
tl.to({}, { duration: 0.001 }, DUR);
|
||
tl.seek(0);
|
||
window.__timelines["group-six-recreate"] = tl;
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|