# REPORT — 06-shape-morph · “一形万物” (one shape, ten thousand things)

**Slug** `06-shape-morph` · **Style** morphing / shape morph
**Output** 1920×1080, 30 fps, 10.000 s, stereo AAC 48 kHz

---

## 1 · The concept, in two sentences

One hero shape moves continuously through eight *meanings* — a cup of coffee, a
sunrise, a sunset over water, a gull, a paper plane, a location pin, a heart and
finally a brand monogram — never cutting, always *becoming*, with the colour
field wiping behind it and the supporting line-work (steam → rays → waves →
wake → speed lines → radar rings → heartbeat → orbit) morphing in lock-step
underneath. The film is a single unbroken sentence about a brand that can be
anything you need it to be: **一形万物**.

---

## 2 · Beat sheet

Landings (the frame a shape is fully formed) are exactly 1.05 s apart, i.e. two
beats of the 114.29 BPM musical grid; picture and soundtrack are both driven
from the one cue sheet printed at the top of `parts/film.mjs` and `audio.py`.

| # | time | beat | what happens |
|---|---|---|---|
| — | 0.00 | intro | The cup drops in from above with an `easeOutBack` overshoot (scale 1.13 → 1.00); the five steam wisps settle; label `01 COFFEE / 咖啡` fades up. |
| 1 | **1.00** | 01 COFFEE | Cup is set. Landing bounce (damped e^(−9k)·cos(34k)) + a tick burst rings the silhouette. |
| 2 | **2.05** | 02 SUNRISE | Cup → circle → sun disc. 13 % squash on the way through; steam blends point-wise into eight pulsing rays. Colour field wipes vermillion over the night indigo (bar enters from the right). |
| 3 | **3.10** | 03 SUNSET | Sun → circle → dome-over-sea. Rays become five scrolling water lines; field goes orange → purple. |
| 4 | **4.15** | 04 SEAGULL | Sunset → circle → gull. Waves become a slipstream wake trailing below the wings; field to dusk blue. |
| 5 | **5.20** | 05 JOURNEY | Gull → circle → paper plane. Wake becomes eight speed lines; field flips to off-white with a rust leading bar (enters from the left). |
| 6 | **6.25** | 06 ARRIVAL | Plane → circle → pin. Speed lines become three radar rings; field to deep teal. |
| 7 | **7.40** | 07 HEART | Pin → circle → heart. Rings become the heartbeat pair; field to crimson. |
| 8 | **7.40** | hero | Heart → circle → **monogram**. Hero bloom + a 430 px expanding pulse ring, the strongest kick + a bell + a saw stab; the field falls to near-black. |
| 9 | 7.55–7.86 | wordmark | `morphe` sets letter by letter (0.055 s stagger, rising from 40 px below); six ascending blips mirror it exactly. |
| 10 | 8.20 | tagline | 一形万物 fades up in vermillion with `SHAPE MORPH` tracked beneath it. |
| 11 | 9.00 | final pulse | A wide ring expands out of the lockup and fades; a sub + a falling chirp land. |
| 12 | 9.55–10.0 | settle | The orbit arcs settle in opacity but keep rotating (constant speed), the end pulse keeps re-firing on the logo float, and a constant-speed light sweep crosses the card — the composed poster frame stays alive. |

Busy/calm contrast: each morph occupies ~0.72 s of its 1.05 s bar; during the
~0.33 s that a settled shape rests, a universal idle float (`idle = 1 − sin(π·uc)`,
6 px sway + 4.5 px bob + 0.006 rad turn) plus the drifting ambient blobs and the
beat's accent system keep something moving, so the eye rests without the film
freezing.

---

## 3 · How each signature feature is realised on screen

