Game Show Flat 综艺节奏扁平
Rhythm of AI, 1997 → 2026·2:29·Lemo-Opuscarby Lemomo
A rhythm-game variety show in flat vector: toy-like mascots with thick ink outlines on candy-coloured stripes and sunbursts, every hit landing on a fixed tempo grid, call-and-response patterns, and a judgement word when each round is won.
The demo: The history of AI as a rhythm game: models take the stage on the beat, and a report card closes the show.
A rhythm-game variety show in flat vector: toy-like mascots with thick ink outlines on candy-coloured stripes and sunbursts, every hit landing on a fixed tempo grid, call-and-response patterns, and a judgement word when each round is won. References (grammar only): rhythm-action party games in the Rhythm Heaven mould (cue → answer, round cards, judgement words), Japanese variety-show telops (thick outlined captions, stripe and sunburst sets), WarioWare-style stage cards. Never use their names, characters, UI, levels or music.
The format works from 45 s (three rounds) to about 3 min (eight rounds). Narration is optional: the style can carry a film on short shouts and telops alone.
1. Essence, and what it is not
- Everything is on the grid. One fixed, fast tempo. Every pop, drop, stamp, typed character and camera punch sits at a (bar, beat) and is also a sound event; the score is generated from the picture's own timeline, so they can't drift.
- One drawing kit, many sets. Flat fills, one heavy ink outline, round caps, hard offset shadows; each round gets a new saturated background colour and pattern.
- Characters are toys. One body construction for the whole cast, told apart by colour, a belly patch, one head accessory and a name tag. They hop on the beat, squash on hits, throw arms up on a shout.
- A round = a repeatable verb + a win. The topic is split into rounds; each round has one action that can repeat on a beat, and ends on a judgement the audience reads in one glance.
Not a microgame frenzy (no timer or one-verb command per 3 s game, no change of art style per round), not a mid-century cartoon (no textured painted backgrounds, no character acting), not a keynote (no slow reveals, no gradients).
2. Materials & rendering
- Vector only, SVG or canvas paths at 1920×1080: flat fills, one outline colour (near-black), 8 px outlines on characters and props, 5–7 px on small parts, round joins and caps.
- No gradients, no blur, no texture, no soft shadow. Depth is a hard offset shadow: the same shape in outline colour shifted ~10 px (stickers, banners) to ~14 px (cards), then the coloured shape on top.
- Sets = one saturated background + a same-hue pattern one step lighter: diagonal stripes (often scrolling slowly), offset polka dots (~20 % white), or a rotating sunburst. A darker floor band with an ink line on top grounds the characters.
- Characters: a rounded bean body, stubby arms with mitten hands, oval eyes with a catch-light, a mouth that swaps to an open one. Humans share the construction; older machines or eras read through a boxier silhouette.
- UI kit: round card, corner sticker banner, judgement word, burst (star + ring + speed lines), scoreboards.
render(t)is a pure function oft: every element's state is recomputed fromt, never carried over from the previous frame.
3. Colour logic
- Ink + paper + candy. Near-black ink, white, and a warm cream for cards and paper props; around eight saturated candy hues (yellow, orange, red, pink, green, lime, sky, blue) plus two or three deep hues with darker partners for night or finale sets.
- One set = one hue. The background, its pattern and the round card share a hue; the card changes colour to match the set it introduces. Neighbouring rounds never share a hue.
- Characters own colours. A contestant's body colour is its identity and never changes; keep it distinct from the set it stands on (a character is never the same hue as the background).
- Big words are white or yellow with a thick ink stroke; judgement words may be pink or yellow.
- Set examples: tangerine ground
#FFB870with#FFA85Astripes; teal with mint dots; navy with a gold sunburst.
4. Type & subtitles
- Display: a heavy rounded sans for Latin and numbers (e.g. Fredoka 700); a black-weight sans for CJK (e.g. Noto Sans SC 900); a hand-lettered face only for handwritten props (e.g. ZCOOL KuaiLe).
- Telop text: white or yellow fill with an ink stroke ≈ 14 % of the font size painted under the fill (
paint-order: stroke). Titles are built glyph by glyph so each character can pop in. - The telops are the subtitles. Round cards, corner banners, labels, prop text and judgement words carry the meaning; keep them short (≤ ~20 CJK characters on a banner line, ≤ ~12 on a plate; Latin runs ~1.6× wider). With narration, subtitles are telop plates at the bottom in the same outlined type, never a thin generic caption.
- Hold every text ≥ max(1.8 s, speech + 0.6 s); a judgement word may be shorter because it is read at a glance.
5. Motion quality
- Idle = hop on the beat: a parabola per beat (8–30 px); neighbours hop on the off-beat so a crowd ripples.
- Hit = squash: a damped wobble (~30 rad/s, ~0.35 s) after each hit; a small squash on every downbeat.
- Entrances drop from the sky with ease-in and land exactly on their beat, so the sound lands with them. Crowds drop in one per beat or half-beat.
- Pops use back-ease for anything appearing (cards, stickers, stamps, words); ease-in for exits. Nothing fades in slowly: things are either on or popping.
- Shout pose: arms up + open mouth for about half a beat.
- Typewriter reveals on fractions of a beat, with a click every few characters.
- Scene changes are hard cuts on bar lines, with a very short white flash. A fade is saved for the very end, if at all.
- Nothing moves off the grid: an event between beats sits on a subdivision.
6. Camera grammar
The stage is a frontal, symmetrical proscenium; the frame itself does the moving. A vocabulary, not a route. Opening and ending come from the topic.
| Move | What it expresses | Can serve |
|---|---|---|
| Beat punch (zoom 0.8–5 % around centre, decays in ~0.16 s) | the hit, felt in the frame | any accent; a slam; a count-in |
| Several punches in one bar | the frame breathing with the music | a chorus; an escalating pattern |
| Locked frontal wide | the whole stage as a game board | a pattern being set up; a crowd; a scoreboard |
| Hard cut on the bar line + flash | a new round, a new set | chapter changes; a before/after |
| Split stage (two halves, two sets) | two sides answering each other | a rivalry; human vs machine; old vs new |
| Shrink to thumbnail (a finished bit scales down into a slot) | memory, accumulation | a recap; "and then all of this happened" |
| Vertical scroll of the set (floor to sky) | rising stakes or scale | a leaderboard; growth; a tower of items |
| Snap zoom onto one prop (hard cut to a 2× framing, back on the next bar) | this detail is the joke | a price tag; a number; a face reacting |
Keep the sum of simultaneous punches below ~0.06 so the frame edge never shows. Characters stand on the floor band; banners top-left, judgement words centre-top. Hard cuts only; no dissolves.
7. Sound palette
- All synthesized from the picture's events: a sine kick with soft saturation, band-passed noise snare and clap, hats, slap bass, detuned-saw brass stabs with a closing filter, square lead with vibrato, marimba-like pluck, music-box bell, triangle pad, square arp, cowbell.
- A bright major pop harmony on a short chord loop, one chord per bar; each round gets its own flavour (funk, quiz, electro, dream, heavy…), not a new song.
- Event foley is toy-like and pitched: pops, plop + boing landings, stamps, typewriter clicks, buzzers, dings, servo whirs, splats; pitched pips can spell a melody as a crowd drops in.
- Shouts, not sentences: one- to three-word calls ("Hey!", a number, a verdict), placed 20–30 ms before the beat so the consonant lands on it. A crowd shout = several different voices stacked a few ms apart and panned across the stereo field.
- Options to vary intensity (pick what the film needs): a jingle (crash + rising brass + snare roll) into each round; a transposition up for a finale; dropping to drums only for one bar before a big hit; a half-time bar for a reveal; one bar of silence broken by a single shout.
- Mix: voices and foley on their own bus; music ducked from that bus's envelope; soft-clip rather than hard limit; −14 LUFS, true peak ≤ −1 dB.
8. Native moves
A menu: use the ones your story needs.
- Rounds with a card. A 1-bar card (round number + name + a tag such as a year or a step) flies in on a jingle, then play. Fits content like: the steps of a recipe; the stages of a startup; the chapters of a museum tour.
- Call-and-response. Anything with a turn becomes a rhythm pattern; speeding up the pattern (quarters → eighths) is the plot. Fits content like: a teacher's question and the class answer; a customer order and the barista's reply; ping-pong between two negotiators.
- Judgement word. A big outlined word pops at an angle on the round's last hit. Fits content like: "Delivered!" on a logistics step; "Approved!" on a permit; "Personal best!" on a training plan.
- Stats as game UI. Numbers become podium scores, counters racing to a target, price tags getting hammered, bar charts that grow per beat. Fits content like: fundraising progress; calories per dish; downloads of an app.
- Remix board. One set with a timeline ribbon; each item gets a few bars centre stage, then shrinks into a slot along the bottom; in the finale all slots bounce together. Fits content like: a year in review; a team's release list; a festival line-up.
- Crowd drop. Many contestants drop in one per beat, each with a pitched pip. Fits content like: every member of a club; the planets; the ingredients of a dish.
- Next-level tease. Something hatches or unlocks. Fits content like: a next version; next season; a next grade.
- Report card. A typewritten recap with a grade stamp. Fits content like: a quarterly review; a trip summary; a final score.
9. Pitfalls of the medium
- Stateful updates break parallel renders: a worker starting cold shows the initial state. Recompute every pose from
t; compare single stills with a sequential render. - Cue lists drift when cut flashes, jingles and arrangement are separate hand lists: derive them from one table.
- Fixed-length audio buffers cut the tail: size them from the duration.
- Text measured before fonts load overflows plates: load fonts, then build.
- TTS misreads digits and symbols in shouts: spell them out.
- Real products as characters: names on tags, original mascots, no logos or UI; real people only as generic stand-ins.
10. Engine
demo/frame_head.html (SVG helpers: el, txt, tf, op, easing E, per-glyph chars/charsPop, measure), demo/main_v1.js (tempo grid at(bar, beat), ev() sound events, punch(), hopY/hitSq, gtext telops, stripes/dots/raysEl sets, burst, judge, banner), demo/main_b.js (bean() characters + pose(), human(), scene(t0, t1, build), card(), dropY, heyAt), demo/music.py + demo/synth_lib.py (event-driven score and SFX). The contract: scene() builds nodes once and returns update(t), which only sets attributes. Module table and a minimal new round: DEMO.md.
11. Variation space
You decide the rounds and their verbs, the cast, the sets and colours, whether there is narration, the tempo, the opening, the ending and the length. The grid, the drawing kit and the telop language stay.
All far from our demo:
- Structures: a tournament bracket (two contestants per round, the winner advances, the final is a split stage); one long round that keeps escalating its single pattern from quarters to sixteenths as the story's pressure rises; a quiz show with three contestants where each question is a fact of the topic and the scoreboard is the argument.
- Openings: the judgement first (a "Perfect!" pops on an empty stage, then we rewind to how it was earned); a single contestant alone hopping on a bare floor until the crowd drops in; the scoreboard showing a final number, then counting back to zero.
- Endings: a group photo (everyone freezes mid-hop, a flash, the frame becomes a polaroid on a card); a level-select map where every finished round is a lit node; a curtain falling bar by bar with the last shout under it.
How our demo was made (story, shots, score, end card, build): DEMO.md. Read it after your treatment exists.
Director's notes for the demo film
One example among many. Don't reuse its story, arc, shots, props or timings.
Demo: AI进化节拍 · Rhythm of AI 1997 → 2026 (v3, 148.8 s, 1920×1080, 30 fps) · game-show.mp4 · source in demo/ · engine: plain SVG DOM with a pure render(t), screenshotted by Playwright + Chrome Headless Shell; original score synthesized in numpy from the picture's own events.json.
Language note. All on-screen text in the demo is Chinese (level names, year banners, labels, the report card, the credit card). The only audio words are short English shouts ("Checkmate.", "Hey!", "Question!"). There is no narration. To make an English version, see "Switching the text to English" below.
Story & structure
The history of AI from 1997 to 2026 staged as a game show made of rhythm levels. The film opens on a title set where the whole cast drops in on the beat and counts "One! Two! Three! Four!". Then comes a run of levels. Each level is one bar of title card (GAME N + name + years, flying in on a brass jingle) followed by eight bars of play: a pattern is set up in the first half, answered or escalated in the second, and the last beat (bar 7, beat 3) is a slam + "Perfect!" judgement. A remix level near the end piles every recent event onto one set, with a month timeline across the top and shrunken thumbnails of each finished bit parked along the bottom. The ending hatches a new character from an egg, types out a report card ("AI 进化史 · 成绩单") with a Superb! star, and closes on a credit card.
How the demo spent the native moves:
| Native power | Demo use |
|---|---|
| Levels with years | Chess and Go (1997–2016), quiz show (2011), attention factory (2017–2020), AI art studio (2021–2024), chat chorus (2022–2023), thinking + price war (2024–2025), agent pipeline (2025), then a 2026 remix. |
| Call-and-response | Human move / machine move (chess), question / buzzer (quiz), lead "Hey!" / crowd "Hey!" (chorus), task drops / stamp DONE (agents). Go stones on eighth notes after chess on quarter notes. |
| Judgement words | "Perfect!", "第 37 手!", "首超!", price tags getting hammered, "9.9 ✓" thought bubbles. |
| Stats as game UI | Podium dollar scores, a users counter racing to 1,000,000, parameter counts, price tags ($60 → $2.19), a bar chart that grows by beat. |
| Remix / thumbnails | One set, a month ribbon, each item gets 2 bars centre stage and then shrinks (×0.27) into a slot at the bottom. In the finale all thumbnails bounce together under a giant year. |
| Egg → report card → credit | A "next level" tease (NEXT egg hatches a "???" baby bot), then a typewriter recap of three lines, then the credit card. |
Demo shape: cold open on the cast + count-in (4 bars) → 7 levels of 9 bars (card + 8) → 1-bar REMIX card → 16-bar remix (7 items × 2 bars + 2-bar finale) → 9-bar ending (egg 2 bars, "Hi!" 1 bar, report card 3 bars, credit 3 bars incl. 0.8 s fade). Total 93 bars = 148.8 s.
How we adapted the topic: we listed the phases, gave each one one verb that can repeat on a beat (place, buzz, pump, splat, sing, hammer, stamp), a year range and a set colour, and picked one "Perfect!" moment per level. The topic had a crowd (many companies and products), so they became bean contestants with name tags dropping in one per beat.
Shots
No camera travel anywhere in the demo: a frontal, symmetrical proscenium throughout. Tempo BPM = 150, B = 0.4 s, BARL = 1.6 s.
| Section | Bars | Camera / cut | Note |
|---|---|---|---|
| Cold open + title | 0–3 | locked wide, punches on each count | "AI 进化节拍" 200 px, white with a 26 px ink stroke, glyphs popping 0.06 s apart; subtitle "RHYTHM OF AI · 1997 → 2026" in Fredoka; the cast drops in behind it one per half-beat, each with a pitched pip |
| Levels 1–7 | 4–66 | hard cut + 0.08 s white flash on each card bar; punches 0.008 (ticks), 0.02 (hits), 0.03–0.05 (slams) | card bar, then 8 bars of play, slam + "Perfect!" on bar 7 beat 3 |
| REMIX card + remix | 67–83 | locked wide, thumbnails shrink into bottom slots | month ribbon across the top |
| Ending | 84–92 | hard cuts on bar lines; the only fade: last 0.8 s to navy | egg → "Hi!" → report card (typewriter, one character per 1/8 beat, key click per 2 characters) → credit card |
Score structure
- Score (
music.py+synth_lib.py): 150 BPM, a four-chord loop F–G–Em–Am (the J-pop "royal road", one chord per bar,CH), two 8-note melodiesMA/MB. Instruments: tanh-shaped sine kick, band-passed noise snare and clap, hats, slap bass (slap, patterns funk/drive/pump/soft), detuned-saw brass stabs with a closing filter, square lead with vibrato, pluck (marimba), bell (music box), triangle pad, square arp, cowbell. - Each bar gets a mode in
PLAN(main,count,funk,quiz,electro,dream,film,pop,popBig,think,heavy,agent,agentFast,fill,future,futureBig,egg,calm,fanfare,end), one flavour per level. Level cards get a jingle (crash + kick + rising brass arpeggio + snare roll into the level). The 2026 remix transposes up 2 semitones. - Event-driven SFX:
node render.mjs eventsexports everyev()from the picture toevents.json(474 events in the demo).music.pymaps each event name to a sound:tock/bleep(chess),stone(Go),ding/boop/buzz(quiz),land(plop + boing),pip(pitched pop, usesf),slam/bigslam,stamp,type,crack/hatch,servo,liftoff,swish,clap,drop,blip,kickhit,splat,denoise,cheer. Unknown names are silently ignored. - Shouts:
v:<name>events playvoices/<name>.wav. Voices are macOSsaysystem voices (Samantha, Fred, Zarvox, Junior, Kathy, Ralph, Superstar) at rate 170–220, pitched up ×1.0–1.35 without changing length (asetrate+atempo, seemake_voices.sh). Zarvox (robot) says machine lines ("Checkmate.", "Move thirty seven.", "Attention!"); Samantha/Fred are hosts. The crowd "Hey!" is six different voices stacked 5–6 ms apart and panned −0.6…+0.6 (v:crowd,v:heyAll,v:heyBig);v:heyVarpicks one voice per contestant. Shouts are placed 20–30 ms before the beat. - Mix: voices + SFX go to their own bus; the music is ducked by up to 40 % from a 12 Hz envelope of that bus; last 0.8 s fades; peak-normalize to 1.5 then
tanhsoft-clip ×0.9 →music.wav(44.1 kHz).finish.shthen applies two-pass loudnorm to −14 LUFS / TP −1.2 and resamples to 48 kHz AAC 256k.
Palette & props
Canvas & line: 1920×1080 SVG. Outline K.ol = #1B1B1B, width OW = 8 px (SO spread: stroke, 8 px, round join and cap); 5–7 px on small parts. Hard offset shadow +10 px (banners) or +14 px (level cards).
Palette (K in main_v1.js/main.js):
| Role | Colours |
|---|---|
| Ink / paper | ol #1B1B1B, white #FFFFFF, cream #FFF6E5 (cards, report card) |
| Candy primaries | yellow #FFD23F, orange #FF8C42, red #FF5A5F, pink #FF8FB1, green #5BD68A, lime #C6F16D, sky #8ED1FC, blue #4D7CFE |
| Deep sets | purple #6B4FBB / purpleD #46318F, teal #3FB8AF / tealD #2B8F88, navy #26264A (ending) |
| Props | wood #C98A52 / woodD #8E5530, gray #C3CAD9, chalk #2F5D50, skin #FFD1A8 |
| Character hues | gpt #19B388, claude #D97757, gem1 #4E7CF6 / gem2 #9B6CF0, llama #F4E9D8, whale #4D6BFE, dblue #2B59C3 |
Sets use stripes() (diagonal bands, often slowly scrolling), dots() (offset polka dots at 18–25 % white) or raysEl() (a rotating sunburst). Floor band at y = 860–900 with an 8 px ink line on top.
Type: Chinese = Noto Sans SC 900 (F.sans) everywhere; Latin display = Fredoka 700 (F.round: GAME N, years, "Perfect!", name tags, numbers); ZCOOL KuaiLe (F.cute) only for hand-written props. Big words use gtext(); title text uses chars().
Characters (bean() in main_b.js): a rounded bean body (160 px wide, 172 px tall at scale 1), two stubby arms with white mitten hands, two foot ovals, oval eyes with a white catch-light, pink blush, a small curved mouth that swaps to an open red mouth. Identity = body colour + belly patch + one head mark (antenna ball, heart, "?", moon, bolt, cloud, sprout, brush, beret, captain hat, llama wool, whale spout) + an ink name-tag pill under the feet. Gemini is two half-size beans (twins()). Humans (human()) have a shirt, a round head, hair cap, optional glasses and a sweat drop. Machines of the 1990s–2010s are boxier robots (robotDB, robotAG, robotT, robotW).
Recurring UI:
- Level card (
card()): full-bleed set colour + scrolling stripes, a cream panel (1240×460, r 50, 10 px outline, 14 px shadow), a pill tag "GAME N", the level name auto-fitted to 720 px, the years in the set colour, an icon hopping on the left. - Year banner (
banner()): top-left sticker: year in Fredoka + name + one-line subtitle, slides in from the left with a back-ease and lifts out upward. - Judgement (
judge()): 120 px Fredoka word, pink/yellow with an 18 px ink stroke, pops at −6°. - Burst (
burst()): 8-point star + expanding white ring + 8 ink speed lines, 0.3 s.
Motion numbers: hopY(t, h) h 8–30 px; hitSq 30 rad/s for 0.35 s; downbeat squash 1 - 0.1*exp(-frac(t)*16); dropY falls 500–900 px; heyAt 0.24 s.
Subtitles & titles. No burned-in subtitles: on-screen words are the telops themselves (≤ 20 CJK characters on a banner subtitle, ≤ 12 on a plate). game-show.srt (generated by make_srt.py from events.json + voices/lines*.txt) lists only the English shouts, one cue per shout, repeated crowd "Hey!"s merged.
End card
The demo's last card carries "LemoLab × Claude Opus 5.5" (× is U+00D7). It is the first line of the credit card that pops in at bar 90 (144.0 s): a cream 1440×360 card (r 40, 8 px ink outline, −1.5° tilt, back-ease pop over 0.3 s) with three centred lines: LemoLab × Claude Opus 5.5 in Fredoka 700 66 px ink (y −80), 本片由 Claude Opus 5.5 全程代码生成 in Noto Sans SC 900 60 px (y 30), 下一关:正在训练中…… 50 px in K.claude (y 118). It holds 4 s and fades to navy with the film. Code: the credit block in the ending scene of main_b.js. This sign-off belongs to this library's demo only; a user's film carries no LemoLab credit and no copy of this card (take the credit block out when reusing the code).
Rights & facts
- macOS system voices are licensed for personal, non-commercial use under the macOS licence. For a commercial or public release, re-voice the shouts with Kokoro (
core/tts/) or another cleared TTS. - Real brands: the contestants stand for real AI products (GPT, Claude, Gemini, DeepSeek, Kimi, Grok, Midjourney…): names only on name tags, original mascots, loose colour nods; never official logos, wordmarks or product UI. Real people (Kasparov, Lee Sedol, the KEN / BRAD podiums) appear only as generic cartoon stand-ins in factual banners.
- Facts date fast. The 2026 remix (Sonnet 5 as default, WAIC 2026, DeepSeek V4-Pro, "GPT-6 Astra", token-volume figures) was written as current news at production time. Re-check every figure and name before reusing any of it.
- The G7 task note on screen literally says "做一个节奏天国风格的视频" (names a real game series). In new films describe the genre, don't name it.
Switching the text to English
All strings are literals in main_b.js (and three in main_v1.js):
- Level cards:
card(SECT.gN, 'GAME N', '<name>', '<years>', …); the name auto-shrinks to 720 px. - Year banners:
banner(g, '<year>', '<name>', '<subtitle>', col); widths auto-measure. - Remix:
item(i, m0, m1, '<label>', …),plate(p, '<text>'), the month ribbon`${m}月`and the finale label'2026 · 未完待续','还没唱完 →'. - Props: quiz
QApairs, the Watson sign, the "9.11 和 9.9" chalkboard, the task note, the users counter label'用户', report card title +lines, the credit text, the titlechars()strings. Inmain_v1.js: the robot chest label'注意力'(robotT) and the sentence tokens in G3. - Prefer
F.round(Fredoka) for Latin headings and keepF.sansfor body;init()preloads fonts from the page's own text, so no font list changes are needed. English runs ~1.6× wider than Chinese: check banners and plates with stills. - Re-run
make_srt.pyif shouts change; the shouts are already English.
Build notes
styles/game-show/demo/
main_v1.js v1 source: palette K, tempo (at/ev/punch), hop/squash helpers, stripes/dots/rays, burst, judge, banner, 1990s robots, chess board
main_b.js v2/v3 source: bean characters + CAST, human, podium, stage (scene/card), props, ALL scenes (buildAll), render(t), init()
assemble.py main_v1.js[0:109] (BPM→150, END_BAR→93) + main_v1 robotDB…stage + main_b.js → main.js
frame_head.html HTML/SVG frame + base helpers (el, txt, tf, op, E, chars, charsPop, measure, bg …)
build.py frame_head.html + main.js → index.html
render.mjs events | stills | video (Playwright + Chrome Headless Shell, PNG → ffmpeg x264 crf 12 per worker)
music.py score + events.json → SFX + shouts → ducked mix → music.wav synth_lib.py instruments & SFX
voices/ 40 shout wavs + lines*.txt (name|voice|rate|pitch|text) make_voices.sh regenerate them (macOS)
finish.sh concat segments + loudnorm −14 LUFS + mux → ../game-show.mp4 make_srt.py → ../game-show.srt
fonts/ fonts.css Noto Sans SC, Fredoka, ZCOOL KuaiLe (woff2 subsets)
historical: main.js (generated), remix2026.js (draft of the remix section, superseded by main_b.js)
All commands from styles/game-show/demo/ (the scripts also chdir there themselves). PY=../../../.venv/bin/python.
- Plan on the grid. Fill
SECT(first bar of each level card) inmain_b.js, setEND_BARvia the replace inassemble.py, add the card bars toCUTS, and write a cue list: which beats get whichev(). - Shouts: add lines to
voices/lines3.txt, thensh make_voices.sh out/voices_new(macOSsay+ ffmpeg), listen, copy the keepers intovoices/, add the names toVOinmusic.py. - Build the page:
$PY assemble.py && $PY build.py. - Look at it:
STILLS_DIR=out/check node render.mjs stills 5.5 20.4 45 78.3 116.5 145(≈1 s for six frames). Or openindex.html?t=78.3in a browser. - Events:
node render.mjs events→events.json(demo: 474 events,dur 148.80). - Score + mix:
$PY music.py→music.wav(≈2 s; deterministic, seed 7).$PY music.py out/test.wavwrites elsewhere. - Render:
node render.mjs video 2→out/seg_0..1.mp4+out/list.txt. The demo used 6 workers (120 s total); on a shared machine keep 2 (≈3–4 min).SEGS=5 node render.mjs video 6re-renders only one of 6 equal segments. - Finish:
sh finish.sh out/test.mp4(concat → loudnorm → x264 slow crf 16 + AAC 256k 48 kHz). With no argument it writes../game-show.mp4and overwrites the library film (back it up first).AUDIO_FROM=old.mp4 sh finish.sh …skips the remix and copies the old film's audio track unchanged (used for the sign-off re-render). - Subtitles:
$PY make_srt.py→../game-show.srt. - Review:
ffmpeg -i out/test.mp4 -vf fps=1/4,scale=384:-1,tile=6x7 -frames:v 1 out/sheet.jpgplus stills around every slam.
Pitfalls we hit
render(t)must be a pure function oft. The remix thumbnails only updated whilet < t1 + 0.01, so their final pose depended on having rendered the frames before. The 6-worker render starts worker 5 cold at 124.0 s, and in the shipped v3 film the bottom thumbnails (robot arm stack, dancing robots, bar chart, Gemini card) show their initial state from 124.0 s to 131.2 s. Fixed inmain_b.jsduring import (the thumbnail is recomputed att1 − 0.001every frame), and the film's last segment (124.0–148.8 s) was re-rendered with the fix.main.jsandindex.htmlare generated. Editmain_v1.js(palette, beat helpers, banner/judge/burst, 1990s robots) ormain_b.js(characters v2, stage, all scenes, render/init), then runassemble.py→build.py. Edits made straight intomain.jsare overwritten.BPMandEND_BARare patched by string replace inassemble.py('const BPM = 140,'→ 150,'const END_BAR = 61;'→ 93). If you edit those lines inmain_v1.jsthe replace silently stops matching. Change the replace targets instead.- Add every new shout in three places: the
.wavinvoices/, a line invoices/lines*.txt(so it can be regenerated), and theVOlist at the top ofmusic.py(otherwiseKeyError). - Level card bars must also be listed in
CUTS(init()inmain_b.js: the flash) and in thejingle()loop +PLANspans ofmusic.py. They are three separate lists. synth_lib.pyallocates a fixedDUR = 150.0s buffer. A longer film needs a biggerDURor the tail is cut.- Fonts:
measure()uses a canvas, so glyph widths are wrong until the webfont has loaded.init()loads every family for the page text beforebuildAll()and again for the built stage text. Keep that when adding text. saypronunciation: write "A. I.", "G P T one", "Nine point eleven", "Move thirty seven" — spelled-out forms.- The frame template (
frame_head.html, inherited from an earlier film) still declares#paper/#fxcanvases and ink/boil filters;init()hides the canvases and the style doesn't use the filters. Leave them alone or delete them together. - zsh does not word-split
$TS; pass timestamps as an array (TS=(1 2 3); … $TS) or literally.
Engine reference
Contract. buildAll() calls scene(t0, t1, build) once per set. build(g, s) creates all SVG nodes inside group g once and returns update(t), which only sets attributes. render(t) shows the scenes with t0 ≤ t < t1, calls their update(t), then applies camera punch, cut flash and final fade. update(t) must depend on t only.
| Module | Function | What it does / key params |
|---|---|---|
| frame_head | el(tag, attrs, parent), txt(parent, str, attrs) |
create SVG nodes |
tf(e, x, y, s=1, r=0, sy) · op(e, o) · show(e, bool) |
transform / opacity / display | |
seg(t, a, b) · lerp · clamp · E.out/in/io/back/elastic · rng(seed) |
timing + easing, seeded random | |
chars(parent, str, {x, y, size, family, weight, fill, anchor, ls, stroke, sw}) + charsPop(c, t, t0, stagger, d, from) |
per-glyph pop-in title | |
measure(str, size, family, weight) · bg(parent, color) |
text width, full-frame rect | |
| main_v1 | at(bar, beat) · B · BARL · frac(t) |
tempo grid (150 BPM) |
ev(t, name, {f, i}) · punch(t, amount) |
register a sound event / a camera punch | |
hopY(t, h) · hitSq(t, t0, amt) · near(t, t0) |
beat hop, hit squash | |
gtext(parent, s, x, y, size, {family, weight, fill, stroke, sw, anchor}) |
outlined telop text | |
stripes(p, col, w, angle) · dots(p, col, step, r) · raysEl(p, n, col) · starPath(n, r1, r2) |
set patterns (rotate rays in update) |
|
burst(p, x, y, t0, col, size) · judge(p, word, x, y, t0, col, dur) |
return f(t); push into s.fx and call each frame |
|
banner(p, year, name, sub, col) + bannerAnim(b, t, t0, t1, x=56, y=44) |
year sticker | |
robotDB/robotAG/robotT(p) · quad(u, v) · pawn(p, col, king) |
era robots, perspective board | |
| main_b | bean(p, {color, belly, antenna, name, kind, eyeFill, acc, ant, tagCol}) |
a contestant; kind bot/llama/whale; acc noise/mask/beret/captain/mustache; ant q/bolt/moon/heart/cloud/sprout/brush |
pose(c, {hey 0..1, open, sq, lean, face 'x'/'up', blink, armsL, armsR}) · poseAny(c, o) |
pose any bean or twins | |
CAST.<key>(p, name?) · ALL |
16 ready contestants (gpt, claude, gemini, llama, deepseek, ernie, qwen, mistral, kimi, grok, bert, dalle, mj, sd, sora, baby) | |
human(p, shirt, glasses, hair) + humanFace(h, open, sweat) + setArm(arm, x1, y1, x2, y2) |
cartoon humans | |
podium(p, col, label) · taskIcon(p, kind) · smallBot(p, col) |
props | |
dropY(t, t0, y, h, d) · heyAt(times, t, w) · nearestIn(times, t) |
entrances, "Hey!" windows | |
scene(t0, t1, build) · card(bar0, num, name, years, col, col2, icon) |
a set, a 1-bar level card | |
| music.py | PLAN[bar] = mode, span(a, b, mode), jingle(at(bar)) |
bar-by-bar arrangement |
event loop if s == '<name>': addv(sound, t, gain, pan) |
map a picture event to a sound |
Minimal new level (inside buildAll() in main_b.js; add g8: 67 to SECT and shift later sections, add bar 67 to CUTS, jingle(at(67)) + a span(68, 75, 'funk') in music.py, and raise END_BAR):
{ const b0 = SECT.g8 + 1, T = (bar, bt = 0) => at(b0 + bar, bt);
card(SECT.g8, 'GAME 8', '开源接力', '2026', K.orange, '#FF9E5E',
(p) => { const w = el('g', { transform: 'scale(2)' }, p); pawn(w, K.white); });
scene(T(0), T(8), (g, s) => {
bg(g, '#FFB870'); stripes(g, '#FFA85A', 50, -20);
el('rect', { x: 0, y: 880, width: 1920, height: 200, fill: '#E08A4A' }, g);
const w = el('g', {}, g); const c = CAST.claude(w);
const calls = [T(0, 0), T(0, 1), T(1, 0), T(1, 1)], answers = [T(0, 2), T(0, 3), T(1, 2), T(1, 3)];
calls.forEach(t0 => { ev(t0, 'v:hey'); punch(t0, 0.015); });
answers.forEach(t0 => { ev(t0, 'pop', { f: 900 }); s.fx.push(burst(g, 960, 420, t0, K.yellow, 0.8)); });
const b1 = banner(g, '2026', '开源接力', '一拍一棒,接住就赢', K.orange);
s.fx.push(judge(g, 'Perfect!', 960, 300, T(7, 3), K.pink, 0.8));
ev(T(7, 3), 'slam'); punch(T(7, 3), 0.05);
return (t) => {
tf(w, 960, 880 + hopY(t, 20), 1);
poseAny(c, { hey: heyAt(answers, t) ? 1 : 0, open: heyAt(calls, t), sq: hitSq(t, nearestIn(answers, t) ?? -9) });
bannerAnim(b1, t, T(0), T(8));
s.fx.forEach(f => f(t));
};
});
}
Then assemble.py → build.py → stills → events → music.py → video → finish.sh.

