# REPORT — 15-variety-captions 「今日份快乐」

## Concept

A ten-second 萌宠综艺感 vlog: a very round grey tabby (「毛球日记」 ep.07) does nothing much in a
cosy room and reacts to six things that never happen, while a full set of 综艺花字 lands on the
beats — title, bouncing ？？？, a 爆炸框 震惊！！, a whispered aside, a sparkle-soaked 绝绝子 and
finally the 猫主子认证 seal.

The film is the caption, not the cat: the base layer is deliberately quiet, desaturated and
slightly out of focus so the high-saturation, thick-stroked lettering is the only thing that
shouts. The cat's face is the second actor — every caption has a matching expression, and the
expression arrives ~3–4 frames before the caption does, which is what makes the gag read.

## Beat sheet

120 BPM, one beat = 0.500 s, 20 beats. One cue sheet (`parts/cues.mjs`, mirrored in the header
comment of `audio.py`) drives picture, expression and sound.

| t | f | beat |
|---|---|---|
| 0.00 | 0 | hard camera punch-in, timer starts, fairy lights twinkle |
| 0.10 | 3 | **「今日份快乐」** — 4 glyphs, 2-frame stagger, `easeOutBack` pop 0→1.15→1.0; confetti ring; underline swooshes on at f11 |
| 0.10–1.30 | 3–39 | hold with per-glyph wiggle, kerning breathes, cat idles and blinks |
| 1.34 | 40 | title flies up-out **with a motion-blur trail** |
| 1.50 | 45 | **「？？？」** bounces in (3-frame stagger), record scratch + slide whistle; cat goes confused |
| 1.50–2.40 | 45–72 | each ？ bobs on its own phase; camera punch |
| 2.44 | 73 | ？？？ scatter outward and shrink |
| 2.60 | 78 | cat's shock face lands (wide eyes, tiny pupils, fangs) |
| 2.62 | 79 | **「震惊！！」** explosion frame slams in + 放射线 flash + white speed lines + camera shake + sweat drop |
| 3.94 | 118 | 爆炸框 flies out |
| 4.06 | 122 | **「（小声）」** aside bubble pops above the crown, two sweat drops slide; cat half-lidded. Deliberately the *quiet* beat — smallest type, softest sound |
| 5.06 | 152 | bubble drifts out |
| 5.20 | 156 | **「绝绝子」** in, gold halo + sparkle field; riser swells from 5.86 |
| 6.48 | 194 | star-glint eyes |
| 6.58 | 197 | **HERO** (66 %): 3-frame anticipation dip → punch to 1.13, 30-wedge radial flash, 28 speed lines, 26-star ring, sub + big impact + bell arpeggio + hardest camera punch |
| 7.42 | 223 | 绝绝子 flies out with blur |
| 7.88 | 236 | **「猫主子认证」** seal slams in from 2x with a dust ring, blur trail, shake, paper snap |
| 8.36 | 251 | seal settles into a float + a highlight sweeps its ring |
| 8.50–8.90 | 255–267 | vlog watermark retires for a clean end card |
| 8.86 | 266 | tagline 「今日份快乐已送达」 rises in |
| 8.90–10.00 | 267–299 | composed end hold — the calmest stretch of the film, kept alive by seal float/tilt/breathe + travelling ring highlight + drifting sparkles + cat breathing + tagline sway + grain |

## Signature features → where they are on screen

