# Author a scene composition

> Create or edit a compositions/*.html plate: wrap in a template, set data-composition-id and data-duration, embed local fonts, build a paused GSAP timeline, and register on window.__timelines.

- Repository: heygen-com/hyperframes-launches
- GitHub: https://github.com/heygen-com/hyperframes-launches
- Human docs: https://www.grok-wiki.com/public/docs/heygen-com-hyperframes-launches-996f3eaa626b
- Complete Markdown: https://www.grok-wiki.com/public/docs/heygen-com-hyperframes-launches-996f3eaa626b/llms-full.txt

## Source Files

- `compositions/connector-morph.html`
- `compositions/chat-response.html`
- `compositions/followup-type.html`
- `compositions/compose-ui.html`
- `compositions/outro.html`
- `FRAME-claude.md`

---

---
title: "Author a scene composition"
description: "Create or edit a compositions/*.html plate: wrap in a template, set data-composition-id and data-duration, embed local fonts, build a paused GSAP timeline, and register on window.__timelines."
---

Each scene in the Claude Paper launch cut is a self-contained HTML plate under `compositions/*.html`: a `<template>` wrapper, a 1920×1080 root with `data-composition-id` and `data-duration`, embedded `@font-face` blocks, a paused GSAP timeline, and registration on `window.__timelines`. HyperFrames loads the plate when `index.html` references it via `data-composition-src`; the root master timeline in `index.html` only blends section seams—scene motion lives inside each plate.

## Plate anatomy

Every scene file in `compositions/` follows the same skeleton. The outer `<template>` id matches the composition id (`{id}-template`). Inside, a single root `div` carries the runtime metadata HyperFrames reads at load time.

```text
compositions/my-scene.html
└── <template id="my-scene-template">
    └── <div data-composition-id="my-scene"
              data-width="1920"
              data-height="1080"
              data-duration="6.0">
        ├── <style>          @font-face + scoped CSS
        ├── markup           scene DOM
        └── <script>         GSAP timeline + window.__timelines registration
```

| Attribute | Required | Purpose |
| --- | --- | --- |
| `data-composition-id` | yes | Stable id; must match `window.__timelines["…"]` key and `index.html` section reference |
| `data-width` / `data-height` | yes | Frame envelope; all plates use `1920` × `1080` |
| `data-duration` | yes | Scene length in seconds; timeline must cover this span (hold tweens at the end if needed) |

The root element also gets a unique DOM id (`#cm-root`, `#cr-root`, `#f4-root`, etc.) so all CSS is scoped under one selector and never leaks into sibling sections.

## Authoring workflow

<Steps>
<Step title="Copy a neighbor plate">

Start from the scene closest in narrative role. UI-interaction scenes (`connector-morph`, `chat-response`, `followup-type`) share composer chrome; editorial beats (`outro`, `sure-response`) are thinner; tool UI (`compose-ui`, `compose-tasklist`) adds window chrome.

</Step>
<Step title="Rename identifiers consistently">

Align four names to one slug (kebab-case):

- filename: `compositions/my-scene.html`
- template id: `my-scene-template`
- `data-composition-id`: `my-scene`
- `window.__timelines["my-scene"]`

The script's root query must use the same id: `document.querySelector('[data-composition-id="my-scene"]')`.

</Step>
<Step title="Set data-duration from the script beat">

`data-duration` is the contract with the master cut. Existing durations in this project:

| Composition id | Duration (s) |
| --- | --- |
| `connector-morph` | 6.7 |
| `chat-response` | 6.9 |
| `response-scroll` | 6.7 |
| `followup-type` | 6.0 |
| `thinking-big` | 1.3 |
| `compose-ui` | 13.3 |
| `sure-response` | 0.4 |
| `thinking-big-2` | 1.3 |
| `compose-tasklist` | 9.8 |
| `outro` | 3.4 |
| `tesla-rap` | 5.0 (standalone plate, not in master stack) |

If you change duration, update the matching `data-duration` and `data-start` on the section in `index.html`.

</Step>
<Step title="Embed fonts and design tokens">

Copy the `@font-face` block from a sibling plate (or subset for minimal scenes like `outro`). Paths are relative to the plate: `url(fonts/HankenGrotesk-normal-400-latin-fe1634.woff2)`. Every face uses `font-display: block` so render frames do not flash fallback glyphs.

Declare Claude Paper atoms as CSS variables on the root, matching `FRAME-claude.md`:

```css
#my-root {
  --paper:#F0EEE6; --ink:#262624; --muted:#6F6E66; --clay:#D97757;
  --surface:#FAF9F5; --hairline:#DCD8CC; --hairline-soft:#E7E3D6;
  --f-body:"Hanken Grotesk","Inter",system-ui,sans-serif;
  --f-serif:"Newsreader",Georgia,serif;
  --f-mono:"Spline Sans Mono","IBM Plex Mono",monospace;
  position:relative; width:1920px; height:1080px; overflow:hidden;
  background:var(--paper); container-type:size;
}
```

Use `cqw` for frame-relative sizing (`2.7cqw` body type ≈ 1.4vw floor at 1920). The dotted paper grain (`::before` radial-gradient at 34px pitch) is repeated across chat-family scenes for seam continuity.

</Step>
<Step title="Build a paused GSAP timeline">

Load GSAP from the CDN, create the timeline paused, set initial states with `gsap.set`, then add tweens. Pattern used throughout the repo:

```javascript
const tl = gsap.timeline({ paused: true, defaults: { ease: 'power3.out' } });
// gsap.set(...) initial states
// tl.to(...) motion beats
// tl.to({}, { duration: 0.4 }, lastMark);  // pad to data-duration

window.__timelines = window.__timelines || {};
window.__timelines["my-scene"] = tl;
```

<Note>
HyperFrames drives scene timelines by seek position; timelines must not autoplay. The master `claude-paper` timeline in `index.html` is also `{ paused: true }`.
</Note>

</Step>
<Step title="Register and wire into the master cut">

Registration is the last line before dev helpers:

```javascript
window.__timelines["my-scene"] = tl;
```

To include the scene in the full cut, add a section placeholder in `index.html`:

```html
<div
  id="sec-my-scene"
  data-composition-id="my-scene"
  data-composition-src="compositions/my-scene.html"
  data-start="25.0"
  data-duration="6.0"
  data-track-index="12"
></div>
```

HyperFrames fetches the plate, instantiates the template, and binds `window.__timelines["my-scene"]` to the section's local clock. Seam blending (crossfades, inverse zoom-through, cut-the-curve) stays in the root script—do not duplicate it inside scene plates.

</Step>
</Steps>

## Runtime registration model

```mermaid
sequenceDiagram
  participant Index as index.html
  participant HF as HyperFrames runtime
  participant Plate as compositions/*.html
  participant TL as window.__timelines

  Index->>HF: section data-composition-src
  HF->>Plate: fetch + clone template
  Plate->>Plate: script runs (IIFE)
  Plate->>TL: __timelines[id] = paused timeline
  HF->>TL: seek(localTime) each frame
  Index->>TL: __timelines["claude-paper"] seam blends only
```

Scene scripts own intra-scene motion. The root timeline owns opacity, scale, blur, and `xPercent` at section boundaries (`CUT`, `CUT2`–`CUT6` in `index.html`).

## Render-safe authoring rules

Plates in this project follow constraints discovered during HyperFrames render. Violations often produce static frames or seam pops.

### Await fonts before measuring text

Scenes that clip-reveal typed text await `document.fonts.ready` before measuring `offsetWidth`:

```javascript
(async function () {
  if (document.fonts && document.fonts.ready) {
    try { await document.fonts.ready; } catch (e) {}
  }
  // build timeline with baked widths
})();
```

`chat-response`, `followup-type`, and `compose-ui` bake per-character or per-line widths into `tl.set` / `tl.to` calls. Do not rely on `tl.invalidate()` or `tl.recent()`—the render-time GSAP proxy does not support them.

### Keep timelines seek-deterministic

- Prefer `tl.set` with precomputed values over runtime measurement inside tweens.
- Use deterministic jitter (indexed sine) for humanized typing, not `Math.random()`.
- Drive Lottie from the timeline via `onUpdate` + `goToAndStop` override (`chat-response`, `outro`), not `autoplay: true`.

### Pad timeline length to data-duration

If the last motion beat ends early, add a hold tween so local time matches `data-duration`:

```javascript
tl.to(box, { opacity: 1, duration: 0.4 }, 6.3);  // connector-morph pads 6.7s
tl.to({}, { duration: 0.2 }, 3.2);               // outro pads 3.4s
```

A shortfall leaves a black tail before the next section or crossfade.

### Match handoff state to the previous scene

Consecutive scenes often share DOM geometry so hard cuts stay invisible:

| Seam | Outgoing end state | Incoming start state |
| --- | --- | --- |
| `connector-morph` → `chat-response` | Composer at 84×18cqw; cursor at 40.7%, 63.7% with `power2.in` | Same cursor position; continues click path |
| `chat-response` → `response-scroll` | Thin composer at 87%; message bubble visible | Overlapping layout (root crossfade) |
| `followup-type` → `thinking-big` | Scroll frozen at response bottom | Inverse zoom-through handled by root |
| `compose-ui` → `sure-response` | Cursor sweeps left off-frame | `sure-response` word enters from `x:210` |

When editing one plate in a chain, inspect the previous plate's final `gsap.set` / last tweens and the next plate's opening state.

## Local development helpers

Every plate ends with query-string helpers ignored by the HyperFrames runtime:

```javascript
const q = new URLSearchParams(location.search);
if (q.has('t')) { tl.seek(parseFloat(q.get('t')) || 0); }
else if (q.get('dev') === '1') { tl.play(); }
```

Open a plate directly (or via preview) with `?t=4.2` to scrub, or `?dev=1` to autoplay while authoring.

## Verification

After editing a plate:

1. Preview the plate in isolation with `?t=` at seam-critical times (last 0.3s and first 0.3s).
2. Preview `index.html` and confirm the section appears at the correct `data-start`.
3. Confirm `window.__timelines["your-id"]` exists in the console after load.
4. Render at 1920×1080 and check the handoff against the previous and next scene.

<Warning>
A mismatch between `data-duration` on the plate root and the section in `index.html` causes sync drift against audio (`data-start` on `<audio>` elements) and root seam constants.
</Warning>

## Common failure modes

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Scene stays on frame 0 | Missing or mistyped `window.__timelines` key | Key must equal `data-composition-id` exactly |
| Text clips on final character | Width measured before fonts loaded | `await document.fonts.ready`; bake widths |
| 1–2 frame scroll pop at section cut | Live `offsetHeight` remeasure mid-render | Cache scroll max once (`followup-type` pattern) |
| Black gap before next section | Timeline shorter than `data-duration` | Add terminal hold tween |
| Font swap flash | Missing `font-display: block` or omitted `@font-face` | Embed full face list from sibling plate |
| Lottie drifts from GSAP | `autoplay: true` on Lottie | Timeline-driven `goToAndStop` only |

## Minimal reference plate

`sure-response.html` is the smallest complete example: two Newsreader weights, one centered word, 0.4s timeline, leftward entry matched to `compose-ui`'s exit sweep.

```html
<template id="sure-response-template">
  <div id="su-root" data-composition-id="sure-response"
       data-width="1920" data-height="1080" data-duration="0.4">
    <style>/* @font-face + scoped #su-root rules */</style>
    <div class="stage"><div class="word" id="suWord">Sure.</div></div>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
    <script>
    window.__timelines = window.__timelines || {};
    (async function () {
      if (document.fonts?.ready) { try { await document.fonts.ready; } catch (e) {} }
      const root = document.querySelector('[data-composition-id="sure-response"]');
      const word = root.querySelector('#suWord');
      gsap.set(word, { x: 210, opacity: 0.55, scale: 1.045 });
      const tl = gsap.timeline({ paused: true });
      tl.to(word, { x: 0, opacity: 1, scale: 1, duration: 0.18, ease: 'power3.out' }, 0);
      tl.to({}, { duration: 0.22 }, 0.18);
      window.__timelines['sure-response'] = tl;
    })();
    </script>
  </div>
</template>
```

`connector-morph.html` is the opposite extreme: multi-phase UI morph, cursor choreography, and a cut-the-curve handoff into `chat-response`.

## Related pages

<CardGroup>
<Card title="HyperFrames composition model" href="/composition-model">
How root and scene compositions register timelines, template wrappers, and the `window.__timelines` contract.
</Card>
<Card title="Claude Paper design system" href="/claude-paper-design-system">
Brand atoms, typography ramps, and frame-scale rules from FRAME-claude.md.
</Card>
<Card title="Scene catalog" href="/scene-catalog">
All composition ids, durations, narrative roles, and handoff constraints.
</Card>
<Card title="Edit the master timeline" href="/edit-master-timeline">
Wire a new section into index.html: data-start, z-index, and seam cut times.
</Card>
<Card title="Transition grammar" href="/transition-grammar">
Hard cuts, crossfades, inverse zoom-through, and cut-the-curve patterns between scenes.
</Card>
<Card title="Preview and render" href="/preview-and-render">
Validate scene loading and 1920×1080 output with the HyperFrames CLI.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Recovery for timeline registration failures, font stalls, and seam misalignment.
</Card>
</CardGroup>
