mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-13 07:40:06 +00:00
268 lines
11 KiB
HTML
268 lines
11 KiB
HTML
<!doctype html>
|
|
<html
|
|
lang="en"
|
|
data-composition-variables='[
|
|
{"id":"bgColor", "type":"color", "label":"Background", "default":"#000000"},
|
|
{"id":"textColor", "type":"color", "label":"Text color", "default":"#ffffff"},
|
|
{"id":"accentColor","type":"color", "label":"Period accent", "default":"#ff6a32"},
|
|
{"id":"flipWords", "type":"string", "label":"Flipbook words (comma-separated)", "default":"Design,Strategy,Branding,Marketing,Production,Campaigns,Experiential,Communication,Event Management"},
|
|
{"id":"resolveText","type":"string", "label":"Lock phrase (empty = pure flicker, no lock)", "default":"everything you need"},
|
|
{"id":"periodChar", "type":"string", "label":"Trailing accent (empty = none)", "default":"."}
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=1920, height=1080" />
|
|
<title>Roll-Driven Flipbook (Word Cycle → Optional Lock) — gruop_3 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: var(--bg, #000000);
|
|
overflow: hidden;
|
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
}
|
|
#root {
|
|
position: relative;
|
|
width: 1920px;
|
|
height: 1080px;
|
|
overflow: hidden;
|
|
background: var(--bg, #000000);
|
|
}
|
|
.g3-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
pointer-events: none;
|
|
}
|
|
/* grid stack — every layer occupies the same centred cell, so scaling a
|
|
layer transforms around its own centre (no translate(-50%) to clobber). */
|
|
.g3-stack {
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
.g3-stack > * {
|
|
grid-area: 1 / 1;
|
|
}
|
|
.g3-layer {
|
|
white-space: nowrap;
|
|
color: var(--text, #ffffff);
|
|
text-shadow:
|
|
0 0 18px rgba(255, 255, 255, 0.45),
|
|
0 0 46px rgba(255, 255, 255, 0.22);
|
|
}
|
|
.g3-flip {
|
|
font-size: 120px;
|
|
font-weight: 800;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.g3-resolve {
|
|
font-size: 116px;
|
|
font-weight: 800;
|
|
font-style: italic;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.g3-period {
|
|
color: var(--accent, #ff6a32);
|
|
font-weight: 800;
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- data-duration = the scene span. Set this to YOUR scene length; the script
|
|
reads it back so duration is single-sourced (no hard-coded value in JS).
|
|
Rebased to scene-relative time — content starts at ~0, no dead pre-roll. -->
|
|
<div
|
|
data-hf-id="hf-g3rt"
|
|
id="root"
|
|
data-composition-id="group-three-recreate"
|
|
data-width="1920"
|
|
data-height="1080"
|
|
data-start="0"
|
|
data-duration="3.8"
|
|
data-root="true"
|
|
>
|
|
<div data-hf-id="hf-g3ov" class="g3-overlay">
|
|
<div data-hf-id="hf-g3sk" class="g3-stack" id="stack"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
// ════════════════════════════════════════════════════════════════
|
|
// ROLL-DRIVEN FLIPBOOK (Word Cycle → Optional Lock) · dense_multiply
|
|
// A hi-hat ROLL drives a centred word that flips every 16th-note (flipbook),
|
|
// cycling a word list to FILL the roll. Optionally, when a SURGE/phrase
|
|
// follows, the flicker resolves (scramble-decode) into a locked phrase.
|
|
// Background is pure black. Every move is a 0ms autoAlpha set on a pre-built
|
|
// node (no GSAP textContent-sets, no onUpdate — both fail under seek render).
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
var root = document.getElementById("root");
|
|
|
|
// ── 1. BEAT MAP — the ONLY block you edit per track ─────────────────
|
|
// SCENE-RELATIVE time (starts at ~0 — no dead pre-roll). When reusing, map
|
|
// your audiomap roll [rollStart,rollEnd] onto [flipStart, flipStart+rollLen],
|
|
// i.e. subtract the roll's start offset. bpm stays your track's tempo.
|
|
var BEAT = {
|
|
bpm: 129.2, // tempo.bpm → 16th-note flip step = 60/bpm/4
|
|
flipStart: 0.1, // tiny entrance lead-in — NO dead air before the flicker
|
|
flipEnd: 2.19, // flicker length ≈ the source roll (~2.1s); auto-fills with words
|
|
resolveStart: 2.33, // lock phrase enters after a short clear (only if a phrase is set)
|
|
resolveLock: 3.33, // locks ~1s later (place just before a downbeat in your track)
|
|
};
|
|
// Scene length is single-sourced from data-duration (edit it in the HTML above).
|
|
var DUR = parseFloat(root.getAttribute("data-duration")) || 7.34;
|
|
|
|
// ── 2. Content variables ────────────────────────────────────────────
|
|
var defaults = {
|
|
bgColor: "#000000",
|
|
textColor: "#ffffff",
|
|
accentColor: "#ff6a32",
|
|
flipWords:
|
|
"Design,Strategy,Branding,Marketing,Production,Campaigns,Experiential,Communication,Event Management",
|
|
resolveText: "everything you need",
|
|
periodChar: ".",
|
|
};
|
|
var vars = Object.assign(
|
|
{},
|
|
defaults,
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {},
|
|
);
|
|
|
|
document.documentElement.style.setProperty("--bg", vars.bgColor);
|
|
document.documentElement.style.setProperty("--text", vars.textColor);
|
|
document.documentElement.style.setProperty("--accent", vars.accentColor);
|
|
var stack = root.querySelector("#stack");
|
|
|
|
var WORDS = String(vars.flipWords)
|
|
.split(",")
|
|
.map(function (w) {
|
|
return w.trim();
|
|
})
|
|
.filter(function (w) {
|
|
return w !== "";
|
|
});
|
|
var RESOLVE = String(vars.resolveText || "").trim(); // empty → pure flicker, no lock
|
|
var HAS_RESOLVE = RESOLVE !== "";
|
|
var PERIOD =
|
|
vars.periodChar && String(vars.periodChar).trim() !== "" ? vars.periodChar : "";
|
|
|
|
// ── 3. Derived timing (auto-fills the roll — adapts to any length / bpm) ─
|
|
var FLIP_STEP = 60 / BEAT.bpm / 4; // one word per 16th-note
|
|
var N_SWAPS = Math.max(1, Math.round((BEAT.flipEnd - BEAT.flipStart) / FLIP_STEP));
|
|
|
|
// ── 4. Build nodes ──────────────────────────────────────────────────
|
|
var flipNodes = {};
|
|
WORDS.forEach(function (w) {
|
|
if (!flipNodes[w]) {
|
|
var s = document.createElement("span");
|
|
s.className = "g3-layer g3-flip";
|
|
s.textContent = w;
|
|
stack.appendChild(s);
|
|
flipNodes[w] = s;
|
|
gsap.set(s, { autoAlpha: 0 });
|
|
}
|
|
});
|
|
|
|
var resolveNodes = [];
|
|
if (HAS_RESOLVE) {
|
|
var GLYPHS = "!<>-_/[]{}=+*^?#$%&01ABXYZ";
|
|
var decodeAt = function (str, p) {
|
|
var n = str.length,
|
|
out = "",
|
|
step = Math.floor(p * 48);
|
|
for (var k = 0; k < n; k++) {
|
|
var c = str.charAt(k);
|
|
if (c === " ") {
|
|
out += " ";
|
|
continue;
|
|
}
|
|
var lock = 0.4 + 0.6 * ((k + 1) / n); // lock left→right, in the back 60%
|
|
out += p >= lock ? c : GLYPHS.charAt((k * 7 + step * 3) % GLYPHS.length);
|
|
}
|
|
return out;
|
|
};
|
|
var DECODE_STEP = FLIP_STEP / 2; // scramble twice as fast as the flip
|
|
var N_STEPS = Math.max(
|
|
1,
|
|
Math.round((BEAT.resolveLock - BEAT.resolveStart) / DECODE_STEP),
|
|
);
|
|
for (var s2 = 0; s2 <= N_STEPS; s2++) {
|
|
var node = document.createElement("span");
|
|
node.className = "g3-layer g3-resolve";
|
|
if (s2 === N_STEPS) {
|
|
node.appendChild(document.createTextNode(RESOLVE));
|
|
if (PERIOD) {
|
|
var pd = document.createElement("span");
|
|
pd.className = "g3-period";
|
|
pd.textContent = PERIOD;
|
|
node.appendChild(pd);
|
|
}
|
|
} else {
|
|
node.textContent = decodeAt(RESOLVE, s2 / N_STEPS);
|
|
}
|
|
stack.appendChild(node);
|
|
gsap.set(node, { autoAlpha: 0 });
|
|
resolveNodes.push({ node: node, t: BEAT.resolveStart + s2 * DECODE_STEP });
|
|
}
|
|
}
|
|
|
|
// ── 5. Timeline — one paused GSAP timeline, absolute audio seconds ──
|
|
window.__timelines = window.__timelines || {};
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
// PHASE A — flipbook word-cycle on the 16th grid, FILLING the roll window
|
|
var prev = null;
|
|
for (var i = 0; i < N_SWAPS; i++) {
|
|
var t = BEAT.flipStart + i * FLIP_STEP;
|
|
var node = flipNodes[WORDS[i % WORDS.length]];
|
|
if (prev && prev !== node) tl.set(prev, { autoAlpha: 0 }, t);
|
|
tl.set(node, { autoAlpha: 1 }, t);
|
|
prev = node;
|
|
}
|
|
// glitchy reveal of the first word as the roll kicks in (scale is safe here)
|
|
tl.fromTo(
|
|
flipNodes[WORDS[0]],
|
|
{ scale: 0.8, filter: "blur(10px)" },
|
|
{ scale: 1, filter: "blur(0px)", duration: 0.12, ease: "power2.out" },
|
|
BEAT.flipStart,
|
|
);
|
|
|
|
// PHASE B — resolve (OPTIONAL). With a lock phrase: clear, then scramble-decode
|
|
// and lock, holding to the end. Without one: the last word just holds (locks).
|
|
if (HAS_RESOLVE) {
|
|
if (prev) tl.set(prev, { autoAlpha: 0 }, BEAT.flipEnd); // clear the flicker
|
|
var prevR = null;
|
|
for (var r = 0; r < resolveNodes.length; r++) {
|
|
if (prevR) tl.set(prevR, { autoAlpha: 0 }, resolveNodes[r].t);
|
|
tl.set(resolveNodes[r].node, { autoAlpha: 1 }, resolveNodes[r].t);
|
|
prevR = resolveNodes[r].node;
|
|
}
|
|
}
|
|
// (if no resolve, `prev` — the last flip word — stays visible to the end)
|
|
|
|
// Extend the timeline to the full duration with a no-op so the held final
|
|
// state renders on every frame to DUR (a set AT the timeline's last instant
|
|
// gets dropped by the seek renderer).
|
|
tl.set(stack, { opacity: 1 }, DUR);
|
|
|
|
tl.seek(0);
|
|
window.__timelines["group-three-recreate"] = tl;
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|