| feature | realised at |
|---|---|
| 多层描边圆体/胖体字：填充色 + 粗白描边 + 彩色外描边/投影 | `drawGlyph()` draws every glyph 4x — coloured outer stroke carrying the drop shadow → thick white stroke → vertical gradient fill → a light `lighter` specular band. Pink outer on the title (0.10 s), deep blue on ？？？ (1.50 s), dark red on 震惊！！ (2.62 s), magenta on 绝绝子 (5.20 s). Fonts: 站酷庆科黄油体 (chunky) + 站酷快乐体 (rounded). |
| 入场 overshoot：scale 0→1.15→1.0（约 8 帧） | `pop(u) = easeOutBack(u, 2.2)` peaks at ≈1.155 over exactly 8 frames, on every caption, plus a 6 % volume-preserving squash/stretch. |
| 逐字弹跳错帧 2 帧 | `STAG = 2/30 s`; the title staggers f3/f5/f7/f9, ？？？ uses 3 frames, 震惊！！ and the tagline use 2. |
| 情绪匹配：吐槽歪斜 / 惊讶爆炸框 + 放射线 / 尴尬汗滴 | 「？？？」 carries a per-glyph sine wobble and a −0.11 rad head tilt; 「震惊！！」 sits on a 17-spike 爆炸框 with a one-shot 28-wedge 放射线 flash and speed lines; 「（小声）」 gets two tear-drop sweat drops (one on the cat's crown, one sliding beside it) plus a half-lidded eye and a 0.14 rad tilt. |
| 出现时机对齐笑点/音效，比语音晚 2~3 帧 | Captions land 3–4 frames *after* the cat's expression changes (`SCHED` in `sketch.mjs` vs `CUES` in `cues.mjs`), and land on the same frame as their sound: the hits at 0.10 / 1.50 / 2.62 / 4.06 / 5.20 / 6.58 / 7.88 are identical in `parts/cues.mjs` and `audio.py`. |
| 长时间驻留花字加轻微 wiggle | Every held caption has per-glyph sine wobble in both `dy` (±5 px) and `rot` (±0.03 rad); the seal adds a float (±9 px), a tilt, a 1.2 % breathe and a highlight that travels around its ring. |
| 手绘装饰符号（爆炸框/汗滴/闪光） | 爆炸框, 汗滴, 4-point sparkles (`star()`), radial flash, speed lines, confused puffs — all drawn procedurally, no sprites. |
| 元素飞出带动转场（不是 crossfade） | Every transition is an element physically leaving: the title flies up rotating, ？？？ scatter outward, the 爆炸框 lifts away, the bubble drifts off-frame, 绝绝子 flies up into the hero. |
| 高饱和撞色 + 白边 | Locked palette: hot pink `#FF2E88`, cyan `#12B7FF`, gold `#FFC400`, red `#FF3B30`, always against a white stroke on a warm neutral room, so exactly one thing is loud per beat. |

## Tech route

HTML + Canvas 2D, no libraries, no WebGL. `index.html` loads two local `@font-face` files, waits
on `document.fonts.ready`, bakes the room into an offscreen canvas once (sharp draw → 5 px
`filter: blur()` pass → a 35 % `overlay` pass to put the bite back), draws frame 0, then flips
`window.__ready`.

`window.renderAt(t)` is a pure function of `t`. There is no `requestAnimationFrame` loop, no
`Date.now()`, no CSS animation, and no randomness outside `mulberry32(seed)`; the cat's
expression is a time-indexed crossfade table, the camera is a closed-form sum of decaying
punches plus seeded 1-D value noise for shake, and every particle field is a closed-form function
of `t`. Because the page can evaluate any `t`, the motion blur on the five fly-outs and the seal
slam is three real evaluations of the same caption layer at `t`, `t−1/60`, `t−2/60` rather than a
filter — weighted by exit progress, so a caption that has just let go stays crisp.

Measured in-page draw cost: **0.2 ms/frame** at 1080x1920 (budget ~250 ms). Grain is one seeded
256 px tile blitted around the frame with its offset quantised to 3 Hz, so it drifts instead of
boiling: 13.8 MB / 11.1 Mbps at CRF 15.

Sound: `audio.py`, deterministic, ~3.5 s to render. Pizzicato bass, chord stabs, claps on 2 and 4,
8th-note shakers, a pentatonic marimba line over F–C–Dm–Bb–F, plus per-cue sound design
(pop/boing/ding, a hand-built record scratch, a slide whistle, impacts, risers, a bell arpeggio,
a paper snap, water drips).

## Check numbers

| spec | required | actual |
|---|---|---|
| duration | 10.000 s | 10.000 s |
| resolution | 1080x1920 | 1080x1920 |
| fps | 30 | 30 (300 frames) |
| video | H.264 MP4 | h264, yuv420p |
| audio | stereo AAC 48 kHz | aac, 48000 Hz, 2 ch, 192 kbps |
| integrated loudness | ≈ −14 LUFS | −14.1 LUFS (LRA 3) |
| true peak | ≤ −1 dBTP | −1.4 dBTP |
| bitrate | 8–25 Mbps | 11.1 Mbps (13.8 MB) |
| black frames | 0 | 0 |
| frozen frames | 0 | 0 |
| console errors | 0 | 0 |
| dead spans (`rhythm.mjs 0.5`) | none | none; calmest window 9.5–10 s at 0.49 % moved, min frame Δ 0.266 |

## Harness bug fixed

`harness/lib/probe.mjs` accumulated subprocess stdout as a **UTF-8 string**
(`let out=''; p.stdout.on('data', d => out += d)`) and then did `Buffer.from(out, 'binary')` in
`scanFrames`. Binary raw-video bytes round-tripped through a utf8 decode are silently corrupted,
so black/frozen-frame detection was running on garbage data — for this film it reported 293
"frames" instead of 300 and a `meanRange` of `[240.96, 246.33]` when the true per-frame means are
`[202.58, 210.11]`. Fixed by collecting Buffers in `run()` and returning both a decoded `out` (for
the ffprobe JSON) and the exact `raw` Buffer, which `scanFrames` now uses. Verified: 300 frames,
correct means, 0 black, 0 frozen; `probe()` and `loudness()` unchanged.

## Honest top-3 weaknesses

1. **The base layer is an illustration, not footage.** There is no `assets/images` in this repo, so
   the "pet-vlog habitat" is a stylised flat scene with a simulated depth of field rather than real
   CC0 photographs under a Ken Burns. It reads as a very polished animated vlog, but the literal
   "footage under 花字" reading — the thing that makes the style funny in the wild — is performed
   rather than achieved. Real photos (with the palette desaturated to match) would sell it harder.
2. **The soundtrack is unmonitored.** There is no audio output in this environment, so the mix was
   built from the synthesis toolkit and the cue sheet without a single listen. The genre and the
   instrument set are right and every hit is frame-locked to its caption, but the record scratch and
   the slide whistle are chirp+noise approximations that may read as generic rather than as a real
   turntable scratch, and the bed's melodic line is deliberately simple. Onset-to-picture alignment
   is asserted by the shared cue sheet, not measured.
3. **The cat cannot act with its body.** Its repertoire is breathing, a tail sway, a head bob, a
   tilt, a blink, a pupil size, a mouth shape and a brow angle; there are no articulated limbs and
   no pose-to-pose body animation, so all of the acting is in the face. A rigged or hand-keyed cat
   would make the reactions land considerably harder. Secondarily, with a four-glyph title the
   composition keeps a fairly wide band of empty wall between the lettering and the cat's ears —
   deliberate breathing room, but a bolder choice would fill it.

## Self-scores (1–10; 6 = template-level, 8 = high-end agency, 9 = award shortlist)

| dimension | score | why |
|---|---|---|
| style fidelity | **9** | Every listed signature technique is present and executed correctly; the type system, stagger, expression matching, decoration set and colour discipline are the canonical form of 综艺花字. |
| concept | **8** | It is the brief's own creative seed rather than a new idea; the invented channel, vlog HUD, running timer and the "reactions to nothing" framing give it a small spine. |
| motion craft | **8** | Purposeful `easeOutBack` pops, 2–3-frame stagger, anticipation dips, squash/stretch, camera punches and decaying shake, real ghost-sampled motion blur on fly-outs. Docked for the cat's limited body animation. |
| design & type | **8** | Controlled five-colour palette, proper CJK punctuation (full-width ？？？ ！！ （）), 5 % safe margins, no element touches the frame edge, two display weights in deliberate contrast. |
| finish & texture | **8** | Depth-of-field base, warm light leak, vignette, twinkling lights, drift-quantised grain, glow and specular passes; no banding, no aliasing on the strokes, bitrate healthy. |
| sound & sync | **8** | Genre-correct bouncy variety bed with claps/shakers/pizzicato/marimba, seven frame-locked hits, mastered to −14.1 LUFS / −1.4 dBTP. Docked because it was never auditioned and the scratch is a synthesis. |
| technical | **9** | Pure function of `t`, seeded noise only, console clean, 0.2 ms/frame, exactly 10.000 s, 0 black/frozen frames, no dead spans, 11.1 Mbps. |