| Brief requirement | Where | How |
|---|---|---|
| **Continuous outline transition, no jumps** | every beat, 1.00 → 7.40 | Each adjacent pair is precomputed at load into two `flubber.interpolate` samplers — A → circle and circle → B — and `pathFor(i, u)` switches between them at u = 0.5. The path is re-rendered as an SVG string → `Path2D` every frame: a true vertex-matched interpolation, not a crossfade. |
| **Intermediate shape bridging** | the 0.72 s before each landing | All seven transitions pass through the *same* circle (`CIRCLE`, r = 300 in the shared 1000×1000 authoring box) — the canonical Apple-keynote bridge. The circle is also beat 2's own artwork, so the intermediate is never throwaway. |
| **Morph with translation + rotation** | all transitions | `morphTransform()` lerps `cx, cy, s, rot` between per-shape keyframes on the same eased `ue` that drives the path, so the shape slides and turns while it re-forms. Large moves align the squash axis to the travel vector via `atan2(dy, dx)`. |
| **Squash & stretch at the moment of morph** | `q = 0.13·sin(π·uc)` | Exactly 13 % (inside the 10–15 % window), peaking mid-morph and exactly zero at both ends, applied as `rotate(ang) → scale(1+q, 1−q) → rotate(−ang)` so the stretch always points along the travel direction. |
| **Slight rotation throughout** | `spin = 0.30·sin(π·uc)·dir` | ±0.30 rad of body rotation, signed per beat (`dir` alternates down the chain), so even a shape that barely translates visibly *turns* as it changes. |
| **cubic-bezier(0.7, 0, 0.3, 1)** | `CB = cubicBezier(0.7, 0, 0.3, 1)` | Every positional/rotational/scale lerp is driven by `CB`, plus 3-frame cushions at both ends of each morph (`uc = clamp((raw − 0.1) / 0.8)`). The shape is never frozen inside those cushions: the universal idle float fills the flat ends. |
| **flubber / vertex matching on the web side** | `parts/film.mjs` | flubber 0.4.2 (MIT) is vendored into `parts/` and used for all seven morphs at `maxSegmentLength: 12`; the fine resampling is what keeps travelling vertices tight and stops the outline knotting mid-morph. |
| **Sub-parts morph in sync** | `ACC[]` + `drawAccents()` | Eight polylines × 13 points per shape state, blended point-wise on the same `ue`, drawn with `catmullRom` and stamped with the *same* transform stack as the silhouette, so steam becomes rays becomes waves becomes wake becomes speed lines becomes rings becomes heartbeat becomes orbit. |
| **Colour-block wipes** | `drawWipe()` | The outgoing flat gradient is painted, then the incoming field is clipped to a skewed parallelogram whose leading edge sweeps the full width across the transition, carrying a 7 px accent bar (`alpha = sin(πp)·0.9`) as its leading light. |
| **Element-carried transitions** | all | No dissolve anywhere: a hard-edged colour block carries the field, the silhouette carries the form, the label carries off the bottom-left on a 22 px slide. |

---

## 4 · Tech route

- **Renderer** plain Canvas 2D, no framework, no WebGL. `index.html` loads
  `parts/flubber.min.js` (UMD, global `window.flubber`) then the ES module
  `parts/film.mjs`.
- **Determinism.** The film is a pure function of `t`: no `Math.random`, no
  `Date.now`, no `requestAnimationFrame`, no per-call state accumulation, no CSS
  animation. `renderAt(t)` recomputes the whole frame from `t` and the
  precomputed tables. The single grain tile and the vignette are built once at
  load with `mulberry32` (seed 11). The grain is *coarse and slow*: noise is
  generated at 96×54 and nearest-neighbour upscaled to a 384×216 tile, so each
  grain cell is 4 screen px (not 1), the variation is gentle, and the tile drifts
  at ~5 px/s in whole-cell steps (`round(t·5)`) rather than being re-randomised
  every frame — the texture survives H.264 instead of boiling into 75 Mbps.
- **Geometry.** All eight silhouettes live in one shared 1000 × 1000 authoring
  box, centred on (500, 500), which is what makes a single common circle a legal
  intermediate for every pair. Accents are authored in the same box, so they
  inherit the silhouette transform for free.
