A hand-crafted, paper-cut storybook cartoon for little kids, rendered entirely from HTML + SVG + GSAP using HyperFrames — no video editor, no timeline app. The whole ~4-minute episode is a set of deterministic, seek-safe compositions that render straight to MP4.
The story: Pip loves to bounce and Bean loves to build. When a grumpy new kid, Milo, shows up and stomps through the fun, Pip & Bean discover that grumpy on the outside is often just lonely on the inside — and that kindness is sunshine you can share. The weather literally follows the kindness: the world dims when Milo is unkind and blooms back to full sun when the three become friends.
- Five acts (
compositions/a1–a5), mounted into one root timeline inindex.html. - Original cast — Pip, Bean, and Milo are drawn as inline SVG and animated procedurally (blinks, talk-sync mouths, bounces, elastic overshoots).
- Emotion-driven art direction — a desaturated overlay for sad beats, a gray cloud that hides the sun, warm sunbursts for the payoff. Light canvas throughout; sadness is never "dark."
- Curved motion — characters ride the slide along a real GSAP MotionPath that hugs the chute (steep at the top, flattening into the run-out), not a straight line.
- Procedural stage kit (
lib/stage-kit.js) — a seeded (deterministic) scenery layer: the oak tree is generated geometry, grass tufts and wildflowers sprout along every ground line, and everything sways in an analytic, seek-safe wind. Generated props measure the real.groundelement and plant themselves on it — grounding is guaranteed by construction, never by hand-typed pixel offsets. - Single-sourced props — the slide, sandcastles, and sandboxes are kit-built too
(
data-kit="slide|castle|sandbox"+ adata-kit-sinkdepth): one canonical slide instead of three copies, castles with seeded sand speckles and wind-swayed pennants, sandboxes scattered with seeded pebbles and a starfish. - Playful transitions — circle-iris wipes, vertical pushes, and a blur crossfade for the sad wind-down.
- Full voiceover + music + SFX — narrator, Pip, Bean, and Milo, over soft CC0 music and cartoon SFX (assets resolved locally; see Assets below).
| Display type | Baloo 2 (800) — chunky storybook headlines & name tags |
| Voice type | Patrick Hand — speech bubbles & narrator caption band |
| Canvas | cream #FFF6E4, sky #C9E9F4, ink #4A3B2F |
| Accents | coral #FF7A45, sun #FFD34E, grass #A8D879 |
| Cast | Pip orange #FF9F4A · Bean green #7BC96F · Milo #A8A0B5 → #9B7BE0 |
| doc | what's in it |
|---|---|
| docs/ARCHITECTURE.md | script→TTS→timeline→render pipeline, acts, audio tracks |
| docs/STAGE-CONTRACT.md | the spatial rules the stage audit enforces, and how to waive one |
| docs/STAGE-KIT.md | the procedural stage kit API: data-kit props, bubble anchoring, wind |
| docs/EPISODE.md | story, cast, act-by-act beats, key timestamps |
| docs/WORKFLOW.md | dev loop, visual verification, render performance |
| docs/DECISIONS.md | engineering decision log — read before changing things |
| docs/YOUTUBE.md | publishing/scheduling episodes to the @pipandbean channel |
| docs/EPISODE-02-PLAN.md | brief for episode 2 and the multi-episode repo restructure |
| docs/EPISODE-03-PLAN.md | brief for episode 3, The Broken Pinwheel |
npm run dev # live preview server (keep running)
npm run check # the gate: framework checks + stage audit
npm run check:stage # stage audit alone — the spatial contract, 291 sampled moments
npm run render # render to MP4 (GPU-accelerated)
npm run publish # publish and get a shareable linknpm run render is tuned for a fast local render (--gpu --browser-gpu --workers 8).
For a maximum-quality pass use npm run render:quality.
Hand-placed cartoon staging drifts: a bubble stops pointing at its speaker, a prop
ends up hanging in mid-air, a line plays over a shot its speaker is not in. The
framework's own checks cannot see any of that, so this project ships
tools/stage-audit.mjs — it loads the same bundled HTML the
renderer screenshots, seeks the real timeline to every voice line and act seam, and
measures the frame against the rules in
docs/STAGE-CONTRACT.md. Deliberate exceptions are declared
in the markup with data-stage-allow-*.
One episode per directory under episodes/; everything reusable is shared at
the root.
episodes/01-big-slide-day/
index.html root timeline — mounts the five acts + audio tracks
compositions/a1..a5 the five acts (self-contained HTML compositions)
script.json per-line voiceover script (speaker + text)
timeline.json act/line timing map
voice_manifest.json measured MP3 durations
meta.json project id + title
hyperframes.json the HyperFrames project file (the CLI runs from here)
assets -> ../../assets symlink: the CLI needs them inside the project
lib -> ../../lib symlink: same reason
renders/ rendered MP4s (git-ignored)
lib/stage-kit.js seeded procedural scenery, bubble anchoring, wind — shared
tools/stage-audit.mjs geometric stage audit (npm run check:stage)
episode.py resolves --episode for the generators
gen_tts.py script.json -> assets/voices/<episode>/*.mp3
gen_index.py timeline.json -> the episode's index.html
inject_fonts.py inlines the fonts into each act
upload_youtube.py publishes/schedules to @pipandbean (channel-wide)
frame.md creative brief: concept, palette, type, audio plan
assets/ fonts, bgm, sfx, voices/<episode>/ (git-ignored)
Every command defaults to 01-big-slide-day; override with EP=<slug>:
EP=03-broken-pinwheel npm run check
python3 gen_index.py --episode 03-broken-pinwheelMedia (fonts, voices, background music, SFX) and rendered output are git-ignored to keep the repo lightweight — this repository holds the source of the composition. All music and sound effects are CC0 / public domain; character voices are generated TTS. Regenerate or re-resolve locally before rendering.
- HyperFrames — HTML-to-video rendering
- GSAP 3 + MotionPathPlugin — animation & curved motion
- Inline SVG for every character, prop, and background
♥ Pip & Bean — a kindness story for the very young.