Code-made motion graphics → MP4 with sound. Write the animation as a web page, yourself or with an AI coding assistant, and this repository renders it frame by frame to a checked MP4 with sound. Project code: 0BSD. Adapted helpers and assets retain their third-party notices.
▶ Intro video, with sound · Live gallery · No build step · Editable motion and sound
GitHub does not play repository videos inline, so the loop above is a short GIF (the only GIF in the repository); the film cards below are posters. The live gallery plays silent loops on its cards, the real videos with sound and every effect.
This repo has two jobs. It shows which effects exist, what videos they can make and which use cases they suit. It also gives you the means to write the animation as web code and render it, frame by frame, to an MP4 with sound. Production work (briefs, prompts, client material) stays outside it.
Describe the video to an AI coding assistant, or write it yourself. It becomes a web page: motion, music and sound effects are made in code, and the text, data and timeline stay editable. Original image assets can be supplied separately. The dialogue films add generated speech with analysed mouth cues.
git clone https://github.com/howardc38/motion-fx-lab.git
cd motion-fx-lab
npm install
npx playwright install chromium
npm run setup:hooks # once per clone: pushes only branches and tags, after npm run check:public
node video/serve.cjs 8000 # preview at http://127.0.0.1:8000/ (byte ranges, so MP4s play and seek)
bash video/build.sh examples/reel.html # render an example, or a page of your own, into media/Rendering needs macOS on Apple silicon for the GPU path, plus Node, Python 3 (standard library only) and ffmpeg with libx264. Tested with Node 26.8.1, Python 3.12.10, ffmpeg 8.1.1 and Playwright 1.59.1. Without a Metal GPU, render on SwiftShader with RECORD_ARGS="--cpu --workers 2" bash video/build.sh examples/reel.html. Films with WebGPU effects, such as examples/stacks.html, need a real WebGPU adapter and refuse --cpu.
Each film page has a scrub bar while you preview it; the recorder hides it. The build checks the video, audio, poster and preview in a staging folder before it replaces the matching files in media/; a failed check keeps the previous ones.
Each card is one editable page. Change the words, numbers or brand near the top of the file and render again; other ratios and brands come from the same page. The studio, shops and numbers are fictional sample data.
Use cases
![]() This weekend only · 12 s · 9:16 One data file, three shop promos, safe for every app’s buttons. Use for: Reels, Shorts and TikTok promos for a shop or venue. Also rendered: bakery, 9:16 · yoga studio, 9:16 Source |
![]() Feature launch in 15 seconds · 15 s · 16:9 One new feature, from problem to result, in 15 seconds. Use for: Feature launches, release notes and app-store previews. Also rendered: 1:1 Source |
![]() Year in numbers · 14.4 s · 16:9 Your figures, each page ending on its point. Use for: Annual reviews, reports and data explainers. Also rendered: 9:16 Source |
![]() Background loops in place · 12 s · 16:9 Seamless loops behind a title, an interview and a “starting soon” screen. Use for: Title backgrounds, interview backdrops and stream holding screens. Source |
![]() Titles & logo stings · 18 s · 16:9 Type a name, pick a preset, render a 6-second opener. Use for: Channel intros, podcast openers and logo stings. Also rendered: 9:16 Source |
![]() Event countdown · 15 s · 16:9 Change the name and details, get a beat-locked countdown and logo reveal. Use for: Launch, livestream and event countdowns. Also rendered: 9:16 Source |
![]() Product spotlight · 10 s · 4:5 One product, three ratios: drop, callouts, colourways, price. Use for: Shop listings, product ads and social posts. Also rendered: 9:16 · 1:1 Source |
![]() Clear · 15 s · 16:9 A drop becomes a crown splash, rises into a bottle that refracts the brand, dives underwater and surfaces on the end card. Use for: Product adverts and material-led openers. Also rendered: 9:16 Source · edit guide |
Demo films
![]() Every Stroke Is Code · 12 s · 16:9 Over 1,000 stroke-sliced Latin and CJK vectors assemble a multi-angle Bento mural, invert polarity from a single dot and close a seamless loop. Use for: Launch reels, kinetic-typography manifestos and developer tool announcements. Source · edit guide |
![]() Liquid Mercury Type · 12 s · 16:9 Liquid metal beads nucleate, coalesce into sculpted Latin and CJK chrome lettering under studio softboxes, then melt away into a seamless loop. Use for: Luxury title cards, album visuals and tactile brand stings. Source · edit guide |
![]() The Final Final · 23.8 s · 16:9 The bear asks for the launch video. Pip has made one small, dramatic improvement. Use for: Voiced character stories, product explainers and comedy shorts. Source · edit guide |
![]() COUNTERFORM · 19.8 s · 16:9 Original action choreography and contact holds; at the climax time stops and the camera circles the fighters as a 3D point cloud. Use for: Action promos, game trailers and sports intros. Source |
![]() Inside the machine. · 14.4 s · 16:9 A section plane opens closed housings while the detailed core keeps moving; real cap geometry preserves wall thickness. Use for: Technical explainers and product interior reveals. Source · edit guide |
![]() Apart. Together. · 14.4 s · 16:9 A detailed turbofan opens into seven service assemblies and returns along their ordered paths. Use for: Product teardowns, maintenance explanations and assembly reveals. Source · edit guide |
![]() Spark / Form · 16.8 s · 16:9 One point becomes an outlined word, a passage through space and solid engraved type. Use for: Title sequences, technical identities and brand openers. Source · edit guide |
![]() One flight. Twenty worlds. · 40.8 s · 16:9 One courier travels through twenty artworks while its flight and cape remain continuous. Use for: Character-led journeys and transitions between visual worlds. Source |
![]() One block. A drawn city. · 12 s · 16:9 Hatching is anchored in object coordinates. Camera movement preserves the marks, while changing light changes ink density. Use for: Architectural and illustrated 3D scenes. Source · edit guide |
![]() One dot. Many roles. · 14.1 s · 16:9 A visual cue connects type, timing, depth and a reveal; closes its loop. Use for: Brand loops, logo openers and identity motion. Source · edit guide |
![]() Same fight. Eight treatments. · 38.4 s · 16:9 A common source makes eight different operations comparable. Use for: Choosing a treatment for moving footage. Source |
Additional format or brand cuts and effect films (the full render of one effect) are listed with everything else under All published films.
Each effect is a small function of time that draws one look or motion: materials and light, simulations, characters, UI, type and time controls. The live gallery plays them all in the browser, orders them by visual design craft and motion feel, keeps variants inside one card, and groups live scene presets below the main grid. See how effects are grouped.
Code families with their own guide: stacks · studio · materials · fluid · design · signals · character · dialogue · plush · mechanical · graphic · collage · presenter · mural · chrome-type · ui-stage · telemetry · editorial · diagrams · sakuga · ascii.
Copy a page from examples/ or video/intro.html and change it. The timeline engine needs:
#frame > #stagewithdata-w,data-handdata-dur(width, height, seconds).section.scenechildren withdata-startin seconds. A scene wipes in over 0.5 s unless it hasdata-trans="cut"; an optional.edgechild draws the wipe's edge.- Elements animated by
data-fxplusdata-at(seconds after the scene starts). The names and their extra attributes are listed at the top ofvideo/engine.js; an unknown name is an error. An element that holds a canvas, a video, a frame (iframe) or a gallery tile is never scaled: its scaling effects slide and fade instead, so parallel renders agree. data-sfxfor a sound cue, using a name fromLEVELinvideo/sfx.py.window.__music = [[seconds, section], …], with sectionsintro,build,drop,lift,break,finalandtail. Put the drop on a 2.4 s bar line.- Optionally
window.__poster = secondsfor the poster frame (a third of the way in by default); the poster is that exact frame. A film with a card on the website also declareswindow.__preview = [[start, end], …], the shots of its silent card loop (cut to fit 800 × 450, at most 2 MB). Only the intro declareswindow.__gif, for the README hero (at most 3 MB). - Optional
window.__audio = {src, sha256}selects a repository-local score instead of generated music and effects. Its hash and duration are checked before delivery. frame(t)andwindow.__renderHooksmay return promises; the recorder and the engine await them before reading pixels.- Gallery effects run inside a scene with
<div class="stage" data-demo="<id>" data-t0="…">, and another timeline page plays inside a frame (<iframe data-src="…" width height>at its own size, drawn at an exact time each frame and loaded only while its scene plays); see the script at the bottom ofvideo/intro.html. For native-resolution shader footage, setwindow.FX_RENDER_SIZE = {w: 1920, h: 1080}before loadingfx/demos.js.
Then run bash video/build.sh video/yours.html. To keep your films in a folder of their own while using this library, or to add one to the published list, see video/PROJECTS.md.
A timeline effect never keeps state between frames that t does not decide. It reads t and sets what it draws:
demo({ id: "count", kind: "type", period: 3.2, hero: 2.4, /* name, stacks, chips, purpose… */
build(stage) {
stage.innerHTML = `<div class="cnt-n">HK$<span>0</span></div>`;
const n = stage.querySelector("span");
return (t) => { n.textContent = Math.round(12000 * outCubic(lin(0.3, 1.6, t))).toLocaleString("en-US"); };
} });No requestAnimationFrame state, no clock, no unseeded randomness at draw time. Simulations follow the same rule: frame t shows the state after exactly round(t × steps per second) fixed steps from a fixed start, and asking for an earlier t starts again from step 0. That is what lets you scrub to any moment, render a frame again and get the same pixels, and split a video across browsers. Blender fluids instead select a frame from a finished offline bake; Blender is only needed to change the simulation.
page.html ─ record.cjs cues ─► cues.json ─► sfx.py + music.py ─► mix.wav ──────────┐
└ record.cjs video ─► lossless BT.709 master (rendered twice, compared) ──┴─► deliver.py ─► .mp4 · _hq.mp4 · .jpg ─► preview.py ─► .preview.mp4 (and gif.py ─► .gif)
video/build.sh runs all of it.
examples/films.json is the only source for this table, the site's film cards and lists, and fx/films-data.js; run npm run sync:films after editing it. Effect films also play inside their effect cards on the site.
40 published films: 1 intro, 17 use cases, 11 demo films and 11 effect films. Effect films also play inside their effect cards.
A loop (or the intro's GIF) is a silent selection of shots, not the full film. To cut the loops again from films already in media/, without rendering, run node tools/make-previews.cjs (add --posters to take each poster again). MP4 container durations can be about 0.1 s longer than the authored timeline because of the final frame and encoding timestamps.
npm run build:all # every published film
node tools/films.cjs build <id> # one film, or a build group such as uses, studio or dialogue
npm run test:media # each bundle against its page: size, length, poster, loop and size budgets
npm run check:films # films.json against the schema, the catalogue and the source codeAn appearance change to a shared renderer means rebuilding the films that use it; the intro embeds gallery effects, and the fight comparison uses the shared studio effects. Review the moving pictures as well as the tests: matching metadata or pixels do not prove a well-directed effect.
| Time | Story |
|---|---|
| 0–2.4 s | Motion graphics in plain HTML, made by you or your AI coding assistant |
| 2.4–16.8 s | Six use cases, 2.4 s each, each played from its own published page: shop promo (9:16), feature launch, year in numbers, product spotlight (4:5), event countdown and B-roll loops |
| 16.8–21.6 s | A shot from The Final Final: the startled plush bear asks whether the launch video is ready and Pip answers "I made it more dramatic", with mouth cues, expressions and gestures in sync |
| 21.6–26.4 s | Force-field dots, animated glyph interiors, distance connections, graphic long shadows and a 3D word tunnel |
| 26.4–33.6 s | One Flight, Twenty Worlds: six readable styles, then the twenty-style overview |
| 33.6–38.4 s | Spark / Form, played from its published page: ember drawing, a dive through the O, then solid engraved lettering |
| 38.4–43.2 s | Mechanical inspection: ordered service disassembly, then a moving solid section through the same detailed engine |
| 43.2–50.4 s | The real line of examples/titles.html changes from SLOW SIGNAL to FLOW STATE, the same page rebuilds the new word, and bash video/build.sh reports 2 renders and 0 differing frames before the MP4 |
| 50.4–55.2 s | Repository and licence call to action |
Source: intro.html and intro-stories.js. The use-case, Spark / Form and mechanical shots are the published pages themselves, drawn at exact times, so the intro shows the current films after a rebuild; the stories combine existing effects and are not extra effects.
| Problem | What this repo does |
|---|---|
| Animation you cannot diff, review or batch | Every timeline effect is a small function of time in a plain web page; no framework or bundler. Render ten variants from a script. |
| Slow 3D in headless Chrome | Playwright's default headless Chromium runs WebGL on SwiftShader, a software GPU (99.8 ms a frame on our test scene). The recorder uses ANGLE Metal (4.0 ms), lossless CDP screenshots and 4 browsers in parallel. |
| Silent wrong frames | A lost WebGL context screenshots as a blank frame, and parallel browsers can disagree. The whole video is rendered twice, each frame on a different browser, and every frame is compared. |
| Colour shifts | ffmpeg's defaults use the BT.601 matrix with no colour tags, and browsers read untagged HD video as BT.709 (#ff90e8 played as #ff9fe8). Screenshots are converted with BT.709 and every file is tagged; the checks refuse an untagged file. |
| Quiet or rejected uploads | Audio is limited, then brought to −14 LUFS by a linear gain, and its loudness and true peak are measured on the final file. AAC stays within 128 kbps for Reels. |
| CJK text | Solid lettering uses bundled CJK vector outlines; unsupported glyphs report how to rebuild the subset. |
| Licence worries | Project code under 0BSD; adapted mechanical helpers keep MIT notices. Music and sound effects are synthesised in code; see THIRD_PARTY.md for all retained notices. |
Four distinct operations cover force-field dots, animated glyph interiors, proximity connections and flat long-shadow type. The graphic guide documents editable controls and the seven standalone cards and extensions: split-flap letters, rolling reels, text bands, spatial lettering, grid choreography, LED lettering and temporal echoes. The four effect films play from their matching cards; all export at 1080p60 using Canvas 2D and the existing three.js core.
Three separate effects cover service disassembly, longitudinal solid sections and a geared slider-crank mechanism. Their complete films play inside the matching effect cards; the mechanical guide documents controls, geometry and checks. Spark / Form is a finished demo film combining existing lettering and surface hatching with authored drawing and camera transitions, not a new collection of effect families.
npm run check:mechanical checks actual part transforms, link length, registered pipe clearances, section geometry, service order and framing. Positive controls must be caught. This complements the two-render check; it does not certify arbitrary meshes, manufacturing tolerance, CFD or visual quality.
Detected rendering discrepancies or failed delivery checks stop publication and keep the previous files. These checks establish repeatability and delivery limits; visual meaning, motion quality and physical plausibility still need review.
- WebGL must run on ANGLE Metal, or on SwiftShader when you pass
--cpu; backends are never mixed in one run. - Every WebGL context is checked on every frame. A page error, or a font still loading or failed, stops the run.
- Every frame is rendered twice, by two different browsers, and all frames are compared. A frame fails if more than 0.01 % of the samples in its luma or chroma planes differ by more than 8 levels, or more than 20 differ by more than 48. On a mismatch the recorder keeps the frames in
video/mismatch/andbuild.shrenders again, up to 5 times. - The audio must reach −14 LUFS by a linear gain after limiting. Each file is checked for codec, BT.709 tags, size, frame rate, peak bitrate, an AAC track within 128 kbps,
moovahead ofmdatand no edit list. - Two causes of disagreement found so far are handled in
video/engine.js: SVG text laid out at a stale scale (it now usesgeometricPrecision), and canvases rasterised at different scales while scaling in (elements that hold a canvas, video or tile now slide and fade instead).
Measured on an Apple M4 laptop on 2026-09-28, on a 56-second, 1,693-frame test video with heavy 3D that is not included in this repository. These historical timings are not measurements of the current intro.
Default headless Chromium (one browser, SwiftShader, page.screenshot) |
399.6 s |
| This recorder, 4 browsers, every frame rendered twice and compared | 52.9 s (again: 51.5 s) |
| With an 8-frame spot check instead of full verification, 1 / 4 / 6 / 8 browsers | 71.0 / 26.4 / 28.5 / 31.9 s |
Pixels identical between the fast CDP capture and page.screenshot |
8 of 8 test frames |
Beyond 4 browsers there is no gain, because a single ffmpeg process decodes every screenshot and writes the master. The live gallery's developer notes have the full table.
| This repo | Remotion | HyperFrames | After Effects | |
|---|---|---|---|---|
| You author | HTML, CSS, SVG, Canvas, three.js, GLSL | React components | HTML compositions | Timeline compositions, expressions and scripts |
| Licence | 0BSD | Free for eligible users; Company License otherwise | Apache-2.0 | Paid subscription |
References checked on 2026-10-03: Remotion licence, HyperFrames and Adobe scripting documentation.
| Path | What it is |
|---|---|
index.html, fx/gallery-page.js, fx/gallery-meta.js |
The site: use cases, demo films, the effect gallery and live scene presets |
fx/manifest.json, tools/fx-manifest.cjs |
Which scripts register which effects, in load order, and the gallery order (npm run sync:fx) |
fx/catalog.js |
Effects, variants and presets: which demo belongs to which card |
fx/demos.js, fx/pack-*.js |
The core effects and the packs that register more with FX.demo |
fx/<family>/, fx/*-effects.js |
Larger effect families (studio, materials, fluid, design, signals, character, dialogue, plush, stacks, mechanical, graphic), each with its guide |
examples/, examples/films.json |
Film pages and the published film list; tools/films.cjs validates it and writes the generated blocks |
video/engine.js |
Timeline engine: scenes, data-fx animations, wipes, sound cues and the preview scrub bar |
video/record.cjs, video/deliver.py, video/build.sh |
Render twice and compare, master the sound, deliver and check the files |
video/sfx.py, video/music.py |
Sound effects and music synthesised from oscillators and noise |
video/serve.cjs |
Loopback preview server with byte ranges, also used by the recorder |
assets/ |
Original characters, footage, glyph outlines, bakes and textures, each folder with its manifest or guide |
tools/check-public.cjs, .githooks/pre-push |
The public-scope check and the hook that runs it before a push |
- The GPU path is verified on macOS only. In our benchmarks, one of 8 parallel SwiftShader browsers lost its WebGL context and returned blank frames without an error, which is why the recorder checks every context on every frame.
- Emoji and a few symbols (✓, ★) are drawn with the operating system's fonts, so a render on Linux or Windows looks slightly different.
- The main gallery uses three.js r128; newer modules pin r180. Upgrading the gallery means re-tuning every colour, light and shader.
- Solid lettering uses a bundled subset of CJK outlines plus Latin capitals and digits; other characters require rebuilding that asset.
- The reaction–diffusion effect gives the same pixels every time on one GPU, but a different pattern on SwiftShader than on Metal. The recorder never mixes the two in one run.
- Browser tests cover effect contracts, gallery framing, film metadata, asynchronous recording and publication rollback. There is no CI yet; full video builds and visual review are done locally.
- Any render difference we have not met: the frames in
video/mismatch/show where two browsers disagreed. - A GPU path on Linux, so renders can run on a server.
- Broader recorder tests and CI, especially failure paths and non-macOS GPU coverage.
- More effects, as long as each one stays a pure function of
t.
If this saves you an export marathon or a night chasing blank frames, star the repo.
0BSD: use, copy, modify and distribute for any purpose, with or without attribution. The adapted mechanical helpers and TSL example, fonts, textures and voices keep their own notices; three.js, Matter.js, Flubber, PixiJS, Rapier and the web fonts load from public CDNs under their own licences. See THIRD_PARTY.md.



