- **Performance.** Static layers (grain, vignette) are precomputed offscreen.
  Measured ≈ 0.41 s/frame at 1920 × 1080 *including* the headless-Chrome
  screenshot round-trip; drawing itself is a handful of `Path2D` fills. The full
  300-frame render took 2.1 min.
- **Sound.** `audio.py` synthesises everything with `shared/audio.py`: a
  five-note pad per beat-section, a pentatonic 8th-note marimba figure, then per
  landing a rising `whoosh` (0.40 s of anticipation) + a `pop` whose pitch climbs
  7.5 % per icon + a soft `kick`/`sub`. The hero beat adds a `riser`, an
  `impact`, a `bell` and a `stab`; the wordmark gets six ascending blips on the
  same 0.055 s stagger as the letters. Finished with light reverb,
  normalise(−4), limit(−1.2), `write(peak_dbfs = −1.5)`; the renderer then
  loud-normalises to the delivery target.

---

## 5 · Check numbers (from `out/06-shape-morph/qa.json`)

| Check | Required | Measured | |
|---|---|---|---|
| Duration | exactly 10.000 s | **10.000 s** | pass |
| Resolution | 1920 × 1080 | **1920 × 1080** (yuv420p) | pass |
| Frame rate | 30 fps | **30 fps**, 300 frames | pass |
| Integrated loudness | ≈ −14 LUFS | **−13.9 LUFS** | pass |
| True peak | ≤ −1 dBTP | **−5.5 dBTP** | pass |
| Bitrate | 8–25 Mbps | **8.9 Mbps** (11.2 MB) | pass |
| Dead spans (rhythm 0.5 s) | 0 | **0** (calmest window 0.72 %) | pass |
| Stuck frames | 0 | **0** | pass |
| Black frames | 0 | **0** | pass |
| Frozen frames | 0 | **0** | pass |
| Console errors | 0 | **0** | pass |
| Audio | stereo AAC 48 kHz | **aac, 48000 Hz, 2 ch, 192 kbps** | pass |

The renderer printed `OK` with no `PROBLEMS` line.

**Harness change made and reported:** `harness/render.mjs` chose the automatic
poster frame at a fixed `dur * 0.72` = 7.2 s, which for a film whose hero moment
is late lands *inside* a transition — the poster came out as a smeared mid-morph
blob. The poster time is now overridable per demo via an optional `posterT` key
in `demo.json` (clamped to `[0, dur − 1/fps]`); `dur * 0.72` remains the default,
so no other demo changes behaviour. This demo sets `"posterT": 9.9` to freeze the
composed lockup. No changes were needed in `shared/`.

---

## 6 · Honest self-critique — top 3 weaknesses

1. **The middle of each morph is a plain circle for a touch too long.** Because
   every pair routes through the *same* circle and `CB` is flat at both ends,
   frames at u ≈ 0.4–0.6 of a transition read as "just a blob". It looks like
   deliberate bridging, but a second, pair-specific intermediate (a capsule for
   gull → plane, say) would keep a hint of identity alive through the middle.
2. **The silhouette drifts across the chain without an obvious compositional
   rule** (`cx` 960 → 992 → 960, `cy` 566 → 398), while the label block sits in a
   fixed bottom-left position; on some beats the frame is top-heavy and the
   bottom-right third is empty. A more deliberate layout plan would make the
   eight stills read as a matched set.
3. **The soundtrack is legible but conservative.** A continuous pad bed gives an
   integrated LRA of only 0.7 LU: it punches on every landing (per-¼-second RMS
   swings between −19.5 and −8.7 dB) but never really breathes, and there is no
   melodic through-line that resolves on the hero beat. Well synced, not yet
   memorable.

## 7 · Scores

| dimension | score |
|---|---|
| Style fidelity (morph craft, intermediate, squash, easing) | **9** / 10 |
| Concept | **8** / 10 |
| Motion craft | **8** / 10 |
| Design & type | **8** / 10 |
| Finish (grain, bloom, vignette, poster frame) | **8** / 10 |
| Sound & sync | **7** / 10 |
| Technical (determinism, perf, console, QA) | **10** / 10 |
