Make a {12}-second kinetic typography video where the words are the whole show.
BRIEF (fill in, or delete a line to let Claude decide)
- Text: "{your line, quote or lyric}" (allow 0.6–0.7s of video per word, so 12s fits about 18 words)
- Mood: {confident | tense | joyful | calm}
- Palette: background {#hex}, text {#hex}, one accent {#hex}
- Font: {e.g. Instrument Serif} as a .woff2 file in ./fonts, or download the open-licence file there
- Format: {1080x1920 vertical | 1920x1080 landscape}, 30fps. On vertical, a long hero word (8+ letters) forces all the type smaller; choose landscape or a shorter hero word if you want it big.
- Music: {path/to/track.mp3, N BPM, first beat at S seconds | none}
PLAN (write plan.md before any code; sizes are targets until you measure them in code, then update the plan)
- Split the text into phrases of 1–4 words. The hero is the most important word in the final phrase: it lands last and holds to the final frame.
- Mood sets the pace. Confident: quick snaps, firm landings. Tense: short, sharp moves. Joyful: lighter, with a slight overshoot. Calm: slower eases, longer holds.
- Give each phrase a treatment chosen for its meaning, inside the one travel direction: a fast word snaps in, a heavy word lands on a slower ease, a quiet word sits smaller in empty space.
- A table in whole frames (1/30s): phrase, word count, enter (first visible frame), settled, exit start, cut. Hold (settled to exit start) is at least 0.3s per word and never under 0.8s. If the text is short for the duration, lengthen the holds and the hero's final hold; never add words.
- With music, every phrase settles on a beat (beat k = S + k x 60/BPM). Without, each phrase enters on the frame the previous one is cut.
- One dominant travel direction for the whole piece. Break it once, for the hero: the phrase before it exits fully off frame in the hero's direction, the frame sits empty and still (one beat with music, 0.5s without), then the hero enters that way. This is the film's only pause.
TYPE
- Words only: no labels, counters, logos or captions beyond the text itself.
- The safe area is 5% in from every edge; on vertical, it ends 12% in from the right and 20% up from the bottom instead. Measure text by its ink bounds with the loaded font. Settled text stays inside it; motion may pass outside.
- Size the hero first: it fills 80–95% of the safe width. Then set every other phrase as large as it can be, capped at two-thirds of the hero's size (compare the font's cap height, even for lowercase words), so the hero leads by at least 1.5x. A quiet word may go smaller on purpose.
- Stack to stay big: break a multi-word phrase across lines, one or two words per line, rather than shrink it.
- Don't park every phrase in the same spot. Move the resting position between phrases so the frame keeps changing.
- Letters in a word share one easing curve. If they stagger, use at most one frame per letter and move each letter no further than the font's x-height, so a word never looks letter-spaced mid-motion.
- The accent color appears only on the hero word. No gradients, glows or drop shadows on text.
MOTION
- A cut is the single frame where the outgoing phrase vanishes, about halfway through its exit and still moving; on that same frame the next phrase appears a few frames into its entrance. No gap and no overlap, except the one pause before the hero. No crossfades.
- Phrases exit in the direction the next enters. At most 3 transition types; a type is a distinct way of entering or exiting (a snap, a drop, a letter rise), not a change of speed or ease.
- Entrances under 0.8s, exits about 75% of that. Ease-out in, ease-in out. No bounce or elastic.
- No idle floating, breathing or wobble between phrases.
BUILD
- First confirm ffmpeg and Playwright exist. Install Playwright if missing (it can drive an installed Chrome with channel "chrome" instead of downloading Chromium). If ffmpeg is missing, stop and tell me how to install it.
- If the project already uses Remotion or HyperFrames (remotion in package.json, or a hyperframes.json), use it and its own render command. Otherwise write one HTML file that sets window.DURATION and exposes async window.seek(t). GSAP is fine as one paused timeline that seek(t) drives.
- Every frame is a pure function of t. Banned: Date.now(), requestAnimationFrame loops, timers, CSS transitions and @keyframes, playing videos or GIFs, unseeded Math.random(). Add *,*::before,*::after{transition:none!important;animation:none!important;scroll-behavior:auto!important}.
CHECK, THEN RENDER
- Export a still at each phrase's settled frame, and at the frame before and the frame of every cut. Fix overlaps and awkward line breaks.
- HTML path: serve the folder over a local http server (Chrome blocks fonts on file:// URLs). Playwright viewport at the exact format, deviceScaleFactor 1. Before capturing, run await Promise.all([...document.fonts].map(f => f.load())), then await document.fonts.ready. Set img.loading = "eager" on every image, then await img.decode() on each, catching failures (a lazy image offscreen never loads, so decode() would wait forever). Capture frame i at t = i/30 for i = 0 to DURATION x 30 - 1.
- Save the frame where the hero has fully landed as poster.jpg, to use as the cover image. Don't bake it into frame 0: it would flash the payoff before the first word.
- In the commands below, DURATION means the real length in seconds: write the number, not the word.
- HTML path encode: ffmpeg -framerate 30 -i frames/%05d.png -c:v libx264 -pix_fmt yuv420p -crf 18 -movflags +faststart video.mp4
- Music (if none, rename video.mp4 to type.mp4): ffmpeg -i video.mp4 -i TRACK -map 0:v -map 1:a -c:v copy -c:a aac -b:a 192k -af "apad,atrim=0:DURATION,afade=t=out:st=DURATION-1:d=1" -movflags +faststart type.mp4
- Confirm with ffprobe that the video has exactly DURATION x 30 frames.
DELIVER: type.mp4, poster.jpg and plan.md.