85453da49f
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
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
353 lines
12 KiB
HTML
353 lines
12 KiB
HTML
<!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>
|