A ~34s launch video for HyperFrames Cloud Render (managed rendering on HeyGen), built as a proper seek-safe HyperFrames project that plays in HyperFrames Studio. This doc is the source of truth for the next agent. Read it fully before editing.
No-VO, text + visuals. Story (Before-After-Bridge), told through a persistent Claude Code chat window: user kicks off a podcast render → asks to also render 5 projects from yesterday → Claude checks (Lottie) → reads the local render crawling at 1% → reports back → "Don't get blocked by your machine." → connect HeyGen → fleet renders all 6 in parallel → "all finished" → payoff grid + logo.
- Storyboard:
STORYBOARD.md· Design law:design-system/frame.md· Static mockups:contact-sheet.html - Two brands on screen, kept apart: Claude Code = the interface (clay
#D97757, the terminal window, the pixel-pet); HyperFrames/HeyGen = the product (mint→cyan#3CE6AC→#00E3FF). - Everything sits on warm paper
#F0EFE9.
cd /Users/jakemoran/Desktop/CC
unset HYPERFRAME_RUNTIME_URL # REQUIRED for cli preview — a wrong value fails silently
node repos/hyperframes/packages/cli/dist/cli.js preview --no-open --port 5211 \
projects/active/cloud-render-launchOpens the Studio at http://localhost:5211. (launch.json config: cloud-render-hf.) Select the
index composition and press play to watch the full film; select any scene to preview it alone.
- Has NOT been rendered. Do not render unless Jake explicitly asks (standing rule).
Follows the proven projects/active/sfx-music-launch pattern.
index.html= the root. A#cloud-render-launchdiv (data-composition-id,data-duration) containing one section<div data-composition-id data-composition-src data-start data-duration data-track-index>per scene. A master timeline (window.__timelines["cloud-render-launch"], paused) drives only the seam transitions (opacity/scale/position of the section wrappers).compositions/*.html= 10 scenes, each a<template>with a rootdata-composition-iddiv, scoped<style>(#root-id .x), and a<script>that builds a paused GSAP timeline and registers it onwindow.__timelines["<id>"]. 1cqw = 19.2px (container-type:size on a 1920×1080 root).- Scenes:
opener, ask, checks, worker, responds, textbeat, connect, fleet, finished, payoff. - Seams in
index.html: zoom-through (checks→worker, finished→payoff), cut-the-curve (responds→textbeat), hard cuts for chat continuity.
- Timelines paused, registered on
window.__timelines. NoDate.now()/ unseeded random. - No spawned tweens or DOM creation inside callbacks. Pre-create all elements; reveal via
timeline tweens.
textContenttyping via a tweenonUpdate(slice) is fine (forward-safe). - Root resolution MUST use the fallback (this was the bug that made the Index static):
Why: when the runtime inlines a sub-comp into the Index and re-runs its
const R = (document.currentScript && document.currentScript.closest('#op-root')) || document.querySelector('#op-root'); if (!R || !window.gsap) return;
<script>,document.currentScriptexists but the recreated script is OUTSIDE the root div, so.closest('#root')returns null. Without the|| querySelectorfallback the script bails and the timeline never registers → the scene is static in the Index (but fine when previewed alone). - Verify the Index drives nested timelines by deep-walking the
<hyperframes-player>shadow root: its iframe'swindow.__timelinesshould list ALL scene ids +cloud-render-launch(not just the master). If a scene id is missing, its script bailed.
payoff and textbeat currently use gsap.from(..., {immediateRender:false}) so they were visible
"at rest." That was a workaround from when I wrongly thought the Index didn't drive nested timelines.
It DOES drive them now (after the root-resolution fix), so you can author normal hidden→reveal
entrances. See issue #4 and #6 — those two scenes should be reworked to proper masked/hidden entrances.
DONE (2026-06-19): A, B, C. All five chat comps (
opener, ask, responds, connect, finished) now share one bottom-anchored.feed > .stacklayout. New rows start clipped below the fold (.r-in); on arrival the stack translates up by exactlynewRow.offsetHeight + gapso prior rows shift up and the oldest fades — this is both the issue-C reaction AND the issue-A continuity (each scene bottom-anchors its last settled row, so scene N's end frame == scene N+1's start frame by construction).connect/finishedresume the real transcript (full M4 reply, no truncation) per Jake's full-continuity call. Cursor target (issue B) now computed from the live.retgetBoundingClientRect(the old offset-chain helper ignored the.wintranslate(-50%,-50%)and parked the cursor off-screen). Verified in Studio: 11/11 timelines register in the composed index; all 4 chat seams match with ≤0.5px drift; cursor lands on↵within 0.5px.Refinements (round 2): (1) composer text bumped to 1.6cqw/weight-500 in ask/connect/responds/ finished so typed text matches the bubble it becomes when sent (opener left as-is). (2) Thinking is now a bottom-slot row (
.thinkinside.feed,bottom:0): on appear, history shifts up and it sits closest to the composer; the reply replaces it in that slot. Applied to responds, finished, AND a new brief thinking beat in opener (opener also drops its Local/CC chips as the window docks → the "On it…" reply clears the composer by ~44px). Opener SEND fixed: M1 starts clipped below the fold (feed empty through typing + morph — message never seen at the window bottom) then RISES up from behind the docked prompt box after the morph, same clipped-rise the other scenes use (was a fade-in-place at the bottom). (5) M1 text → "Hey, can we render my edited podcast video we worked on in HyperFrames?" (updated in opener/ask/responds — all 3 must stay identical for continuity). The longer prompt WRAPS to 2 lines: the input box auto-grows (.tlinewhite-space:pre-wrap + inputbar content-height) during typing then collapses on send before the morph; opener cursor now measures the live.ret(temporarily fills the prompt to find the ↵ at its wrapped position) so it lands on ↵ regardless of wrap. M1 feed bubble also wraps to 2 lines everywhere. (3)connectrewritten realistic — NO auto-connect; faithfully recreates Claude Code's bash PERMISSION PROMPT. Flow: user types "Okay cool — how do I do that?" → Claude "I'll connect HeyGen for you." → the composer morphs into the permission card (.permit: ">_ Bash command / hyperframes auth login / Connect this project to HeyGen Cloud Render / Do you want to proceed? / ❯ 1.Yes / 2.… / 3.…", clay border, selected row highlighted) and the chat pushes UP byPSHIFT(permit height − composer height) so messages clear the card — cursor clicks Yes → permit closes, composer restores, the command runs in the feed as the tool row$ hyperframes auth login→✓ Connected to HeyGen(.okpart2-stage).finishedresumes end frame[Q, A, TOOL](A = "I'll connect HeyGen for you."). (4)responds+1.0s read total (dur 3.4→4.4, cut 18.34→19.34); connect dur 3.8→6.4. Film now 38.4s (was 34.8) — downstream data-starts + master seam times in index.html shifted. Re-verified: 11/11 register, 4 seams ≤0.5px, permit shows & chat clears it ~60-70px, cursor on ↵ and on "Yes" ≤0.5px, ✓-result reveals + composer restores at end. Refinements (round 3): M1 text → "…we worked on in HyperFrames?" (opener/ask/responds, wraps to 2 lines; input box auto-wraps+grows during typing, opener cursor measures live.ret). D DONE —textbeatnow uses the masked upward waterfall (.wmoverflow:hidden /.wiyPercent 115→0, stagger .12); mint gradient kept. (The old handoff note to also waterfall the connect entrance is SUPERSEDED — connect is now a full chat scene whose messages rise from behind the prompt box; a word-waterfall there would clash, so it keeps the rise.) E DONE —fleetall 6 bars fill to 100% and all show "✓ done", staggered completions (cascade 0→2→4→6); "✓ done" pre-filled in markup + revealed by opacity (seek-safe, no callback). F DONE (payoff, latest): clean 2-row × 3-col GRID of 16:9 cards (CSS gridrepeat(3,1fr)+aspect-ratio:16/9,left/right:3cqwcentred vertically) maximising horizontal space — cards ~582×328px, native 16:9 (object-fit:cover, no distortion). Each card plays a REAL render clip fromassets/renders/(6 most-recent projects with renders: aiewf, mcp, sfx, promo, outro, framemd — downscaled to 640×360, ~4.5s,-an, via ffmpeg from projects/*/renders/). Videos usedata-start=0 data-duration=3.6 data-track-index=50-55 muted(drive like the pet — manual tl.seek won't advance currentTime; that's a verify limitation, they play in render). Labels = story names. Cascade (column sweep TL,BL,TM,BM,TR,BR), then the MIDDLE COLUMN (cards[1],cards[4]) clears + logo reveals in the vacated centre.Payoff final tweaks (latest): (1) TOP-LEFT clip is now
assets/renders/spotify.mp4(was aiewf — dropped; 6 clips are now spotify/mcp/sfx/promo/outro/framemd). (2) SUBTEXT CONFIRMED: the real CLI ishyperframes cloud render(repos/hyperframes docs/packages/cli.mdx — "HeyGen runs the render, pay per credit") — the subtext is correct. The service is called "HeyGen Cloud Render" in the story (connect scene); the command people type ishyperframes cloud render. (3) LOGO HAND-OFF: ~1s after the HyperFrames lockup reveals (2.15s), it crossfades to the HeyGen parent logo at ~3.3s. Payoff extended 3.6→5.0s (film now 41.0s; payoff is last so nothing downstream shifts; video data-duration 4.5). ⚠assets/heygen-logo.svgis a PLACEHOLDER (copied from archive/design-html-launch HeyGen_Primary_Logo_Black_RGB.svg) — Jake to drop in the real one (same path, or tell me the name); sizedwidth:20cqw, adjust if his differs. All A–F DONE.Transitions pass (round 4): (1) ask→checks is now an UPWARD cut-the-curve (master): chat slides up + fades (
yPercent:-13, power2.in, .22s) and checks rises up in (yPercent:14→0, power3.out, .32s). (2) worker is now FULL-SCREEN (.winset to 1920×1080 radius 0 at rest) running the render scan, then MORPHS DOWN to the chat-window footprint at its tl end (win→1599×875 r26, log fades) — master crossfades worker→responds as the window forms (terminal text → cloud reply). Mirrors the SFX window↔full-screen morph (opposite direction). (3) textbeat reverted from the upward waterfall to a RIGHT→LEFT sweep (wordsxPercent:60→0, staggerfrom:'end') — continues responds' leftward exit. (4) connect→fleet: fleet now starts at the chat-window footprint (crossfade from connect), MORPHS UP to full-screen (win→1920×1080), then the bar rows reveal ONE BY ONE (staggered0.6+i*0.27, each pops in → fills → "✓ done"). worker & fleet are the two full-screen "terminal" scenes; chat scenes stay windowed; morphs bridge them.Transitions pass (round 5 — revised): (1) checks→worker: worker now enters scaling UP (
scale 0.86→1, was 1.12→1). (2) worker is full-screen the WHOLE scene (no self-morph now); log is centred + large (.contranslate -50%,-50%, 2.1cqw); the slow ETA line has a mint→cyan highlight sweep (.markscaleX 0→1) and the counter TICKS DOWN 1s (elapsed 00:09→00:10, eta ~14m 51s→50s via onUpdate). (3) fleet content centred + large (font 2.1cqw, bigger bars). (4) worker→responds morph is now real: responds STARTS full-screen and MORPHS DOWN to the window in its own tl while the traffic lights, prompt box, and messages animate ON (win1920×1080→1599×875; bar/composer/pastRows fade+rise in over 0–0.6); master just crossfades worker→responds at full-screen (seamless dark). (5) fleet→finished does the SAME morph-down reveal infinished. responds +0.6 (dur 5.0) and finished +0.6 (dur 3.2) for the entrance → film now 39.6s; downstream data-starts + seams retimed. Continuity end-frames preserved (responds→connect, connect→finished still match).Seamless-seam fix (round 6): the morph crossfades were flashing the page bg (opacity-crossfading two opaque layers lets the cream bleed through at the midpoint). Fixed: the OUTGOING comp fades its CONTENT to a BARE dark surface at its end (worker
.con→0; fleet.con→0; connect.feed+.composer→0 but KEEP.bar), then a HARD CUT (set/set, no overlap) to the INCOMING which starts at the IDENTICAL bare dark surface (responds/finished full-screen dark content-hidden; fleet windowed dark WITH traffic lights matching connect). Hard cut between matched darks = invisible; content swaps via the fades + morph reveal. No more fade-to-background. Verified: at each cut section opacity is a clean {out:1,in:0}→ {out:0,in:1} switch.Pixel-pet rules (round 7) — ported from sfx-music-launch: the
pet-typing.mp4pixel-pet now follows the SFX rules in ALL chat comps (opener/ask/responds/connect/finished): the.petdiv lives INSIDE.inputbar(perched on the prompt's top edge:right:3cqw; bottom:100%; width:7.6cqw; height:4.4cqw), the feathered mask is on the<video>(not the container) so the bg dissolves — no visible box, LOOPED with multiple<video>instances to cover each comp (opener/connect use 2; others 1), unique ids + data-track-index (40–46), and it's NEVER faded (persists; opener's pet now rides the morph instead of fading). Because our chat is BOTTOM-anchored (SFX was top-anchored), the pet collided with the newest message, so the shared.feedbottom was raised 13cqw→16.5cqw (uniform → continuity holds) giving a ~34px gap above the pet. Verified: pet perched (parent=inputbar, bottom on input top), no message overlap, opener→ask & responds→connect seams still match.Textbeat motion (round 8, corrected round 9): IMPORTANT — the "cut-the-curve" Jake wants is the Spotify-style SINGLE
power4whip (fast throw across a seam), NOT the 3-phase nudge-curve. The 3-phase nudge (slow ramp→burst→long tail) is for SLOW group repositioning; its long tail made the words linger and read as GAPS. Reference:scratch/plv-spotify/videos/spotify-personal/scene_1.html(exitpower4.in) &scene_2.html(entrypower4.out, x-throw, stagger). Current textbeat: ENTRY =gsap.set(words, {opacity:0,x:82}); tl.to(words,{opacity:1,x:0,duration:.32,ease:'power4.out',stagger:.055})— reading order ("Don't" first), tight overlap (~3 words in flight at once), settles by ~0.85s. EXIT = downward waterfalltl.to(words,{y:200,opacity:0,duration:.4,ease:'power3.in',stagger:.05},1.9). Using a SINGLE staggered tl.to (not a forEach) keeps it lint-clean (no overlapping_gsap_tweens) with nooverwrite. textbeat→connect: connect WINDOW drops in via MASTER#s-connecty:-130→0singlepower4.out.45s (fast, was 3-phase). No retiming. Next: render when Jake asks ([[no-auto-render]]).
The chat scenes (opener → ask → responds → connect → finished) should read as one continuous
window — when we cut from one to the next, the messages should be in the same place at the same
size. Right now they jump: opener.html uses a .convo layout (absolute .usermsg top-right +
.agent at top:8.5cqw) while ask/responds/connect/finished use a .feed flexbox, and font sizes
/ positions differ.
- Fix: give ALL chat comps ONE identical chat layout — same
.wingeometry (already 83.3cqw×81cqh), same.feedstructure, same row font-size (1.6cqw), same gaps, same composer. The end frame ofopener(after its morph docks) must match the first frame ofaskpixel-for-pixel. Easiest path: makeopenerend in the SAME.feedlayout the others use (convert its.convoto.feed), and factor the shared chat CSS so it's identical everywhere. - Acceptance: scrub across each chat→chat cut — the existing messages don't move.
compositions/ask.html — the cursor tween lands at left:92.5% top:88.5%, which isn't on the ↵
send key. Fix: target the actual .ret glyph (compute its center from R.querySelector('.ret')'s
offset, or hand-tune to sit on it). Mirror the opener's tap grammar (tip-tap scale .84, ↵ flashes
clay). Check connect too (same pattern).
Right now history is static (just dimmed). Desired: when a new message comes in, shift the previous messages UP at the same moment, and fade the oldest down/out — so the newest message is prominent. The shift-up and the fade happen together, only on the arrival beat.
- Fix: in each chat comp, on the new-message reveal, add a timeline tween that translates the prior
rows up by ~one row height (
y) while fading the oldest (opacity → ~0), synced with the new row's entrance. Keep it seek-safe (tweens on pre-existing rows, no DOM mutation).
Currently the line is already on screen at scene start and doesn't do a real waterfall.
- Fix: copy the upward masked waterfall from
sfx-music-launch/compositions/button-spam.html(the.claim"Video without sound is…"). Pattern:<!-- each word wrapped: <span class="wm"><span class="wi">word</span></span> ; .wm{overflow:hidden} -->Remove the currentgsap.set(words, { yPercent: 115 }); // masked below their line (HIDDEN at rest) tl.to(words, { yPercent: 0, duration: .55, ease: 'power4.out', stagger: .12 }, t0);
from(immediateRender:false)approach. Words start hidden (below the mask) and waterfall up. Keep the mint→cyan gradient on "machine." - Apply the same waterfall logic to the
connectscene entrance (per Jake) — the new line should waterfall in the same way rather than a plain fade. - Reference:
sfx-music-launch/compositions/until-now.htmlalso shows masked word swaps.
compositions/fleet.html — the payoff of this scene is that all 6 renders complete, but right now
the bars fill to partial % (targets=[100,72,61,80,52,66]) and only one shows "✓ done".
- Fix: animate every bar to 100% and show "✓ done" on all 6 by the end of the scene
(stagger the completions so it feels alive, but they ALL land done). That sets up
finished("All 6 projects have finished rendering.").
compositions/payoff.html:
- The logo SVG is visible behind/through the tiles before it animates on (it's
z-index:3and was made visible-at-rest). Fix: the logo must be hidden until its reveal (opacity 0 at frame 0, then animate in) so it only appears once the center clears. (Now that the Index drives timelines, a normal hidden→reveal is fine.) - There are 12 tiles (4×3) but there are only 6 videos. Fix: 6 tiles total (e.g., 3×2), one per finished project (podcast + the 5 from yesterday). Rework the center-clear/logo placement for a 6-tile layout (e.g., logo lands over the center, tiles part to make room).
- Morph + cursor + typing (seek-safe):
sfx-music-launch/compositions/terminal-sfx.html(this is whatopeneris based on; note itsroot || querySelectorfallback). - Masked waterfall entry:
sfx-music-launch/compositions/button-spam.html(.claim),until-now.html. - Root stitch + seam master timeline:
sfx-music-launch/index.html. - Lottie, frame-driven (render-safe):
claude-paper/compositions/thinking-big.html(basis forchecks; theClaude Icon Loadinglottie lives atassets/claude-thinking.json). - Design tokens / fonts / motion law:
design-system/frame.md.
- Fonts: local
@font-facewith literal family names (Hanken Grotesk, Spline Sans Mono, Galaxie Copernicus) pointing tofonts/. cqw units; root iscontainer-type:size. - Lint with the Studio "Lint" button or
npx hyperframes lint— currently clean except a minoroverlapping_gsap_tweenswarning on the opener cursor tap (harmless; can addoverwrite:'auto'). - Timings are loose (~34s); Jake may want it tightened toward ~30s after the above fixes.
- Assets in
assets/:pet-typing.mp4(Claude pixel-pet),hyperframes-dark.svg(HeyGen·HyperFrames lockup),claude-thinking.json/.lottie, fonts infonts/. - Do not auto-render. Wait for an explicit request.