references/asset-prompts.md
# AI-generated assets for the Papercut style
## Which parts to generate and which to build
| Element | Build in code (Remotion/SVG) | Generate with an image tool | Why |
|---|---|---|---|
| Characters (the critter and its variants) | ✅ | no | Need posable parts; must look identical in every frame |
| Caption tags, banner, labels, buttons, gauges | ✅ | no | Text must be exact and animatable |
| Real UI (terminal, comment box) | ✅ | no | Text accuracy |
| Backgrounds (sky, slate room, space, wall) | either | ✅ good fit | Rich texture, static, no text |
| Complex static props (rocket, crane, consoles, Earth, metal panel, easel) | possible | ✅ good fit | Faster than drawing; animate as whole pieces |
| Paper-grain texture tile | no | ✅ | One-time asset |
**Text-to-video models are the wrong tool for the finished video.** They can't hold exact on-screen text, word-synced captions, or the same character across 10 shots. Use them at most for an optional B-roll insert. The pipeline is: **AI stills for textured assets → Remotion for all motion, text and timing.**
## Tool
Use whatever image tool the machine has. It needs to support **transparent PNG output** for props and, ideally, **image-to-image edits** for style matching. The shape of the calls, whatever the tool:
```bash
<image-tool> generate "<prompt>" --size 1024x1536 --out public/assets/<name>.png # backgrounds
<image-tool> generate "<prompt>" --background transparent --out public/assets/<name>.png # props
<image-tool> edit "<prompt>" --image library/papercut/public/bg/backdrops/kitchen.jpg --out ... # style-match against a library backdrop
```
If no image tool is available, build every prop in code with the paper filter. The style holds up without generated images. Don't generate any text inside images; add all text in Remotion.
## Style-lock suffix (append to every prompt)
```
Digital cut-paper collage illustration. Flat solid colour shapes cut from matte construction paper,
visible fine paper fibre grain, slightly irregular scissor-cut edges with a thin lighter rim,
soft realistic drop shadows between stacked paper layers, no gradients on objects, no outlines,
no text, no letters, no logos. Muted retro 1960s space-race palette: dusty blue, khaki, slate teal,
sage green, cream, coral #C9573C accents. Calm, clean, charming, handmade.
```
## Background prompts (1024×1536, then upscale or cover-fit to 1080×1920)
- **Launch sky:** `Vertical background plate: dusty blue sky (#567E97 at top) fading to a bright pale-blue glow in the centre (#BAD7E9), mottled watercolour-paper texture, strong soft vignette at the corners, flat khaki concrete ground strip across the bottom quarter (#BFB087). Empty, no objects. + STYLE-LOCK`
- **Mission-control room:** `Vertical background plate: very dark slate-teal wall (#132124) with subtle mottled parchment texture and heavy vignette, slightly darker floor zone in the bottom third, empty room, no objects. + STYLE-LOCK`
- **Deep space:** `Vertical background plate: deep navy night sky (#081027) with scattered tiny white paper-punch stars of varied sizes, faint mottled texture. No planets. + STYLE-LOCK` (Better: build the stars in code so they can twinkle, and generate only the navy texture.)
- **Briefing room:** `Vertical background plate: warm khaki plaster wall (#8A7A60) with heavy grunge mottling and vignette, dark brown wooden floor band across the bottom 25% (#452A15). Empty. + STYLE-LOCK`
## Prop prompts (transparent background)
- **Rocket:** `Single retro space-race rocket, tall and slender, cream body with black checkerboard panels, three black fins, small round porthole window near the nose (empty, dark), grey nozzle, side view, centred, isolated on transparent background. + STYLE-LOCK`
- **Launch crane:** `Tall red lattice launch tower with X-bracing and one short horizontal service arm, side view, isolated. + STYLE-LOCK`
- **Smoke cluster:** `Cluster of overlapping grey paper circles forming a puffy exhaust cloud, darker grey inner shadows between circles, isolated. + STYLE-LOCK`
- **Mission console:** `Wide sage-green mission-control desk seen from the front, a row of small square indicator lights (green, amber, white) along the top edge, flat front panel with empty space for a label, isolated. + STYLE-LOCK`
- **Big screen:** `Wall-mounted monitor with thick dark bezel, dark teal grid screen showing flat green paper continents, empty space for an orbit line, isolated. + STYLE-LOCK`
- **Spacecraft panel:** `Brushed light-grey metal panel with rounded corners and a row of rivets along the edges, one round porthole with a dark glass centre and riveted ring, front view, isolated. + STYLE-LOCK`
- **Earth curve:** `Top arc of planet Earth filling the bottom of a vertical frame: saturated blue ocean, flat green landmass blobs, flat white elongated cloud shapes, thin pale atmosphere rim, isolated on transparent. + STYLE-LOCK`
- **Fuel gauge:** build in code (the needle must rotate). The dial face can be generated: `Round analog fuel gauge face, cream dial, black tick marks, a red arc on the left and a green arc on the right, no needle, no letters. + STYLE-LOCK`
- **Easel + clipboard:** `Wooden easel holding a large blank cream paper clipboard with a grey metal clip at the top, front view, isolated, clipboard blank. + STYLE-LOCK`
- **Paper grain tile:** `Seamless tileable texture of off-white matte construction paper, fine fibres and subtle speckles, flat even lighting, no shadows.` (use as a multiply overlay)
## Consistency checklist for generated assets
- Same light direction: light from the top left, so shadows fall down and slightly right.
- If the prompt produced perspective, regenerate. Every asset has to be flat orthographic.
- Re-colour generated props in code if a hue drifts (CSS `filter: hue-rotate()` or re-generate with the hex in the prompt).
- Generate at high quality, trim transparent padding, and store in `public/assets/` with kebab-case names.
references/frame-breakdown.md
# Worked example: a ~30 s explainer, frame by frame
**This is an example of the style, not a template to copy.** Use it to calibrate pacing and density: how often events land, how long scenes run, how a metaphor maps onto a topic. For a new video, invent a new world, cast and props with the rules in `style-guide.md`.
The example is an invented feature-unlock explainer (28.3 s, 30 fps). The product ("Codewright", a coding assistant), its models ("Atlas 2" does the work, "Sage 3" advises) and the command are fictional. The world is a space mission control room, which gives the metaphor natural props for the actors, the work, the failure, the approval and the cost. Events are listed at 0.2 s steps, so each timing is good to about ±0.2 s. The caption column shows the word page on screen at that moment.
## Voiceover script
> Once Atlas 2 is your main model, stop leaving Sage 3 doing nothing. Because Codewright just added a command called advisor, and it puts Sage 3 to work reviewing what Atlas builds. Basically, Atlas keeps writing the code while Sage reads your whole session. Then Sage steps in at the exact moments mistakes happen, like right before Atlas builds the wrong thing or calls broken code finished. And get this: it costs less than running Sage 3 the whole time, because Sage stays quiet unless it's needed. Which means you get Sage's judgment on every big decision without burning through your limit. Good thing I made one prompt that sets it all up for you. For the link, comment ADVISOR.
About 120 words in 28 s, which is **4.3 words/s**.
> **Role-framing check. Confirm who does the work before you script.** In this feature the main model (Atlas 2) does *all* the execution, and the advisor model (Sage 3) does none. The advisor is the slower, costlier model, so it only gets called in at decision points. It doesn't read or run continuously. The lines "stop leaving Sage 3 doing nothing" and "Sage reads the whole session" suggest it is always active, which would be wrong. In a real script, keep the advisor idle by default ("radio silence") and show it waking only when the main model hits a big call. The visuals of scenes 5, 7 and 8 (alarm to transmit, fuel race, GO/NO-GO) already fit that model. Scene 4 should show the tape as the main model's own log, with the advisor looking at it only when called.
---
## Scene 1: Hook / launch (0.0 → 3.7 s)
Banner: `SAGE 3 CAN WATCH ATLAS 2` · Location: launch pad, dusty-blue sky, then tilting down to mission control
| t (s) | Caption page | Visual event |
|---|---|---|
| 0.0 | `Once` | Rocket on pad with the `ATLAS 2` pilot portrait in the nose porthole and a red arrow pointing at it. Crane arm attached. Small flame, smoke clusters left and right. |
| 0.4 | `Once Atlas` | Flame grows (scaleY up) |
| 0.6 | `Once Atlas 2` | Crane arm retracts |
| 1.0 | `is` | Page clears; rocket starts rising, smoke puffs expand |
| 1.2 | `is your` | Rocket accelerating upward, flame elongates |
| 1.4 | `main` | Rocket at top third |
| 1.6 | `main model` | Rocket leaving the top of frame |
| 1.8-2.4 | `stop` → `stop leaving` → `stop leaving Sage` | **Camera tilts down** in one continuous world: pad and smoke scroll up and out, a `MISSION CONTROL` building rises in. The Sage character is asleep at the console (flat-line eyes, rising `Z z z`), and an orbit sine-wave plays on the big screen. |
| 2.6-2.8 | `3` | Pink alarm beacon cone sweeps across the room from the `OFF COURSE` light |
| 3.0 | `3 doing` | One eye pops open (white bubble) |
| 3.2 | `nothing` | Shocked face: big eyes, O mouth, beacon sweeping |
| 3.4-3.6 | `nothing` | Eyes droop back toward sleep (the comedic beat) |
## Scene 2: The unlock (3.7 → 5.6 s)
Banner: `ONE COMMAND PUTS SAGE ON DUTY` · Location: mission control, dark slate
| t | Caption | Visual |
|---|---|---|
| 3.8 | `Because` | **Hard cut.** Terminal window as a paper prop in front of the world-map screen. User character (blue cap) at keyboard, arm extended to the keys. |
| 4.0 | `Because Codewright` | Input shows `/a` |
| 4.2 | `added` | `/advi`: each keystroke flashes a white spark on the keyboard |
| 4.4 | `added a` | `/advis` |
| 4.6 | `added a command` | `/advisor ` with caret |
| 4.8 | `called` | `/advisor sa` |
| 5.0 | `called advisor` | `/advisor sage` complete |
| 5.2 | `called advisor` | Green check line `✓ Advisor set to Sage 3` appears with a **sparkle burst** |
| 5.4 | `called advisor` | Second sparkle burst on the `FLIGHT` sign at the right |
## Scene 3: Roles (5.6 → 8.2 s)
Banner: `SAGE ADVISES, ATLAS FLIES`
| t | Caption | Visual |
|---|---|---|
| 5.6 | `and it` | **Hard cut.** Sage asleep behind the console, blank black monitor above, dim `GO` light, amber `FLIGHT` tag |
| 5.8 | `and it puts` | Purple headset drops onto Sage from above |
| 6.0 | `Sage` | Headset on, eyes open, `SAGE 3` pill appears above head |
| 6.4 | `Sage 3` | Monitor powers on: `LIVE · ORBIT` feed showing the Atlas astronaut beside the `LAB` module over the Earth curve |
| 6.6-7.0 | `to` → `to work` → `to work reviewing` | Slow camera push-in (the console grows about 8%) |
| 7.4-7.8 | `what` → `what Atlas` → `what Atlas builds` | `GO` light switches from dim to **lit green** |
## Scene 4: Mechanism 1 (8.2 → 11.2 s)
Banner: `IT SEES EVERY TOOL CALL` · **Split screen**: top 40% is space, bottom is mission control
| t | Caption | Visual |
|---|---|---|
| 8.2 | `Basically` | **Hard cut.** Top: Atlas astronaut presses `RUN` on the `LAB` module; a code monitor above shows `fix(auth): check token`. Bottom: **telemetry tape** (tool-call list with green dots) scrolls up out of a `TELEMETRY` box; Sage with headset watches. Amber radio arcs from the tape to Sage. |
| 8.8-9.2 | `Atlas` → `Atlas keeps` → `Atlas keeps writing` | Code lines type into the top monitor one by one. The tape keeps scrolling: `edit src/auth.ts`, `run npm test`, `└ 2 tests failed` (red), `└ 14 tests passed` (green) |
| 9.4-9.6 | `the` → `the code` | Code monitor scrolls as new lines arrive |
| 10.0-10.2 | `Sage` → `Sage reads` | Excess tape piles into a coil at the left of the box |
| 10.6 | `whole` | **Red hand-drawn ellipse** draws on around `2 tests failed` |
| 10.8-11.0 | `whole session` | Sage's arm extends to point at the circled line |
## Scene 5: Mechanism 2 (11.2 → 13.8 s)
Banner: `CALLED IN AT THE BIG MOMENTS`
| t | Caption | Visual |
|---|---|---|
| 11.2 | `Then` | **Hard cut.** `MASTER ALARM` panel lit red. Sage at a desk mic with a `TRANSMIT` button; the monitor shows Atlas with a hammer beside the `LAB`. |
| 11.4 | `Then` | Alarm blinks to dim (about 4 Hz on/off through the scene) |
| 11.8 | `Sage steps` | `TRANSMIT` lights; Sage's arm reaches toward the mic |
| 12.0 | `Sage steps in` | **Radio waves** pulse from the mic to the monitor |
| 12.2-12.4 | `at the` → `at the exact` | Waves reach the Atlas feed |
| 12.6-13.0 | `moment` → `moment mistakes` | **Strong camera push-in**: the desk rises and scales about 15%, and the LAB on the monitor gets a sparkle (fixed) |
| 13.4 | `moment mistakes happen` | Alarm dims |
## Scene 6: Concrete examples (13.8 → 16.8 s)
Banner: `A SECOND LOOK BEFORE 'DONE'` · Location: orbit, brushed-metal panel over the Earth curve
| t | Caption | Visual |
|---|---|---|
| 13.8 | `like` | **Hard cut.** Atlas (helmet) pushes a wooden crate toward the porthole; seismograph screen and `MISSION COMPLETE` lever on the panel |
| 14.0-14.8 | `like right before` → `Atlas builds the` | Crate at the porthole; a small amber radio signal hits Atlas; Atlas looks worried (O mouth) |
| 15.0-15.2 | `wrong` → `wrong thing` | Atlas presses a green button; the crate is ejected, tumbling |
| 15.4 | `wrong thing or` | Porthole ring turns **green**; crate flies off the top right |
| 15.6-15.8 | `calls` → `calls broken` | Atlas slides down to the seismograph; sparkles on the screen; arm reaches for the lever |
| 16.0 | `calls broken code` | Spark and smoke puff from the seismograph (it's broken) |
| 16.2-16.4 | `finished` | Red `NO-GO` tag pops above the lever |
| 16.6 | `finished` | Tape patch slapped over the screen; the tag flips to green `GO` |
## Scene 7: Cost objection (16.8 → 21.8 s)
Banner: `SAGE ONLY RUNS WHEN CALLED` · Location: launch pad (callback to the hook)
| t | Caption | Visual |
|---|---|---|
| 16.8 | `And` | **Hard cut.** Two rockets, `SAGE 3` (left) and `ATLAS 2` (right), each with a `FUEL LEFT` gauge. Labels: red `SAGE FLIES IT`, green `ATLAS FLIES, SAGE ADVISES`. Small Sage controller at a `RADIO SILENCE` console, bottom right. |
| 17.0-17.2 | `And get` → `And get this` | Both rockets ignite; smoke puffs spawn |
| 17.4-18.0 | `it` → `it costs` → `it costs less` | Left rocket climbs **faster**; its gauge needle drops quickly |
| 18.2-18.6 | `than` → `than running` → `than running Sage` | Left rocket high; left needle nearing E; right rocket steady, needle barely moves |
| 18.8-19.2 | `3` | Left gauge at E; `NO-GO` tag pops under it |
| 19.4-19.6 | `whole` → `whole time` | Left engine **coughs a white puff**; flame dies |
| 19.8-20.4 | `because` → `because Sage stays` | Left rocket tilts and sinks back down; right rocket rising steadily |
| 20.6-21.2 | `quiet` → `quiet unless it's` | `RADIO SILENCE` sign lit amber |
| 21.4 | *(no caption)* | Sage controller transmits: radio arcs toward the right rocket |
| 21.6 | `needed` | Right rocket keeps climbing |
## Scene 8: Benefit (21.8 → 25.1 s)
Banner: `BIG CALLS GET A GO / NO-GO`
| t | Caption | Visual |
|---|---|---|
| 21.8 | `Which` | **Hard cut.** `GO / NO-GO` board with 3 rows (`SWAP THE DATABASE?`, `DELETE THE OLD API?`, `LAUNCH TONIGHT?`), all dim. `YOUR LIMIT` gauge at right. Crew of three (red, green, purple caps) at consoles, Sage with headset at right edge. |
| 22.0-22.8 | `Which means` → `get Sage's judgment` | `FLIGHT` tag lights amber |
| 23.2 | `on every` | Row 1 lights `GO` with sparkles; a `GO` card flies to the red-cap crew member |
| 23.4 | `on every big` | Row 2 flips to red `NO-GO` with sparkles; card flies to green-cap |
| 23.6 | `decision` | Row 3 `GO` (flower-shaped sparkle); card to purple-cap |
| 24.0-24.2 | `decision without` → `decision without burning` | Small flame flickers on the `YOUR LIMIT` gauge |
| 24.4-24.8 | `through your limit` | Flame replaced by a green check; camera drifts right |
| 25.0 | `through your limit` | Check scales up (celebrate) |
## Scene 9: Offer (25.1 → 27.0 s)
Banner: `ONE PROMPT DOES THE SETUP` · Location: briefing room, khaki wall, brown floor
| t | Caption | Visual |
|---|---|---|
| 25.2 | `Good thing` | **Hard cut.** `PRE-FLIGHT CHECKLIST` on an easel with 5 unchecked lines (`"model": "atlas"`, `"advisorModel": "sage"`, `version 2.1+`, `CODEWRIGHT.md: when to ask`, `new session, then go`). A locked `PROMPT` card with a gold `?` padlock sits over it. `PROMPT` machine at right with a slot and indicator dots; dim `ALL SYSTEMS GO` sign. User (blue cap) bottom left. |
| 25.4-25.8 | `made` → `made one` → `made one prompt` | The prompt card flies in an arc into the machine's slot |
| 26.0-26.2 | `that` → `that sets` | Checkboxes start ticking top to bottom, about 1 per 0.2 s |
| 26.4-26.6 | `all` → `all up for` | 3, then 4 checked |
| 26.8 | `you` | All 5 checked; `ALL SYSTEMS GO` lights with sparkles; user smiles |
## Scene 10: CTA (27.0 → 28.3 s)
Banner: `COMMENT ADVISOR`
| t | Caption | Visual |
|---|---|---|
| 27.0 | `for` | **Hard cut.** Dark screen panel, user character below at a row of consoles. A link chip pops in; a social comment box `Add a comment… Post` |
| 27.2 | `for the` | Locked `PROMPT` card pops in at top right (the reward) |
| 27.4 | `comment` | (no new event) |
| 27.6 | `comment` | Comment box types `AD`… |
| 27.8 | `comment ADVISOR` | …`ADVISO` |
| 28.0 | `comment ADVISOR` | `ADVISOR` complete; **Post** button bursts with sparkles |
---
## Pacing stats
- 10 scenes in 28.3 s, **2.8 s average**. Shortest are 1.3 s (CTA) and 1.9 s (unlock), and the longest is 5.0 s (the cost race, the one argument that needs a mini-story).
- Caption pages hold 1-3 words (one 4-word page), and a new page starts about every **0.6 s**.
- Each scene has **2-4 discrete visual events** (a state change, a prop entrance, a character action) on top of continuous ambient motion. A visual event lands about every 0.4-0.8 s, tied to a stressed word in the voiceover.
references/motion-principles.md
# The 12 principles of animation, in cut paper
Each principle with what it means for paper and the kit component or helper that does it. Helpers: `keyed`, `swing`, `onN` from `'../lib/paper/motion'`; `bob`, `hop`, `reach`, `PopIn`, `SlideIn`, `Boil`, `Blink`, `SparkleBurst`, `CameraPush` from `'../lib/paper/kit'`; camera moves from `'../lib/paper/camera'`. Timings in frames at 30 fps; the full timing table is in `style-guide.md`.
Paper does not deform. A prop is a rigid cut-out: it scales, rotates, slides and hinges, but it never bends or changes volume. Where a principle asks for a squash, use overshoot instead.
| # | Principle | In papercut | How |
|---|---|---|---|
| 1 | Squash and stretch | Overshoot, not squash. A pop-in grows 0 → 1.08 → 1 and settles; a limb stretches along one axis only (scaleX, 10% overshoot). A hit gets a 3-frame shake. | `PopIn` (spring, damping 12, stiffness 180), `SnapZoom` (1 → 1.25 in 4 frames, then settle), `reach` for limbs |
| 2 | Anticipation | Move back 3-4 frames before the main move: a hop starts with a dip, a stamp lifts before it lands, the hook's cut starts with a tiny pull back. | a negative first key in `keyed`, `PullBack`, the hook-to-scene-2 rule in `papercut-hook` |
| 3 | Staging | One focal prop per beat. Frame 0 is a full, readable set. Keep y < 420 (banner) and y 1420-1580 (captions) free of key props and faces. The camera points at the thing the voiceover names. | `OrbitCamera` + `Depth`, `RackFocus`, `FocusPulse`, `FocusHunt`, `Backdrop` floor line |
| 4 | Straight ahead and pose to pose | Key poses sit on stressed words (pose to pose); ambient motion runs straight ahead underneath. | `cueOf(ID)(word, n)` + `keyed` for poses; `bob`, `Handheld`, `Blink`, `Boil` for ambient |
| 5 | Follow-through and overlapping action | Things keep moving after the body stops, and parts start at different times. A hung sign swings after impact; the critter's arm trails its body by 2 frames. | `swing(f, at, amp)` (pendulum, 2 cycles then still), `DutchTilt` judder, offset arm and body phases |
| 6 | Slow in and slow out | Everything eases. Default ease-out cubic; slides use (0.16, 1, 0.3, 1) with 4-8% overshoot; the camera push is ease-in-out. A linear move reads as a glitch. | `keyed` (ease per segment), `SlideIn`, `CameraPush` |
| 7 | Arcs | Natural motion follows curves. Hops are a sine arc; thrown props and pointing arms follow a curve, not a straight line. | `hop(f, at, dur, h)`, `OrbitCamera` yaw arc, `CrabTrack`, a cubic path for thrown props |
| 8 | Secondary action | Small motion that supports the main one without stealing it: an LED blinking, a sparkle on a number, a radio wave, idle bob. Always on, always quieter than the event. | `Blink`, `SparkleBurst`, `RadioWaves`, `bob` |
| 9 | Timing | One visual event per stressed word, every 0.4-0.8 s. Pop-ins 6-9 frames, slides 8-12, cuts 6-12. Stepped "on twos" for the hand-made feel; hold a beat after a gag lands. | `cueOf`, `onN(f, 2)`, `Boil`, the transition frames in `CUT_DEFAULTS` |
| 10 | Exaggeration | Push the one beat that matters: a number lands with a snap zoom, a realisation gets a dolly zoom, a gauge overshoots 3-5°. Exaggerate one beat per scene, and keep it readable in under 1 s. | `SnapZoom`, `DollyZoom`, `DutchTilt`, `TimeRamp`, `FreezeFrame` |
| 11 | Solid drawing | Volume from stacked cut pieces, not outlines: a darker underside strip, a lighter top strip, light from the top-left everywhere, one consistent drop shadow, grain and a rough edge. Depth from layers. | `Paper`, `Slab` (`shade`, `hi`), `Depth` z layers, `PaperDefs` |
| 12 | Appeal | Simple geometry, eyes only, stubby limbs, a readable silhouette. Cast differs by colour, accessory and name pill, not by new anatomy. Expressions are a small fixed set. | `Critter` (`expr`, `acc`, `tag`), accessories (`cap`, `headset`, `monocle`…) |
## Using this list
- **Storyboard**: for each scene block, name the anticipation (principle 2) and the one exaggerated beat (10) in the event table.
- **Build**: when a move looks wrong, find its row. Stiff and robotic is usually 6 (no easing) or 5 (nothing trails). Muddy is usually 3 (two focal props) or 11 (shade strips missing).
- **QA**: see the principles pass in `papercut-qa`.
references/remotion-recipes.md
# Remotion recipes for the Papercut style
These are starting-point components, **not yet compiled against a project**. If you have the library, use its compiled kit (`library/papercut/kit/`, listed in `library/papercut/CATALOG.md`) instead and treat these as sketches of how the pieces work. Before writing real code, load `remotion:remotion-best-practices` → `remotion-markup` and adapt them to its current conventions (`Interactive.Div`, inline `interpolate()` in `style`, `scale`/`translate`/`rotate` properties instead of `transform`). All motion must be driven by `useCurrentFrame()`, because CSS animations and transitions do not render.
Imports (`remotion`, `../tokens`) are left out of most snippets for brevity.
## Project layout
```
src/
Root.tsx # registers the main video + each scene as a connected composition
PapercutVideo.tsx # <Series> of scenes (hard cuts), <Audio> VO + music, <WordTagCaptions>, <TapeBanner>, <GrainOverlay>
tokens.ts # palette, fonts, timing constants (from style-guide.md sections 4, 5 and 9)
paper/
PaperDefs.tsx # one <svg><defs> with the grain / rough-edge filters, mounted once
Paper.tsx # wrapper: grain + rough edge + drop shadow on any child
PopIn.tsx # spring pop with rotation settle
TapeBanner.tsx
WordTagCaptions.tsx
SparkleBurst.tsx
RadioWaves.tsx
Typewriter.tsx
CameraPush.tsx
GrainOverlay.tsx
Blink.tsx
cast/
Critter.tsx # the mascot, with accessory + expression props
scenes/
S01Hook.tsx ... S10Cta.tsx
public/
vo/voiceover.wav # from the user's TTS (e.g. Qwen3-TTS)
vo/captions.json # Caption[] with word timings (from @remotion/captions transcription)
music/bed.mp3
sfx/*.mp3
textures/paper-grain.png # optional scanned texture
assets/*.png # AI-generated props with transparent backgrounds
```
Composition: **1080×1920, 30 fps**. Get the duration from the voiceover (`calculateMetadata`). Each scene's `durationInFrames` comes from the caption timestamps of its first and last words.
## tokens.ts
```ts
export const C = {
sky: '#567E97', skyGlow: '#BAD7E9', ground: '#BFB087',
slate: '#132124', slateLow: '#121617', space: '#081027',
wall: '#8A7A60', floor: '#452A15',
cream: '#DACEC0', tape: '#C3B284', console: '#8A9A86', consoleShade: '#546054',
panel: '#B1B9BC', smoke: '#726F68', board: '#2E3235', wood: '#7F5231',
hero: '#D67354', heroLo: '#A9553B', you: '#D67354', youLo: '#A9553B', // the critter's body colour
tag: '#C9573C', tagText: '#F3E6D3', // caption tags
purple: '#4E3881', ink: '#1C1B17', navy: '#293042',
go: '#2DA34D', goDim: '#165329', nogo: '#D63A2B', nogoDim: '#4D140D',
amber: '#F3A53B', violet: '#8154B9', capBlue: '#3968AD',
flameCore: '#FBF9D0', flameOuter: '#F68B29',
sparkYellow: '#F5C542', sparkPink: '#F4A7B9',
};
// Defaults only. Any font from the families in style-guide.md section 6 works; pick per video.
export const FONT = {
banner: 'Archivo Black', // heavy caps sans (or Poppins Black, Montserrat Black, Unbounded)
caption: 'Fraunces', // soft bold serif (or Recoleta, Cooper Black, Young Serif)
label: 'Inter', // 800 for pills/signage
mono: 'JetBrains Mono',
};
export const T = { pop: 8, wordPop: 4, slide: 10, press: 6, blinkHz: 4 };
```
Load fonts through `@remotion/google-fonts` (see the `google-fonts.md` reference in the Remotion skill).
## PaperDefs + Paper wrapper (the core of the look)
```tsx
// PaperDefs.tsx: mount once at the root, zero-size
export const PaperDefs = () => (
<svg width={0} height={0} style={{position: 'absolute'}}>
<defs>
{/* grain multiplied into the fill, clipped to the shape */}
<filter id="paper" x="-5%" y="-5%" width="110%" height="110%">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves={4} seed={3} result="noise" />
<feColorMatrix in="noise" type="saturate" values="0" result="grey" />
<feComponentTransfer in="grey" result="soft">
<feFuncA type="linear" slope={0.12} />
</feComponentTransfer>
<feComposite in="soft" in2="SourceAlpha" operator="in" result="grainClip" />
{/* rough, scissor-cut edge */}
<feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves={2} seed={7} result="edgeNoise" />
<feDisplacementMap in="SourceGraphic" in2="edgeNoise" scale={2.5} result="rough" />
<feBlend in="grainClip" in2="rough" mode="multiply" />
</filter>
{/* torn edge for banner + caption tags: stronger displacement */}
<filter id="torn">
<feTurbulence type="fractalNoise" baseFrequency="0.12" numOctaves={3} seed={11} result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale={6} />
</filter>
</defs>
</svg>
);
// Paper.tsx: wrap any prop to make it paper
export const Paper: React.FC<{children: React.ReactNode; lift?: number; torn?: boolean; style?: React.CSSProperties}> =
({children, lift = 4, torn, style}) => (
<div style={{
filter: `url(#${torn ? 'torn' : 'paper'}) drop-shadow(0 ${lift}px ${lift * 2}px rgba(0,0,0,0.35))`,
...style,
}}>
{children}
</div>
);
```
Add the lighter cut-edge rim with `box-shadow: inset 1px 1px 0 rgba(255,255,255,0.18)` on rectangular parts. For SVG shapes, use a 1.5 px lighter stroke offset up and to the left.
**Performance:** SVG filters on many layers slow rendering. If a preview frame takes over ~300 ms, bake static paper props into PNGs (render a `<Still>` or use AI-generated assets) and keep live filters only on animated parts.
## PopIn
```tsx
import {spring, useCurrentFrame, useVideoConfig, random} from 'remotion';
export const PopIn: React.FC<{at: number; seed: string; children: React.ReactNode}> = ({at, seed, children}) => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const s = spring({frame: frame - at, fps, config: {damping: 12, stiffness: 180, mass: 0.6}});
const tilt = (random(seed) - 0.5) * 8; // ±4° start tilt
return (
<div style={{
scale: String(s), // overshoots ~1.08 then settles
rotate: `${tilt * (1 - s)}deg`,
opacity: frame < at ? 0 : 1,
}}>{children}</div>
);
};
```
## WordTagCaptions
Get word timings with the `remotion-captions` skill (`transcribe-captions.md`). Then page them 1–3 words at a time, breaking on punctuation or a gap > 250 ms:
```tsx
import type {Caption} from '@remotion/captions';
import {random, spring, useCurrentFrame, useVideoConfig} from 'remotion';
import {C, FONT} from '../tokens';
type Page = {words: Caption[]; startMs: number; endMs: number};
export const pageWords = (caps: Caption[], max = 3): Page[] => {
const pages: Page[] = [];
let cur: Caption[] = [];
caps.forEach((w, i) => {
cur.push(w);
const next = caps[i + 1];
const punct = /[.,!?:;]$/.test(w.text.trim());
const gap = next ? next.startMs - w.endMs > 250 : true;
if (cur.length >= max || punct || gap) {
pages.push({words: cur, startMs: cur[0].startMs, endMs: next ? next.startMs : w.endMs});
cur = [];
}
});
return pages;
};
export const WordTagCaptions: React.FC<{captions: Caption[]; bandY: number /* 0.68–0.80 */}> = ({captions, bandY}) => {
const frame = useCurrentFrame();
const {fps, height} = useVideoConfig();
const ms = (frame / fps) * 1000;
const page = pageWords(captions).find(p => ms >= p.startMs && ms < p.endMs);
if (!page) return null;
return (
<div style={{position: 'absolute', top: height * bandY, width: '100%', display: 'flex', justifyContent: 'center', gap: 10}}>
{page.words.filter(w => ms >= w.startMs).map((w, i) => {
const s = spring({frame: frame - Math.round((w.startMs / 1000) * fps), fps, config: {damping: 14, stiffness: 260}});
const key = `${w.startMs}-${w.text}`;
return (
<div key={key} style={{
background: C.tag, color: C.tagText, fontFamily: FONT.caption, fontWeight: 800,
fontSize: 64, lineHeight: 1, padding: '10px 16px 14px',
rotate: `${(random(key) - 0.5) * 10}deg`,
translate: `0px ${(random(key + 'y') - 0.5) * 12 + (1 - s) * -10}px`,
scale: String(0.6 + 0.4 * s),
filter: 'url(#torn) drop-shadow(0 4px 6px rgba(0,0,0,0.35))',
}}>{w.text.trim()}</div>
);
})}
</div>
);
};
```
## TapeBanner
```tsx
export const TapeBanner: React.FC<{text: string}> = ({text}) => (
<div style={{position: 'absolute', top: '14%', left: '6%', width: '88%', rotate: '-1deg'}}>
<div style={{
background: C.cream, padding: '28px 36px', textAlign: 'center',
fontFamily: FONT.banner, fontSize: 60, color: C.ink, letterSpacing: '-0.01em', textTransform: 'uppercase',
filter: 'url(#torn) drop-shadow(0 5px 8px rgba(0,0,0,0.35))',
}}>{text}</div>
{[{left: -28, rotate: '-35deg'}, {right: -28, rotate: '35deg'}].map((p, i) => (
<div key={i} style={{position: 'absolute', top: -18, width: 110, height: 44, background: C.tape, opacity: 0.8, ...p}} />
))}
</div>
);
```
Auto-shrink long banner text with `fitText()` from `@remotion/layout-utils` (see `measuring-text.md`).
## SparkleBurst
```tsx
export const SparkleBurst: React.FC<{at: number; x: number; y: number; seed: string}> = ({at, x, y, seed}) => {
const f = useCurrentFrame() - at;
if (f < 0 || f > 12) return null;
const cols = [C.sparkYellow, C.sparkPink, '#FFFFFF'];
return <>{Array.from({length: 6}).map((_, i) => {
const a = (i / 6) * Math.PI * 2 + random(seed + i);
const d = 30 + 30 * (f / 12);
const sc = Math.sin((f / 12) * Math.PI); // 0 → 1 → 0
return <svg key={i} width={28} height={28} viewBox="-10 -10 20 20" style={{
position: 'absolute', left: x + Math.cos(a) * d, top: y + Math.sin(a) * d,
scale: String(sc), rotate: `${f * 15}deg`,
}}><path d="M0-10 L2-2 L10 0 L2 2 L0 10 L-2 2 L-10 0 L-2-2Z" fill={cols[i % 3]} /></svg>;
})}</>;
};
```
## Boil (stop-motion paper jitter, measured on a finished reel)
Wrap the banner and its labels. They hold for 2 frames, then jump to a new random offset. Don't wrap world props: the world layer only moves through `CameraPush`.
```tsx
export const Boil: React.FC<{amp?: number; rot?: number; seed?: string; children: React.ReactNode}> =
({amp = 1.5, rot = 0.2, seed = 'boil', children}) => {
const step = Math.floor(useCurrentFrame() / 2); // re-roll every 2 frames (15 Hz)
return (
<div style={{
translate: `${(random(`${seed}x${step}`) - 0.5) * 2 * amp}px ${(random(`${seed}y${step}`) - 0.5) * 2 * amp}px`,
rotate: `${(random(`${seed}r${step}`) - 0.5) * 2 * rot}deg`,
}}>{children}</div>
);
};
// Usage: <Boil><TapeBanner text="..." /></Boil>. amp 1.5 px at 1080 w ≈ ±1 px at 720 w.
```
Caption tags get a gentler, smooth float instead: `translate: 0 ${Math.sin((frame + seedOffset) / 9) * 1.2}px`.
## RadioWaves, Blink, CameraPush, Typewriter, GrainOverlay
```tsx
// RadioWaves: 3 amber arcs pulsing from (x,y) toward angle `dir`
export const RadioWaves: React.FC<{x: number; y: number; dir: number}> = ({x, y, dir}) => {
const f = useCurrentFrame();
return <svg style={{position: 'absolute', left: x - 60, top: y - 60, rotate: `${dir}deg`}} width={120} height={120} viewBox="-60 -60 120 120">
{[0, 1, 2].map(i => { const p = ((f + i * 4) % 15) / 15; const r = 12 + p * 40;
return <path key={i} d={`M ${r * Math.cos(-0.7)} ${r * Math.sin(-0.7)} A ${r} ${r} 0 0 1 ${r * Math.cos(0.7)} ${r * Math.sin(0.7)}`}
stroke={C.amber} strokeWidth={6} strokeLinecap="round" fill="none" opacity={1 - p} />; })}
</svg>;
};
// Blink: hard on/off at ~4 Hz; pass the lit/dim children
export const Blink: React.FC<{on: React.ReactNode; off: React.ReactNode; hz?: number}> = ({on, off, hz = 4}) => {
const {fps} = useVideoConfig();
return <>{Math.floor((useCurrentFrame() / fps) * hz * 2) % 2 === 0 ? on : off}</>;
};
// CameraPush: slow scale toward a focal point, whole scene
export const CameraPush: React.FC<{to?: number; origin?: string; children: React.ReactNode}> = ({to = 1.08, origin = '50% 45%', children}) => {
const {durationInFrames} = useVideoConfig();
const f = useCurrentFrame();
return <AbsoluteFill style={{transformOrigin: origin, scale: String(interpolate(f, [0, durationInFrames], [1, to], {easing: Easing.inOut(Easing.quad)}))}}>{children}</AbsoluteFill>;
};
// Typewriter: ~2 frames per char, block caret
export const Typewriter: React.FC<{text: string; at: number; fpc?: number}> = ({text, at, fpc = 2}) => {
const f = useCurrentFrame() - at;
const n = Math.max(0, Math.min(text.length, Math.floor(f / fpc)));
return <span style={{fontFamily: FONT.mono}}>{text.slice(0, n)}<span style={{opacity: Math.floor(f / 8) % 2 ? 0 : 1}}>▌</span></span>;
};
// GrainOverlay: full-frame animated grain, 3% opacity
export const GrainOverlay = () => (
<AbsoluteFill style={{pointerEvents: 'none', mixBlendMode: 'overlay', opacity: 0.35}}>
<svg width="100%" height="100%"><filter id="g"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves={2} seed={useCurrentFrame() % 8} /></filter>
<rect width="100%" height="100%" filter="url(#g)" opacity={0.1} /></svg>
</AbsoluteFill>
);
```
## Critter (the mascot)
Build it from rectangles so every part can be posed:
```tsx
type Expr = 'idle' | 'sleep' | 'shock' | 'happy' | 'worried';
type Acc = 'none' | 'capBlue' | 'capRed' | 'capGreen' | 'capPurple' | 'headset' | 'helmet';
export const Critter: React.FC<{size?: number; expr?: Expr; acc?: Acc; armReach?: number /* 0–1 */; tag?: string; tagColor?: string}> =
({size = 220, expr = 'idle', acc = 'none', armReach = 0, tag, tagColor = C.purple}) => {
const f = useCurrentFrame();
const bob = Math.sin((f / 36) * Math.PI * 2) * 3;
const u = size / 10; // grid unit
// body 10u × 6u in C.hero (a C.heroLo strip for the underside), eyes 1u × 1.6u at (3u,2u) and (6u,2u), arms 2u × 1.4u from the sides (right arm extends by armReach × 6u),
// legs: four 1u × 2.4u rectangles under the body at 1.5u spacing
// sleep → eyes 1.2u × 0.25u lines; shock → eyes scale 1.4 + 1u circle mouth; happy → 2u × 0.3u smile
// accessories are separate absolutely positioned paper parts above the body
// tag pill follows with a 2-frame lag: compute bob at (f - 2)
return null; // implement per the spec above; wrap every part in <Paper>
};
```
(The body is left as a spec rather than full JSX because it's mostly positioning boilerplate. Build it once, preview it with a `<Still>`, then reuse it across every video.)
## Scene assembly (hard cuts)
```tsx
import {Series} from 'remotion';
<Series>
<Series.Sequence name="S01 Hook" durationInFrames={111}><S01Hook /></Series.Sequence>
<Series.Sequence name="S02 Unlock" durationInFrames={57}><S02Unlock /></Series.Sequence>
{/* ... */}
</Series>
```
Put the banner, captions, grain and audio **outside** the `<Series>`, as siblings in the root `AbsoluteFill`, so they run on the global timeline. Pass the banner text from a `scenes[]` array keyed by start frame.
references/style-guide.md
# Papercut style guide
**This is a style system, not a recreation spec.** It describes how to make *any* topic look and move like layered construction paper brought to life. The worked example in `frame-breakdown.md` is an invented explainer set in a space mission control room. Its colours, fonts and props are **defaults**. Swap them freely as long as the rules in section 2 hold.
Numbers marked *(measured)* come from frame-by-frame analysis of a finished reel in this style (5 fps contact sheets, pixel-sampled colours, motion tracking at 30 fps). Everything else is observation or recommendation.
---
## 1. What the style is called
| Layer | Names | Search terms for more references |
|---|---|---|
| Visual look | **Digital cut-paper collage**, papercraft, construction-paper illustration, textured flat | `cut paper motion graphics`, `papercraft explainer`, `paper cutout animation` |
| Animation | **Digital cut-out animation**: rigid flat parts moved by transforms, not redrawn (the classic cut-out tradition) | `cut-out animation`, `rigged 2D`, `puppet animation` |
| Storytelling | **Extended-metaphor explainer**: an abstract idea acted out in a physical world | `visual metaphor explainer`, `analogy explainer` |
| Captions | **Word-pop captions** (karaoke / word-by-word kinetic captions), here on torn paper tags | `word by word captions`, `kinetic captions` |
Working name: **Papercut Explainer**. Format: faceless vertical short (Reels/TikTok/Shorts), voiceover-led.
---
## 2. Style DNA: the non-negotiables
Change the topic, palette, fonts or world, but keep these six rules:
1. **Everything is paper.** Flat fill, paper grain, a slightly irregular cut edge, and a soft drop shadow for every object. No gradients or outlines on objects.
2. **Shading comes from stacked cut pieces, not blends.** A darker strip for the underside and a lighter strip for a highlight, each its own paper layer.
3. **Two-voice typography.** A heavy caps sans on a taped paper banner for the beat's takeaway, and a soft bold serif on torn tags for the spoken words.
4. **Every set is a paper world.** Either one extended metaphor world (the mission-control example in `frame-breakdown.md`) or, more often, a **new set per banner** whose room and props illustrate the VO's words (the Echolark and Turbo Lane examples in `../../papercut-script/references/storytelling.md`). Real UI is always placed *inside* the set as a rough paper rebuild on a monitor or laptop.
5. **Never fully still.** Motion runs on ones with a slow camera drift, and the paper overlays "boil" on twos (section 8).
6. **Saturation is earned.** Backgrounds are muted and dark or dusty. Bright colour only goes on characters, captions and state signals.
---
## 3. Drawing in papercut (for any object or character)
### Shape language
- **Build from primitives:** rectangles, rounded rectangles (corner radius 2–6 px at 1080 w), circles, ellipses, trapezoids, triangles. Get the silhouette right first; if it reads as a flat shape at thumbnail size, it works.
- **Orthographic only.** Draw everything front-on or side-on, with no perspective and no 3/4 views. Depth comes from overlap, shadow and scale, never from vanishing points.
- **2–3 tones per object:** base, shade (about 12–18% darker, same hue), and an optional accent. The shade is a separate cut shape (underside band, inner rim, cast strip), not a gradient.
- **No outlines.** Separate objects with value contrast and drop shadow. Black only appears as a *fill* (screens, eyes, tyres, checkerboard panels).
- **Details are separate stacked pieces.** A porthole is a dark circle, then a grey ring, then small rivet dots. A button is a dark bezel plus a coloured cap. A screen is a black bezel, a coloured display, and paper text on top.
- **Controlled imperfection.** Edges wobble very slightly (noise displacement 1.5–3 px), shapes are nearly but not perfectly symmetric, and small tape or patch pieces are allowed. It should look handmade, not sloppy.
- **Chunky, legible scale.** Nothing meaningful smaller than about 12 px at 1080 w. Props are bold and simplified, closer to a children's book than a technical diagram.
- **Light from the top-left** for every object in every scene, so shadows fall down and slightly right.
### Material cheats (still paper, printed differently)
| Material | How to fake it on paper |
|---|---|
| Metal | Light grey paper plus a very fine horizontal streak texture, with rivet dots along the edges |
| Wood | Brown paper plus a faint grain texture and one darker plank-gap line |
| Glass/screen | Near-black paper plus one diagonal lighter sliver as a reflection |
| Fire | 2–3 nested trapezoids or teardrops (cream core → yellow → orange), flickering |
| Smoke/clouds | Clusters of overlapping circles in 2 greys (or whites), with darker circles tucked behind |
| Water/planet | Flat blue base, flat irregular blobs for land, flat elongated white cloud strips |
| Light beams | Semi-transparent flat wedge (50–60% opacity), no blur |
| Stars/sparkles | Four-point star shapes or 1–3 px dots |
### Characters
- **One base creature for the whole cast.** Simple geometry, a boxy or blobby body, **eyes only** (small solid shapes), and stubby rectangular limbs. The kit's default is a boxy orange critter (token `C.hero` `#D67354`, shade `C.heroLo` `#A9553B`, with `you` as an alias for the viewer's stand-in). Use whatever mascot fits the series, but keep it at this level of simplicity.
- **Characters differ only by accessory and name pill:** caps in different colours, headset, helmet, apron, hard hat, tie. The name pill is a small rounded label above or across the head.
- **Expressions are part swaps:** sleep (eye lines plus rising Z's), alert (eyes scale ×1.4 plus a small O mouth), happy (short smile line), worried (eyes shifted, one limb raised).
- **Acting:** limbs stretch to point or press (rectangle scaleX with overshoot), the body leans ±6°, hops 8–15 px, peeks up from behind furniture, and bobs ±2–3 px while idle. Walking is a slide plus a bob.
### Recipes for common props
- **Monitor/screen:** dark bezel (rounded rect) → display colour → paper content → reflection sliver. Add a thin top "title bar" for app windows.
- **Control desk:** front face (base tone) + a darker top lip + a row of 6–10 small square LEDs (green/amber/white) + a black label pill.
- **Gauge:** cream dial, black ticks, red arc on the low end, green arc on the high end, black needle with a round hub. The needle is its own layer so it can rotate.
- **Button/light:** a dim state (a dark shade of the colour) and a lit state (full saturation), swapped instantly.
- **Board/list:** dark rounded panel, rows as slightly lighter strips, a status pill at the right end of each row.
- **Checklist:** cream paper on a clipboard or easel; square boxes; ticks drawn on.
- **Paper document/card:** cream rectangle, a few grey bar "text lines", optional corner fold.
---
## 4. Canvas and layout
- **9:16, 1080×1920, 30 fps.** (Platform re-encodes often land at 720×1280. Always render at 1080×1920.)
| Zone | Y (% of height) | Contents |
|---|---|---|
| Top margin | 0–13% | Background only |
| **Header banner** | 14–21% | Taped paper strip, same position all video |
| **Stage** | 22–72% | Props, characters, embedded UI |
| **Caption band** | 68–80% | Word tags; the band shifts per scene to stay clear of faces and focal props |
| Platform safe area | 80–100% | Empty ground, floor or dark wall |
- Banner spans about 6–94% width, rotated about −1°.
- Let stage props **bleed off the side edges**, so the frame feels like a window into a bigger world.
- **Depth stack (back → front):** background wash → far props → mid props → characters → foreground lip (desk/console edge) → UI labels → captions → banner → grain.
---
## 5. Colour system
Build a palette by **role**. The values below are the default set.
| Role | Rule | Default |
|---|---|---|
| World backgrounds (1 per location) | Desaturated, dark or dusty, mottled and vignetted | sky `#567E97` → glow `#BAD7E9`, ground `#BFB087`, room slate `#132124`, space navy `#081027`, wall khaki `#8A7A60`, floor `#452A15` |
| Neutral paper | Warm off-white, used for documents and banners | cream `#DACEC0`, tape `#C3B284` |
| Prop mid-tones | Muted, one hue family per material | console sage `#8A9A86`/`#546054`, metal `#B1B9BC`, smoke `#726F68`, dark panel `#2E3235`, wood `#7F5231` |
| **Accents** | Saturated colours for the main character and the caption tags | hero critter orange `#D67354` (shade `#A9553B`), caption tag coral `#C9573C`, tag text cream `#F3E6D3` |
| Identity accents | Name pills, accessory colours | purple `#4E3881`, cap blue `#3968AD`, violet `#8154B9` |
| **State signals** | Fully saturated, each with a dim twin | go `#2DA34D`/`#165329`, no-go `#D63A2B`/`#4D140D`, amber `#F3A53B` |
| Ink | Near-black for text and labels | `#1C1B17`, navy sign `#293042` |
| Effects | Sparkles, fire | spark yellow `#F5C542`, pink `#F4A7B9`, flame `#FBF9D0` → `#F68B29` |
**Re-theming:** keep the roles and swap the values. Pick the accents first, choose backgrounds that are their muted complements, and keep signal colours standard (green/red/amber) so states read instantly.
---
## 6. Typography (families, not exact fonts)
Any font with these characteristics works; the listed ones are free options.
| Role | Characteristics | Good options |
|---|---|---|
| **Header banner** | Heavy geometric/grotesk sans, ALL CAPS, tight tracking, near-black | Archivo Black, Poppins ExtraBold/Black, Montserrat Black, Inter Black, Unbounded |
| **Caption tags** | Soft, rounded, chunky bold serif with a friendly handmade feel | Fraunces (Black, SOFT axis up), Recoleta, Cooper Black, Young Serif, DM Serif Display |
| **Labels/signage** | Bold sans caps inside pills | Inter 800, Archivo 800, Manrope 800 |
| **Embedded UI** | Whatever the real product uses | Inter for app chrome, JetBrains Mono / IBM Plex Mono for code |
Sizes at 1080 w: banner 56–64 px, one line with auto-shrink; captions 58–68 px; labels 26–40 px; UI 24–34 px.
**Banner copy:** 3–7 words, caps, stating the *takeaway* of the beat rather than repeating the voiceover. Test: a sound-off viewer reading only the banners should get the whole argument.
---
## 7. Material and texture recipe (apply to every object)
1. Flat fill.
2. **Paper grain:** fine fibre noise multiplied at 8–15% (SVG `feTurbulence` fractalNoise, baseFrequency ≈ 0.8–1.2, 3–4 octaves, clipped to the shape), or a tiled scanned-paper PNG with `multiply`.
3. **Cut edge:** a 1–2 px lighter rim on the top and left edges, plus low-frequency edge displacement (1.5–3 px).
4. **Drop shadow:** 0 px x, 3–6 px y, 6–10 px blur, black 25–40%. Foreground layers get the larger values. This is what sells the depth, so never skip it.
5. **Torn edge:** only on the banner and caption tags. Stronger displacement plus a faint white fibre fringe.
6. **Background wash:** mottled watercolour/parchment texture with a radial vignette (centre +10–15%, corners −25–35%). An optional bloom behind the hero prop.
7. **Global grain:** 2–4% animated film grain over the whole frame.
8. **Masking tape:** tan translucent (70–85%) strips with torn short ends, rotated about ±35° over the banner corners.
---
## 8. Camera and jitter *(measured)*
The handmade feel comes from **two separate motion layers**. Don't mix them up.
### A. World camera: smooth, continuous, never shaky
- Every scene has a slow **push-in and/or drift** toward the focal prop. The measured rate is about 0.6–1% scale per second, with the frame drifting 0.3–0.5 px per frame (720p) in x/y toward the subject.
- Measured jitter on world props is **~0.05 px RMS**: effectively none. The world layer moves on smooth easing curves only.
- Bigger moves are deliberate: a strong push (the desk grows about 15% in 1 s) on a dramatic line, and one **world-continuous tilt/pan** between two locations in the hook (about 0.6–0.8 s, ease-in-out).
### B. Paper boil: the stop-motion "hand-placed" jitter
- Screen-space paper overlays (the **header banner**, and likely the pills and labels on it) hold their position for **2 frames, then jump**. Offsets re-roll every 2 frames (15 Hz): about ±0.7–1.3 px at 720p ≈ **±1–2 px at 1080p**, RMS ≈ 0.4–0.5 px (720p). The values are random, with no drift.
- **Caption tags** float slightly once they've landed (±1 px at 720p), smoothly rather than stepped.
- Implementation: `const step = Math.floor(frame / 2); x = (random('bx' + step) - 0.5) * 3; y = (random('by' + step) - 0.5) * 3;` Optionally add `rotate = (random('br' + step) - 0.5) * 0.4deg`.
- Keep boil **subtle**. It should be felt rather than seen, and the text must stay easy to read.
### C. Everything else runs on ones
All 60 frames of every 2 s window I measured are distinct *(measured)*. Only the boil is stepped.
---
## 9. Motion language
All motion is transforms on rigid parts (position, scale, rotation, opacity). The only exceptions are draw-on strokes and typing.
### Timing constants (30 fps)
| Motion | Frames | Easing |
|---|---|---|
| Prop pop-in | 6–9 | Spring (damping ≈ 12, stiffness ≈ 180): scale 0 → 1.08 → 1, rotation ±4° → 0 |
| Caption word pop | 3–5 | Scale 0.6 → 1.05 → 1, y −8 → 0 px, no fade |
| Slide-in | 8–12 | Ease-out (0.16, 1, 0.3, 1), 4–8% overshoot |
| Limb stretch/press | 5–7 out, 5 back | 10% overshoot |
| State change (dim ↔ lit) | 1–2 | Hard swap plus sparkle burst |
| Blink (alarm/LED) | ~4 Hz | Hard on/off |
| Camera push | whole scene | Scale 1.00 → 1.06–1.12, ease-in-out |
| Idle bob | 30–40 period | Sine ±2–3 px |
### Signature effects (reusable in any world)
- **Sparkle burst:** 4–6 four-point stars (yellow/pink/white) fly out 30–60 px, scale 0 → 1 → 0 and spin over ~10 frames. Use for any confirmation or success.
- **Signal waves:** 2–3 concentric amber arcs pulse from a source toward a target, staggered 3 frames and fading as they travel. Use for any communication.
- **Alarm:** a blinking red panel plus a rotating translucent light wedge. Use for any error or urgency.
- **Typing:** about 2 frames per character, a block caret, and a spark on the keyboard per keystroke. Use for any command or input.
- **Scrolling tape/list:** content moves up at about 40 px/s, curls under a paper roller, and piles into a coil. Use for any stream of logs or data.
- **Draw-on annotation:** a hand-drawn red ellipse (stroke-dashoffset, ~8 frames) around the key item, while a character points at it.
- **Gauge needle:** ease-out with 3–5° overshoot and a small settle; step it down for "burning".
- **Stamp/card fly-out:** a card pops from a board, arcs to a character's hand, and lands rotated ±8°.
- **Engine/fire:** flames flicker (scaleY 0.9–1.1 at ~12 Hz noise), smoke circles expand and drift; failure is a white cough puff and a sink.
- **Ambient loops:** twinkling stars, rising Z's, blinking LEDs, waveforms on screens.
### Event density
One **discrete visual event per stressed word**, roughly every 0.4–0.8 s: a prop entering, a state flipping, a character acting, or a camera accent. Ambient motion runs continuously underneath.
---
## 10. Scene transitions
- **Hard cut on the banner change** is the default. Measured with scene detection on finished reels: 9 hard cuts in 28 s, 17 in 33 s and 11 in 30 s.
- **One world-continuous camera move** (a tilt or pan across a tall or wide canvas that holds two locations) in the hook is optional.
- **Slide-push / whip-pan into the CTA set:** the next set slides in from the side, or the frame smears horizontally with motion blur. About 6–8 frames.
- The banner stays in place and its text swaps on the cut.
- Sets last about 1.8–3 s in product tours and news videos, and up to 5 s for a metaphor beat that needs a mini-story.
---
## 11. Captions: word-pop torn tags
- One paper tag per word in the caption accent (coral `C.tag` by default), cream serif text, torn edges, drop shadow.
- Each tag gets a random rotation of −5° to +5° and a y-offset of ±6 px (seeded, so the same tag always gets the same values).
- 6–10 px gap between tags. Pages hold **1–3 words** (rarely 4) and break at phrase boundaries or punctuation.
- Each word appears exactly on its spoken onset; the page clears when the next page starts.
- The CTA keyword goes in CAPS in the same font, with no colour change.
- Pace: about **3.5–4 words/s** of voiceover, so 75–95 words per 22–30 s.
---
## 12. Narrative template (metaphor-world variant)
> The full storytelling system (spoken spine, banner fact track, set-per-banner staging, URL → rough UI, three modes, and the storytelling prompt) is in **`../../papercut-script/references/storytelling.md`**. This section covers the single-metaphor-world variant used by the mission-control example in `frame-breakdown.md`.
| # | Beat | Share of runtime | Purpose | Example (`frame-breakdown.md`) |
|---|---|---|---|---|
| 1 | Hook | ~13% | Absurd or funny visual plus a claim; uses the continuous camera move | Rocket launches, the advisor is asleep at mission control |
| 2 | Unlock | ~7% | The one action or command that changes things, shown in the real UI | `/advisor sage` typed in a terminal |
| 3–5 | Mechanism ×2–3 | ~30% | How it works, one metaphor prop per point | Telemetry tape; alarm → transmit; wrong cargo caught |
| 6 | Objection | ~17% | Cost or risk, told as a mini-story (usually the longest scene) | Two-rocket fuel race |
| 7 | Benefit | ~12% | The payoff, shown with state signals | GO/NO-GO board, limit gauge ✓ |
| 8 | Offer | ~7% | Free resource | Checklist plus a locked prompt card |
| 9 | CTA | ~5% | Comment keyword typed into a comment box, then Post | `COMMENT ADVISOR` |
**Choosing a metaphor world:** it needs a natural prop for the *actors*, the *work*, the *failure*, the *approval* and the *cost*. Worlds that fit: space mission, restaurant kitchen, railway yard, construction site, heist crew, hospital ward, post office, sports team, film set, garden. Call back to the hook's location in the objection or benefit beat.
---
## 13. Sound
- **Voiceover:** any TTS works. The recommended local option is **Qwen3-TTS** (open weights, `Qwen/Qwen3-TTS-12Hz-*` on Hugging Face; runs on Apple Silicon MPS or CUDA, usually through a small Gradio app). Use the user's existing install if there is one, and ask where it lives.
- **TTS (CustomVoice)**, 0.6B or 1.7B: preset speakers Aiden, Dylan, Eric, Ryan, Serena, Vivian, Sohee, Ono_anna, Uncle_fu, plus an optional style instruction. For this style, try 1.7B with Ryan or Aiden and an instruction like *"friendly, natural, conversational, like telling a friend; clear pauses between sentences; stress the key word of each line"*. Delivery rules (breathing room, emphasis, tone): `../../papercut-voice/SKILL.md`.
- **Voice Design** (1.7B): describe a voice in words.
- **Voice Clone** (Base 0.6B/1.7B): clone from a reference clip plus its transcript.
- Export the WAV to the video project's `public/vo/` and transcribe it for word timings (Remotion captions skill).
- **Measured on a finished reel:** the audio ran continuously with no gaps over 0.25 s at −35 dB, so voiceover lines were butted tight over a music bed.
- **Recommended:** a light playful music bed at −20 to −24 LUFS, plus paper and UI foley (pop on entrances, key clicks, a GO chime, a NO-GO buzzer, radio squelch, a whoosh on camera moves, a rumble on launches).
references/style-prompt.md
# Portable Papercut style prompts
Copy-paste blocks for using this style outside this skill: another coding agent, p5/Canvas, After Effects notes, image or video generators. Each block stands alone.
**To write the story** (script + banner fact track + set-per-banner staging + rough UI spec) from a link, use the storytelling prompt in `../../papercut-script/references/storytelling.md` section 7. Pair it with block A below for the visuals.
---
## A. Master style prompt (for any coding or motion agent)
```
Make this in the "Papercut Explainer" style: digital cut-paper collage with cut-out animation.
LOOK
- Every object is flat construction paper: one solid fill, fine paper-fibre grain multiplied at ~10%,
a slightly irregular scissor-cut edge with a 1–2px lighter rim on the top-left, and a soft drop shadow
(0 x, 3–6px y, 6–10px blur, 30% black). No gradients or outlines on objects.
- Shading = stacked cut pieces (a darker underside strip, a lighter highlight strip), never blends.
- Build everything from simple primitives (rects, rounded rects, circles, trapezoids), orthographic
front/side views only, chunky and legible at phone size, light from the top-left.
- Backgrounds: muted dark or dusty colour with a mottled watercolour-paper texture and a strong vignette.
Saturated colour only on characters, captions and state signals (green GO / red NO-GO / amber).
- Characters: one simple geometric mascot (boxy orange body, eyes only, stubby rectangular limbs) reused
for the whole cast, told apart only by accessories (caps, headset, helmet) and a small name pill.
- Light 2–4% animated film grain over the whole frame.
LAYOUT (9:16, 1080×1920, 30fps)
- Taped paper header banner at 14–21% height: cream torn strip, two translucent tan tape pieces at the
corners, heavy ALL-CAPS geometric sans, 3–7 words stating the takeaway of the current beat.
- Stage 22–72%. Word captions 68–80%. Keep the bottom 20% empty for platform UI.
- Props may bleed off the side edges.
CAPTIONS
- Voiceover shown word by word: each word on its own torn paper tag in the caption accent colour (coral by default),
cream soft bold serif text, random ±5° tilt, ±6px y offset, drop shadow. Each word pops in on its
spoken onset (scale 0.6→1.05→1 over 4 frames). Pages of 1–3 words, cleared at phrase breaks.
MOTION
- Rigid cut-out animation: only position/scale/rotation/opacity on paper parts. Everything on ones.
- Springy pop-ins (6–9 frames, overshoot ~8%, rotation settles from ±4°); slide-ins with ease-out;
limbs stretch to point or press; idle bob ±2–3px; blinking LEDs; twinkling stars.
- Camera: a slow smooth push-in/drift toward the focal prop in every scene (~1% scale per second),
with no random shake. One continuous tilt/pan between two locations in the opening scene.
Hard cuts on sentence boundaries otherwise; scenes 1.5–5s.
- Paper boil: the banner and its labels re-offset randomly every 2 frames by ±1–2px (stop-motion
hand-placed jitter). Subtle enough to feel, not to read.
- One visual event per stressed word (a prop enters, a light flips dim→lit with a sparkle burst,
a character acts, a gauge moves, a red ellipse draws around the key item).
- Signature effects: four-point-star sparkle bursts, amber signal arcs between characters/devices,
blinking alarm with a rotating light wedge, typewriter text with key sparks, scrolling data tape.
STORY
- Explain the topic through ONE physical metaphor world (e.g. mission control, a kitchen, a rail yard).
Real product UI appears inside that world as a paper prop.
- Beats: hook → the unlock → mechanism ×2–3 → objection (cost/risk mini-story) → benefit → free
resource → "comment KEYWORD" CTA typed into a comment box.
```
---
## B. Image-generation style lock (append to any prop or background prompt)
```
Digital cut-paper collage illustration. Flat solid colour shapes cut from matte construction paper,
visible fine paper-fibre grain, slightly irregular scissor-cut edges with a thin lighter rim, soft
realistic drop shadows between stacked paper layers, shading made from separate darker paper pieces,
no gradients, no outlines, orthographic front view, simple geometric forms, light from the top-left.
No text, no letters, no logos. Muted palette with one warm accent.
```
Add `isolated on a transparent background` for props, and the colour hexes of the current palette.
---
## C. Text-to-video models (for B-roll only)
These can't hold exact text or a consistent cast, so use them only for textless inserts:
```
Stop-motion cut-paper animation, flat construction-paper shapes with visible grain and soft drop
shadows, rigid paper pieces sliding and popping into place, slow smooth camera push-in, subtle
frame-to-frame paper jitter, muted palette with a warm orange accent, orthographic flat view,
no text. [SUBJECT AND ACTION]
```
---
## D. One-line descriptor (for briefs, notes, search)
> Faceless vertical explainer in digital cut-paper collage: grainy construction-paper props with soft drop shadows, rigid cut-out animation with springy pops, a slow camera push, stop-motion paper boil, a taped-banner headline per beat, and word-by-word captions on torn coral paper tags, all inside one extended visual metaphor.
SKILL.md
---
name: papercut-explainer
description: Make a papercut reel, video or explainer end to end. Short vertical (9:16) cut-paper explainers from a link (repo, product page, announcement), idea to mp4. Also routes partial asks to the stage skills and hands out the style prompt for other tools.
---
# Papercut Explainer
A ~20-45 s vertical explainer that looks like layered construction paper brought to life: grainy paper props with drop shadows, rigid cut-out animation, a slow camera push with stop-motion paper boil, a taped headline banner per beat and word-by-word torn-paper caption tags. Built with Remotion, optional AI-generated textured assets, and any TTS.
Every beat runs three tracks:
- the voiceover tells the story
- the taped banner adds a hard fact the voiceover skips
- a fresh paper set illustrates the voiceover's words, with a rough papercut rebuild of the product UI doing the demo
Reuse the style and the storytelling, not the worked examples' content. Every product, company and number in the examples is invented. The style rules are in `references/style-guide.md` (section 2 is the Style DNA).
## Preflight
- **Library.** Resolve `$PAPERCUT_LIB`, else the `PAPERCUT_LIB=` line in `${XDG_CONFIG_HOME:-~/.config}/papercut-explainer/config.env`, else ask once. Never hard-code a machine path. Stages 1 to 4 need no library.
- **Install mode.** Probe it before promising a stage (see Install modes below).
- **Series.** Find the series folder (it holds `episodes/`). Read its `SERIES.md` if it exists.
## Run modes (ask first)
1. **Full reel**, idea to mp4: stages 1 to 8, then stage 9 after posting.
2. **Script only**: strategy, hook, script (stop after the staging table).
3. **Storyboard from an existing script**: `papercut-storyboard`. Ask for the script, the facts and the HOOK block.
4. **Style prompt for another tool**: give the right block from `references/style-prompt.md` and stop.
5. **Fix or rebuild one scene**: `papercut-build` (scenes step), then `papercut-qa`.
## Intake (ask only what is missing)
- **Source**: usually a link (repo, product site, announcement). Everything else is researched from it.
- **The one claim** the video proves, and the topic.
- **Role framing**: who or what does the work, who advises or approves. Confirm it with the user and never invent product behaviour.
- **CTA keyword** and what the viewer gets (the deliverable).
- **Length**: default 22-30 s.
- **Audience level** (from `SERIES.md`) and any accent colour or mascot.
Write the answers as the BRIEF block at the top of `episodes/NN-slug/research/facts.md` (format in `papercut-strategy`).
## Stages
| # | Stage | Skill | Reads | Writes |
|---|---|---|---|---|
| 1 | Strategy | `papercut-strategy` | `SERIES.md`, the source | BRIEF in `research/facts.md`, `SERIES.md` |
| 2 | Hook | `papercut-hook` | BRIEF | HOOK block at the top of `papercut/STORYBOARD.md` |
| 3 | Script | `papercut-script` | BRIEF, HOOK | `research/facts.md`, `narration/script.json`, staging table in `STORYBOARD.md` |
| 4 | Storyboard | `papercut-storyboard` | `script.json`, `STORYBOARD.md` | Camera/Cut table, scene blocks, SFX plan in `STORYBOARD.md` |
| 5 | Scaffold and plan | `papercut-build` | `STORYBOARD.md` | the project, `episode.json` |
| 6 | Voice and timing | `papercut-voice` | `script.json`, `voice/voice.json` | `narration/` takes, `public/vo/captions.json`, `src/plan.json` |
| 7 | Scenes, sound, render | `papercut-build` | `plan.json`, `STORYBOARD.md` | `src/scenes/*`, `tools/cues.json`, `out/*.mp4` |
| 8 | QA | `papercut-qa` | stills, `STORYBOARD.md` | fixes, the delivery summary |
| 9 | After posting | `papercut-strategy` (autopsy), `papercut-build` (harvest) | platform numbers, the project | `research/analytics/`, library promotions |
## Project files
The pipeline lives in files, not in chat. Each stage reads the files above and writes its own, so a new session can resume from any stage.
```
<series>/
SERIES.md optional: audience, voice, palette, mascot, keyword log
voice/voice.json optional: voice settings (copy library/voice/voice.example.json)
papercut-assets/ optional: overlay textures and props
research/analytics/ optional: competitor studies and autopsies
episodes/NN-slug/
research/facts.md BRIEF + facts with sources
narration/script.json [{"id": "l1_hook", "text": "..."}, ...]
papercut/
episode.json scenes, banners, hook, gaps, bed, cuts
STORYBOARD.md HOOK, staging table, Camera/Cut table, scene blocks
```
Library catalogs: `library/papercut/CATALOG.md` (every kit component) and `library/audio/CATALOG.md` (every SFX and bed). They are generated, so a fresh checkout may lack them or have stale ones. Run `python3 $PAPERCUT_LIB/tools/catalog.py` to rebuild. Agents read the catalogs, not the kit source.
Reuse before writing: a kit component, a props set or a scene template beats new drawing code. Pick SFX and beds by name.
## Install modes
- **Minimal** (skills only): strategy, hook, script, storyboard, style prompts. No rendering.
- **Standard**: Minimal + build and render with a placeholder voice (silent, or macOS `say`) via `library/tools/placeholder_vo.py`, stills QA, SFX and bed audio, the full mp4.
- **Full**: Standard + local Qwen3-TTS voiceover and word-timed captions from real audio.
Probe (read-only):
```
. "$(grep '^PAPERCUT_LIB=' "${XDG_CONFIG_HOME:-$HOME/.config}/papercut-explainer/config.env" | cut -d= -f2-)/env.sh" # fills PAPERCUT_PY, QWEN_TTS_PY
node -v; ffmpeg -version | head -1
"$PAPERCUT_PY" -c 'import numpy, PIL, soundfile'
"$QWEN_TTS_PY" -c 'import qwen_tts'
```
The first three lines passing means Standard. The `qwen_tts` import passing as well means Full. If a stage needs more than the install provides, say so in one line, run `./install.sh --doctor` from the repo root, and hand over to `papercut-setup` instead of improvising. Find the root with `readlink` on the installed skill folder (under `.claude/skills`, `.agents/skills`, `.config/opencode/skills` or another harness's skills folder), or `PAPERCUT_HOME` in the config file.
## Rules across stages
- Strategy notes win over style notes: 22-30 s, and no news-framed hooks.
- Parallelise independent work (scene groups, backdrops and props, re-renders) with agents in one go.
- Render only when asked. Preview in the Remotion Studio.
## Shared references
- `references/style-guide.md`: Style DNA, drawing in papercut, palette, fonts, texture, camera and jitter, motion timings, captions, sound.
- `references/style-prompt.md`: portable prompts for other agents and generators.
- `references/motion-principles.md`: the 12 principles of animation mapped to kit components and helpers, with a use-in-each-stage note.
- `references/remotion-recipes.md`: Remotion component sketches, for when there is no library.
- `references/asset-prompts.md`: what to generate versus build in code, with prompt templates.
- `references/frame-breakdown.md`: a worked ~28 s example at 0.2 s resolution. Copy its event density, not its content.
## Hand-off
Start at stage 1 for a full reel, or at the stage the run mode names. After intake, run `papercut-strategy`; it reads `SERIES.md` and the BRIEF and hands to `papercut-hook`. Each stage skill names the next one.