motion-design: the workshop (Remotion, theme, sounds, music)
Everything is local and deterministic: same code = same MP4. No image or video generated by AI.
The stack
| Piece | Tool | Where |
|---|---|---|
| Animation | Remotion 4.0.529 (React / TSX), versions pinned in package.json | src/; npx remotion add <package> for one more module |
| Theme | colors, fonts, sizes, margins | src/illustration/theme.ts (read by the C and F constants of src/illustration/scenes/kit.tsx), fonts in public/fonts/ |
| Remotion modules | transitions, effects, gsap, three, noise, paths, shapes, motion-blur, light-leaks, starburst, rough-notation, captions, layout-utils, lottie | package.json |
| Graphics compositions | BrandedVideo (full-screen scenes), LowerThirdTrack (lower thirds, section titles), Screencast (site demos), Scene (scene catalog), ControleTexte (text check), FondAnime (looping animated background; its grain is made by scripts/make-grain.py) | src/illustration/, declared by Compositions.tsx, wired in src/Root.tsx |
| Shared cues | key times in seconds, read by the animation AND by the sound | src/<project>/cues.json |
| Music | a score written in Python on a numpy synth (scripts/synth.py: FM electric piano, pad, bass, percussion, vinyl crackle, reverb) | music/<project>.py, model: music/example_pad.py |
| Sound effects | synthesized palette: read reference/sound-palette.md before any sound. Synth recipes scripts/make-sfx.py -> public/sfx/*.wav + palette.json; gesture vocabulary scripts/son.py (a plan names a gesture, never a file; lists are one placed card per item by default, son.LISTES; son.releve: a numbered list of the sounds of a video, to be reviewed by number); scripts/palette-import.py (a palette of real recordings, same file names, falls back to the default); scripts/sfx.py (a few extra sounds: shutter, focus tick, whoosh) | scripts/ |
| Mix | music + sound effects + voice, music ducked under the voice, EBU R128 target loudness, true peak limited to -1.5 dBTP (oversampled limiter, only when it overshoots) | scripts/mix.py |
| Checks | tonal balance per octave, loudness | scripts/ltas.py, mix.py |
Licenses. Remotion is free for individuals and for companies of up to 3 employees; beyond that a company
license is required (remotion.pro). Read its LICENSE.md before using the kit commercially in a larger company.
The fonts Inter and Source Serif 4 are under the OFL (public/fonts/OFL-*.txt); the sounds and the example
music are synthesized here, with no third-party rights. Music found elsewhere: check its license (no CC-BY-NC on
a monetized channel).
Setup, and what the node_modules line does
The plugin installs once with:
bash "${CLAUDE_PLUGIN_ROOT}/install.sh" --data "${CLAUDE_PLUGIN_DATA}"
(Claude Code replaces both variables with real paths when it reads this file.) Why a data folder: the plugin
folder is replaced at every plugin update, and node_modules weighs about 1 GB. So npm ci runs once in the
plugin's data folder (${CLAUDE_PLUGIN_DATA}), which survives updates, and skills/motion-design/node_modules
is a symlink to it. If node_modules is missing after a plugin update (the new plugin folder has no link),
rerun the line above: it only does what is missing and relinks in a second.
Manual install (the four skills/ folders copied side by side into ~/.claude/skills/): bash install.sh
from the kit root; node_modules is then a normal folder installed by npm ci. Python side:
python3 -m pip install numpy scipy soundfile pillow, and brew install ffmpeg. Details and a check of the
whole chain: INSTALL.md of the kit.
Run every command of this skill from the workshop folder (skills/motion-design).
Pipeline of a whole motion-design video
- Frame it: duration, format (1920x1080 at 30 fps by default; 1080x1920 for a vertical reel; 1080x1080 for a square), use, platform constraints. Write
src/<project>/cues.json: the key times drive both picture and sound. - Animate: one composition per deliverable in
src/<project>/, declared insrc/Root.tsx, props typed with zod. Everything derives fromuseCurrentFrame(): never a free CSS animation, neverMath.random()(random(seed)ornoise2D). Theme colors and fonts only. - Check the picture without waiting for a render:
npx remotion still src/index.ts <Id> out.png --frame=Non the key frames, then a contact sheet of the MP4 (ffmpegtile). Judge at phone size: the image reduced to 390 px wide must stay legible. - Voice-over (if any): recorded by the user (48 kHz WAV), placed with
--voice-at. The kit does not clone voices. - Music and sound effects:
python3 music/<project>.py <duration> music.wav; sounds throughson.py, placed on the same instants as the picture (the plan callsson.releve(...)to write the numbered list to listen to); check withpython3 scripts/ltas.py music.wav(no hole deeper than about 6 dB against a gentle slope). - Mix:
python3 scripts/mix.py --music ... --sfx ... [--voice ... --voice-at s] --lufs -16 --out public/audio/<project>_mix.wav(-14 for a reel, -16 for YouTube;--sfx-voix-ref -22rescales the sound effects on the real voice). - Render:
npx remotion render src/index.ts <Id> out.mp4 --props='{"audio":"audio/<project>_mix.wav"}' --codec=h264 --crf=16 --audio-bitrate=320k. - Deliver without overwriting an existing deliverable (suffix
-v2), with a preview.
Renders placed in Premiere (graphics)
- No logo in a scene, a full-screen graphic or a lower third: it loads the image for nothing.
- A file meant for a 29.97 fps sequence must cover ALL of its clip: n sequence frames last n x 1.001 / 30 s. The compositions of
src/illustration/accepttailFrames: theTenirFincomponent (src/illustration/tenir-fin.tsx) plays the animation over its duration, then holds the last frame; exit animations do not move. Theoverlaysskill'sscripts/render-segments.pycomputestailFrames. - The graphics compositions are rendered by
scripts/render-segments.py,scripts/screencast-render.pyandscripts/montage-tella.pyof theoverlaysskill (work folder = this workshop, PNG frames, 4 cores), never by hand outside aremotion stillcheck. Media called throughstaticFilego inpublic/scenes/<video>/.
Make it yours (change the theme)
The kit ships with a neutral theme: blue accent #2F5BEA, Inter and Source Serif 4, one theme file,
src/illustration/theme.ts. To use your own brand:
- Colors:
src/illustration/theme.ts(color: base, contrast, inkSoft, muted, surface, line, accent, accentDark, accentWash, darkBg, paperWarm); the scenes read it throughC. The accent and its light tints are still hard-coded in a few places:grep -rn "#6B8CFF\|rgba(47,91,234\|rgba(214,224,252" src/(highlights inpreuves.tsx,Screencast.tsx, thePALETTESvariants ofcompositions/FondAnime.tsx), to match the new accent. One accent, used as a signal, never as a fill. - Fonts: drop the
.woff2files inpublic/fonts/, then change the file names and family names in the loading lists (src/illustration/scenes/kit.tsx,compositions/BrandedVideo.tsx,compositions/LowerThirdTrack.tsx,compositions/Screencast.tsx) and intheme.ts(font.display,font.sans). Check that the font has digits and accents (a "latin-ext" subset alone has neither digits nor base letters, and everything falls back to Georgia). Family name without a digit (or quoted in CSS). - Animation curve:
easeStdinscenes/kit.tsxandEASINGinscripts/make-sfx.pygo together (the sound follows the picture's curve). - Check:
npx tsc --noEmit, then aremotion stillof each composition (quote, lower third, section card, number...), then--forceon the nextrender-segments.pyrun (its cache already follows the code ofsrc/illustration/).
Add a scene or a sound
- Scene: a component in
src/illustration/scenes/opaque.tsx(full-screen) oralpha.tsx(overlay), itsdatacontract as a comment above it, one line in the registryscenes/Scene.tsx, one line in the catalog of theoverlaysSKILL.md. Every instant comes from thecues. Check:remotion stillat several frames, the overlay composited on a frame of the clip. - Sound:
reference/sound-palette.md, "Add or tweak a sound".
Synced folders trap
If the workshop lives in a synced folder (Dropbox, iCloud, Drive...), another machine can send back an old
version of a file and rename the good one "conflicted copy". Symptom: Could not find composition with ID ....
Reflex: find . -name '*conflicted copy*' -not -path './node_modules/*', compare, keep the most recent version.
The webpack cache (node_modules/.cache) can conflict too: delete it, Remotion rebuilds it. Better: keep
node_modules out of the sync.