Make editable motion graphics and export them to MP4: social clips, product demos, infographics, animated titles, backgrounds and character sequences. Choose a use-case example or finished sequence, then adapt its copy, data or motion. Start with the 12 selected films; the intro and nine supporting studies remain available separately. The effect library contains 70 families, 22 selectable variants and 5 combined showcases. Every exported frame is rendered twice and compared before delivery.
▶ Full intro video, with sound (55 s, rendered by this repo) · Live gallery · Licence: 0BSD, no conditions · No build step · No AI-generated images or sound
GitHub does not play video files from a repository inline, so the loop above is a GIF. The gallery plays the real video.
Describe the video to an AI coding assistant, or write it yourself. It becomes a web page, and this repo renders it to MP4, frame by frame. No AI-generated pictures or sound, so every word and number is exact and every frame can be edited.
![]() Social clips. 9:16, 4:5 or 16:9. 12 s video · source |
![]() Product demos. Your screen, a cursor, a tap. 7 s video · source |
![]() Infographics. Charts drawn from your numbers. 14 s video · source |
![]() B-roll. Background loops with no seam. 12 s video · source |
Plus characters drawn in code and 70 reusable effect techniques. The 55-second intro moves from four uses and character action into three stories: One Flight, Twenty Worlds; One Word, Many Forms; and One Push, Chain Reaction. It closes by changing text, scrubbing the timeline and exporting MP4. Use the viewing guide below to choose a starting point. The four use-case examples keep their editable text or numbers near the top of their HTML files; change them and run bash video/build.sh examples/reel.html for a new MP4. The studio and the shop are fictional, and the numbers are sample data.
The website starts with three levels:
- Use-case examples: the four GIFs above show familiar video formats you can adapt.
- Finished sequences: the three featured films below combine motion, visual treatments, editing and sound around one subject.
- Single-effect library: 70 reusable operations, with variants inside their family cards. Use these building blocks in your own films.
The Choose from 12 selected films shortlist groups five sequences, four use-case templates, one comparison and two single-effect films. The intro stays at the top of the page. Nine supporting studies are reached through related effect cards or the source archive below, rather than repeated in the main film list. The three featured choices cover art direction, connected graphic motion and materials. See the viewing and effect-family guide.
Social and Product share a fictional booking app but show different video formats. COUNTERFORM and Eight Treatments share choreography: one is a finished action sequence, the other compares operations on that action. The three Blender films are single-effect renders linked from their effect cards. All 22 video bundles and their GIF previews remain in the repository. The shortlist below contains 12; its collapsed source archive contains the other ten.
Three effect families cover 3D spatial type, source-size soft shadows and light-responsive surface hatching. Letter reflow, a spring-response graph and a radial dot reveal are variants of existing type, timing and wipe families. The dot and city sequences combine these operations; the shadow film isolates one. They reuse Three.js, Canvas and GLSL—no new runtime dependency. See the design-film editing guide.
Complete subjects with a beginning, development and closing beat. Effects and motion are reused across these films.
| Type | Film | Authored length | Format | Preview | Source |
|---|---|---|---|---|---|
| Sequence | One flight. Twenty worlds. | 40.8 s | 16:9 | GIF · poster | HTML |
| Sequence | One dot. Many roles. | 19.2 s | 16:9 | GIF · poster | HTML |
| Sequence | One drop. Many forms. | 21.6 s | 16:9 | GIF · poster | HTML |
| Sequence | One word. Many forms. | 21.6 s | 16:9 | GIF · poster | HTML |
| Sequence | COUNTERFORM | 19.2 s | 16:9 | GIF · poster | HTML |
Start here for a social ad, interface walkthrough, data story or reusable background.
| Type | Film | Authored length | Format | Preview | Source |
|---|---|---|---|---|---|
| Use case | Social clips | 12 s | 9:16 | GIF · poster | HTML |
| Use case | Product demos | 7.2 s | 16:9 | GIF · poster | HTML |
| Use case | Infographics | 14.4 s | 16:9 | GIF · poster | HTML |
| Use case | Animated backgrounds | 12 s | 16:9 | GIF · poster | HTML |
Compare eight operations against the same choreography.
| Type | Film | Authored length | Format | Preview | Source |
|---|---|---|---|---|---|
| Comparison | Same fight. Eight treatments. | 38.4 s | 16:9 | GIF · poster | HTML |
Longer exports for inspecting a specific material, lighting or simulation behaviour.
| Type | Film | Authored length | Format | Preview | Source |
|---|---|---|---|---|---|
| Single-effect render | Find the Form | 9.6 s | 16:9 | GIF · poster | HTML |
| Single-effect render | Overflow | 9.6 s | 16:9 | GIF · poster | HTML |
Supporting studies and intro — source archive (10 bundles)
The intro is already shown above. Ink City and Shadow Lab are linked from surface hatching and soft shadows. The optical and three-operation samplers belong to related effect cards; the room comparison belongs to its combined-scene preset. Slow Gold is retained as an archived material study pending visual refinement. The three signal-driven studies below connect actual audio features to solid relief, actual image movement to emitted fragments, and music attacks to a held 3D impact.
| Type | Film | Authored length | Format | Preview | Source |
|---|---|---|---|---|---|
| Overview | Intro | 55.2 s | 16:9 | GIF · poster | HTML |
| Sequence | One block. A drawn city. | 19.2 s | 16:9 | GIF · poster | HTML |
| Single-effect render | One light. Different edges. | 9.6 s | 16:9 | GIF · poster | HTML |
| Comparison | Six optical treatments | 18 s | 16:9 | GIF · poster | HTML |
| Comparison | Image distortion, collisions and particles | 24 s | 16:9 | GIF · poster | HTML |
| Comparison | One scene. Many eras. | 24 s | 16:9 | GIF · poster | HTML |
| Single-effect render | Slow Gold | 9.6 s | 16:9 | GIF · poster | HTML |
| Signal-driven study | Pulse / Form | 12 s | 16:9 | GIF · poster | HTML |
| Signal-driven study | Motion / Release | 12 s | 16:9 | GIF · poster | HTML |
| Signal-driven study | Impact / Orbit | 12 s | 16:9 | GIF · poster | HTML |
All 22 retained video bundles have animated GIF previews and still posters. A GIF
is a silent selection of shots, not the full film. MP4 container durations can be
about 0.1 s longer than the authored timeline because of the final frame and
encoding timestamps. assets/studio/fight-source.mp4 is a silent green-screen
source asset, not another finished film.
npm run build:all # all 22 retained video bundles
npm run build:showcase # intro, optical, stacks, studio, material, design, signal and baked-fluid films
npm run render:design-films # dot story, ink city and soft-shadow study
npm run render:signal-films # measured audio relief, motion fragments and impact orbit
npm run test:media # source dimensions/duration and MP4/poster/GIF bundle checksAn appearance change to a shared renderer requires rebuilding its dependent
films and previews. In particular, the intro embeds gallery adapters; the
fight-effects comparison uses the shared studio effects. Run
npm run build:studio-assets first when changing the shared fight choreography
or the 3D model, then rebuild the dependent film bundles. Review actual moving
pictures as well as tests: different metadata or pixels do not by themselves
prove a useful or well-directed effect.
| Time | Story |
|---|---|
| 0–2.4 s | Motion graphics in plain HTML |
| 2.4–16.8 s | Social clips, product demos, infographics and B-roll; 3.6 s each |
| 16.8–21.6 s | A character hands over information; an impact freezes while camera and lighting keep moving |
| 21.6–26.4 s | Audio-driven solid relief, motion-driven fragments, spatial type and volumetric smoke |
| 26.4–33.6 s | One Flight, Twenty Worlds: six readable styles, then the twenty-style overview |
| 33.6–40.8 s | One Word, Many Forms: 流動 → solid lettering → particles → contour lines → signal breakup → 流動 |
| 40.8–45.6 s | One Push, Chain Reaction: follow the fall, then reveal all 48 dominoes |
| 45.6–50.4 s | Scrub time, change WAVE to FLOW and export MP4 |
| 50.4–55.2 s | Repository and licence CTA |
These three intro stories combine existing capabilities; they do not add three new effect registrations. Source: intro.html and intro-stories.js.
- Changing one word means another export. Your motion graphics live in a desktop app. You cannot diff them, review them in a pull request or render ten variants from a script.
- Some alternatives use React and eligibility-based licensing. Remotion uses React; its licence offers free use for eligible individuals, small for-profit organizations and nonprofits, with a Company License for others.
- Headless Chrome renders your 3D on a CPU. By default Playwright's headless Chromium runs WebGL on SwiftShader, a software GPU. On our test scene that was 99.8 ms a frame instead of 4.0 ms on the real GPU.
- Browser capture fails silently. A lost WebGL context screenshots as a blank frame, with no error. Parallel browsers can disagree too: after a scale animation, one of ours kept laying out SVG labels at 0.6 of their size. A spot check of 8 frames missed that; we only caught it by comparing whole renders.
- Colours shift in the browser. Converting screenshots to video with ffmpeg's defaults uses the BT.601 matrix and writes no colour tags, and browsers read untagged HD video as BT.709. Our pink
#ff90e8played back as#ff9fe8. - The upload comes out quiet, or gets rejected. Our first mix measured −21 LUFS, far quieter than the −14 LUFS convention, and used AAC at 160 kbps, over Meta's 128 kbps limit for Reels. ffmpeg's
loudnormsilently switched to dynamic compression when a linear gain would clip. - Chinese, Japanese and Korean text break common effects. Small Latin-only 3D fonts cannot render CJK, and halftone or glitch effects can wipe out dense strokes. This repo now includes a separate CJK outline subset for solid type.
| Pain | What this repo does |
|---|---|
| Hand-built, un-diffable animation | Every timeline effect is a small function of time in a plain web page. Serve the gallery over localhost; no framework or bundler. |
| Starting from a blank page | 70 effect techniques you can copy: kinetic type, UI mock-ups, charts, generative patterns, ray-marched and GPU-particle shaders, physics simulations, chrome, and toon, flat and dithered characters. |
| Slow 3D in headless Chrome | GPU rendering through ANGLE Metal, lossless CDP screenshots and 4 browsers in parallel: 399.6 s → 52.9 s for a 56-second, 3D-heavy video, including the second render that verifies it. |
| Silent wrong frames | The whole video is rendered twice, each frame on a different browser, and every frame is compared. The recorder also stops on a CPU fallback, a lost WebGL context, a page error or a font that did not load. |
| Colour shifts | Screenshots are converted with the BT.709 matrix and every file is tagged BT.709; 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 encoding can lift the true peak (by up to 0.7 dB on our videos), so the encoded audio is measured and mastered again lower if it would pass −1 dBTP. Video and audio are checked against Meta's Reels limits. |
| CJK text | Canvas-sampled particles use available fonts. The solid CJK variant uses bundled vector outlines, also shared by its particle and contour treatments; unsupported glyphs report how to rebuild the subset. |
| Licence worries | Code under 0BSD: use it for anything, no attribution needed. Music and sound effects are synthesised in code, so there is no sample to license. |
git clone https://github.com/howardc38/motion-fx-lab.git
cd motion-fx-lab
npm install
npx playwright install chromium
bash video/build.sh video/intro.html # writes media/intro.mp4, intro_hq.mp4, intro.jpg and intro.gif
bash video/build.sh examples/reel.html # any example, or a page of your own
python3 -m http.server 8000 # optional gallery preview: http://localhost:8000/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 containing WebGPU effects, such as examples/stacks.html, require a real WebGPU adapter and refuse --cpu.
The build verifies the video, audio, poster and optional GIF in a staging directory before replacing the matching bundle in media/. A failed render or GIF generation preserves the previous bundle.
Measured on an Apple M4 laptop on 2026-09-28; the two previous-intro rows on 2026-09-29, before the nine new studies were added. Those historical timings are not measurements of the current 55-second intro.
56-second, 1,693-frame promo with heavy 3D, default headless Chromium (one browser, SwiftShader, page.screenshot) |
399.6 s |
| The same video, this recorder, 4 browsers, every frame rendered twice and compared | 52.9 s (again: 51.5 s), of which 17.6 s is the first render and 16.8 s the second |
| Before full verification was added, with an 8-frame spot check instead, 1 / 4 / 6 / 8 browsers | 71.0 / 26.4 / 28.5 / 31.9 s |
| Previous 41-second intro (2026-09-29), 1,225 frames: render, then the verifying second render | 12.6 s + 12.1 s, all 1,225 frames matching on the first attempt |
Pixels identical between the fast CDP capture and page.screenshot |
8 of 8 test frames |
| Previous intro audio and colour (2026-09-29) | −14.1 LUFS integrated, −1.4 dBTP true peak, AAC 126 kbps; BT.709, tagged |
The PixiJS / Rapier / WebGPU film is verified by rendering every frame twice on independent browsers, including arbitrary backward seeks. The recorder awaits asynchronous GPU completion, checks WebGL context loss and WebGPU device errors, and compares every frame before delivery. Pixel equivalence across different GPU models is not claimed.
The 56-second promo is one of ours and is not in this repo. Beyond 4 browsers there is no gain, because a single ffmpeg process decodes every screenshot and writes the master.
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. Browser-side stateful simulations follow the same rule: frame t shows the state after exactly round(t × steps per second) fixed steps from a fixed start, a cache only saves re-running steps already taken, 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. The original 3D tiles share one WebGL renderer; the three optical shaders share a second WebGL context. Each copies its result into its own canvas. PixiJS and Rapier own renderer contexts; WebGPU/TSL uses a compute-capable device. The studio effects share an additional r180 renderer. Blender fluids instead select a frame from a completed offline bake; their physics do not run in the browser. All effects obey the same awaited timeline contract.
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 ─► gif.py ─► .gif
video/build.sh runs all of it.
Copy video/intro.html and change it. The 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 or a gallery tile is never scaled: its scaling effects slide and fade instead, so parallel renders agree (see below). 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.__gif = [[start, end], …]for the GIF preview, andwindow.__poster = secondsfor the poster frame (a third of the way in by default). - Optional
window.__audio = {src, sha256}selects a repository-local score instead of generated music/SFX. Its hash and duration are checked before delivery; root-relative paths start at the repository root. frame(t)andwindow.__renderHooksmay return promises. Await them before reading pixels; the recorder and timeline engine do so.- Gallery tiles run inside a scene with
<div class="stage" data-demo="<id>" data-t0="…">. See the script at the bottom ofvideo/intro.html. - For native-resolution shader footage, set
window.FX_RENDER_SIZE = {w: 1920, h: 1080}before loadingfx/demos.js. The gallery uses 640 × 360; existing film embeds default to 480 × 600 when omitted. This sets the shared renderer size for the page.
Then run bash video/build.sh video/yours.html.
| Path | What it is |
|---|---|
index.html |
The gallery: timeline previews, including replayed offline bakes, filtered by kind or implementation, with the stack table and render measurements |
fx/catalog.js, fx/gallery-layout.js, fx/gallery-layout.css |
Technique/variant/showcase classification and landscape gallery compositions; original demo IDs remain compatible with films |
fx/demos.js, fx/demos.css |
The core effects, each build(stage) returning frame(t, abs), their styles, and the helpers the packs share (window.FX) |
fx/pack-dither.js, fx/pack-2d.js, fx/pack-shaders.js, fx/pack-sims.js |
Effect packs: the dithered character, 2D motion and generative patterns, GLSL shaders, and the two simulations. Each registers its tiles with FX.demo |
fx/optical-effects.js, fx/pack-optical.js |
Shared optical renderer and six timeline effects: moiré, slit-scan type, ribbon, caustic light, foil and path morph |
fx/material-effects.js, fx/pack-materials.js, fx/material-studies/ |
Shared water surfaces, CJK solids, contours, frame glitch, room treatments and film choreography |
tools/blender/, assets/fluid/, fx/fluid/ |
Optional Mantaflow scene generation, verified baked frame assets and deterministic replay |
fx/material-player.js |
Shared pause, seek and export player for the three material films |
assets/type/, tools/build-cjk-font.py |
Licensed vector glyph subset and optional offline outline exporter |
fx/font-helvetiker-subset.js |
14 glyphs of Helvetiker Bold for the extruded 3D text |
video/engine.js |
Timeline engine: scenes, 30-odd data-fx animations, wipes, the sound-cue list |
video/record.cjs |
Renders a page to a verified, BT.709 lossless master on the GPU, in parallel |
video/sfx.py, video/music.py |
Sound effects and music synthesised from oscillators and noise |
video/deliver.py |
Loudness mastering and the final MP4 files, each checked before it is delivered |
video/gif.py |
The GIF preview, cut from the clips the page declares |
video/intro.html, fx/intro-stories.js |
Intro storyboard and the continuous flight, typography and edit/export sequences |
examples/ |
Twenty-one example films, timeline players and effect settings; the overview intro is in video/ |
fx/stack-effects.js, fx/pack-stacks.js, fx/stacks/ |
Reusable PixiJS, Rapier and WebGPU timeline effects and gallery adapters |
fx/studio/, fx/studio-effects.js, fx/pack-studio.js |
Fourteen film effects, twenty art directions, fixed-frame footage, skeletal points and time controls |
assets/studio/ |
Original animated GLB, source clip, transparent frame sequence and technique guide |
video/import-clip.py, tools/build-studio-assets.cjs |
Import fixed-FPS RGBA footage or regenerate the original studio assets |
video/serve.cjs |
Loopback server used by the recorder for local ES modules |
video/build.sh |
The whole pipeline in one command |
Short studies isolate an operation. The related fight studies use input / effect comparisons so the difference is visible even when they share the same characters. Open a study at full size to compare source clocks, a fixed versus moving camera, solid versus sampled geometry, or a local contact before and after deformation. Drawing tools and implementation chips are expandable details.
70 technique families, 22 selectable variants and 5 separate showcases. All 77 original demo IDs remain available; twenty additions bring the callable total to 97. The gallery uses one 16:9 card per technique, including characters; variants are selected inside that card. Every main card participates in the kind and renderer filters. Renderers initialize when their cards approach the viewport.
fx/catalog.js owns this classification. Character poses/compositions, dot palettes, shape-morph implementations, particle-text implementations, diagram layouts and flythrough scenes are variants of their respective families. The five-style shader comparison, twenty-art-direction collection, combined gallery journey, underwater scene and eight-treatment room are showcases, not additional techniques. Transparent liquid morphing belongs to clay; solid CJK type belongs to lit3d; whole-frame signal breakup belongs to glitch. Families are editorial groupings of visible operations, not a claim of 70 unrelated algorithms. Image displacement includes the PixiJS poster; screen-space halftone includes text and footage; soft colour fields include the glow-orb variation. Similar input subjects do not make source-time remapping, local deformation, temporal echoes, 2.5D relief and real 3D surface points the same operation. See the grouping rules.
The default gallery order favours visual impact and contrast between adjacent cards. Focused lighting studies and small utility animations appear later. The first still for solid lettering uses the CJK variation; camera travel starts with the low-poly course.
The gallery recomposes older portrait examples into landscape: text and UI are rearranged, charts and flow diagrams use horizontal layouts, Canvas backgrounds expand their drawing area, and 3D cameras use the correct aspect. It does not stretch a portrait image into a wide card. Films retain their authored output format; the social reel remains a deliberately labelled 9:16 example.
- Rendered looks (Canvas, WebGL and WebGPU): 200,000 particles moved on the GPU, ray-marched clay with smooth blending, liquid-glass refraction, an endless grid by domain repetition, chrome with a painted environment map, one object in five styles (Bayer dither, halftone, ASCII, pixel sort, risograph), particles that assemble into words, lit 3D type with soft shadows, rays with bloom and dust, a noise dissolve patched into a lit material, fbm smoke, a line tunnel, moiré interference, procedural caustic light, holographic foil, PixiJS liquid poster, WebGPU orbital particles, twenty art directions, a low-poly flight course, footage-to-dots, rhythmic dot styles, 2.5D relief, skinned point clouds, and particle depth of field.
- Simulations: reaction–diffusion (Gray–Scott) growing out of a word, rigid bodies falling and stacking at 240 Hz, and a Rapier 3D domino chain at 120 Hz.
- Characters and their composition variants: an agent character dithered to three inks, peeking over a logo in a coin rain, fanning out cards, narrating from a badge and standing; the same agent toon-shaded with outlines and drawn as flat SVG with a per-part rig; and a halftoned figure built from spheres and cylinders, bouncing and standing; plus the original flying courier with an animated cape.
- Motion and time controls: a 0.5 s wipe, beat sync, squash and stretch next to its timing graph, a polar shape morph on a spring, a folding paper ribbon, a Flubber morph between concave outer contours, a cross-frame style portal, gallery camera travel, dot impacts, temporal echoes, time remapping and freeze-orbit camera motion.
- Backgrounds: film grain, glow orbs, a low-contrast flowing gradient, a warm grade with a soft glow, Bauhaus tile rhythm, noise ridgelines, a code-rain backdrop.
- UI and chart families: light sweep, frosted glass, scan and check, a camera move over a UI card, blueprint callouts on a dot grid, a self-drawing flow chart, a self-drawing data chart, a 3D card-flip grid, a 24-hour countdown ring.
- Type: whip-in letters with motion blur, an RGB-split glitch, halftone dots on a word, character pops, highlighter, counting numbers, typewriter, a red flash with a shake and a stamp, a 3-second headline hook, word-synced captions, sticker labels, variable-font kinetic type, slit-scan typography.
The first 36 effects were built for short promo videos about an Instagram DM assistant for insurance agents in Hong Kong, which is why the sample text talks about DMs, drafts and savings plans. Swap in your own words.
Pulse / Form, Motion / Release and Impact / Orbit are 12-second studies linked from their effect cards; they do not enlarge the 12-film shortlist. The first adds solid, audio-driven cells as a variation of image relief. Motion-driven fragments are one new family. The held-impact sequence is a variation of freeze orbit.
Offline Python/NumPy measures RMS, frequency bands and attack times from the actual score. OpenCV measures optical flow in the original RGBA fight footage. The browser reads cached measurements, so seeking and export do not depend on live microphone input or simulation history. The audio analysed is the same verified local score used by the video build. The new studies reuse the original fighters and draw solid cells/fragments in Three.js; no reference footage is bundled. See the editing and rebuild guide.
Three films share the same factories as their gallery previews. They use the existing Three.js r180 and Canvas renderers; no new runtime engine is needed.
| Gallery ID | Demonstrated operation | Catalogue role |
|---|---|---|
water-material |
Transmission, thickness and environment reflections on 3D geometry | Technique |
water-morph |
Transparent sphere, block, torus and separated droplets via an implicit surface | Variant of clay |
water-impact |
Authored drop, crown, rebound and travelling surface waves | Technique |
water-underwater |
Dive, bubbles, refracting surface and projected light lines | Showcase |
cjk-solid |
Bevelled, extruded Chinese outlines; editable supported text | Variant of lit3d |
organic-contours |
Animated nested closed curves | Technique |
frame-glitch |
Image-wide block displacement, channel offsets and scan tears | Variant of glitch |
scene-eras |
Eight room treatments sharing one cup-lifting action and layout | Showcase |
The water is authored surface animation, not a fluid-volume solver. The crown, ballistic droplets and waves are functions of time. Underwater light lines are procedural approximations, not traced caustics. Rapier is not used for liquid dynamics. The room's architecture, silhouettes, marks and palettes are drawn in code; no artwork or footage from the reference posts is included. Eight treatments do not mean eight extra effect families.
npm run build:materials # three MP4s with sound, GIFs and postersThe sources examples/water-forms.html, examples/word-forms.html and
examples/scene-eras.html declare text and preview selections;
fx/material-studies/timelines.js owns chapter durations and cut cues, while
films.js draws the shots. Edit materialFilm.text in the word film to replace 流動 with
another supported word, such as 光影. See the module guide
for factories, options and the three storyboards. The intro reuses the word film at
three times its source clock, preserving its 7.2-second chapter.
The checked-in CJK outlines require no Python package to render. Adding glyphs uses an optional fontTools build step; see the font asset guide. These original procedural material films do not require Blender. The separate baked-fluid films below use an optional offline simulation and rendering workflow.
Overflow · Slow Gold · Find the Form
These three original scenes add simulated liquid/vessel interaction, a high-viscosity flow variant and a volumetric smoke reveal. Overflow starts partly filled, shows a rising water level and lets excess water drain over the rim. Slow Gold uses a moving stream over a dark ceramic ring; this is viscous flow, not a paint-adhesion model. They are scripted in Blender 5.2 LTS and baked with Mantaflow. The liquid scenes use Cycles for reflection and refraction; the smoke scene uses Eevee. Geometry, lights, cameras and materials are defined in source.
The browser replays checked-in 1280×720 frames at fixed times, then adds editable film typography, synthesised sound and the normal MP4/GIF delivery checks. Blender is needed to change/rebuild the 3D simulation, not to watch the films or export their supplied frame assets. Browser verification compares the final composite across independent browsers; it does not rerun or validate fluid physics.
npm run render:fluid-films # use the checked-in frames
python3 tools/blender/build.py overflow --replace # optional Blender rebuildSee scene recipes, bake controls and provenance.
fluid-overflow and fluid-smoke are two technique families; fluid-viscous
is a variant of fluid-overflow. The three finished films are not counted again
as extra effects. The earlier procedural water and smoke studies remain available
for work that needs fast browser-side changes.
One flight. Twenty worlds. · Dot battle · Same fight. Eight treatments. · All fourteen effect settings · Asset and technique guide
Fourteen additional demos cover a flying character, cross-frame style changes, twenty art directions, a gallery camera journey, low-poly flight, footage-to-dots, beat-controlled styles, dot impact/recovery, colored time echoes, source-time remapping, 2.5D relief, animated skinned point clouds, freeze-orbit motion and particle depth of field. The twenty art directions are presets, not twenty extra effect registrations. The art-direction film runs for 40.8 seconds; the rebuilt COUNTERFORM dot battle runs for 19.2 seconds.
The battle film uses original illustrated fighters with authored joint poses, root travel, planted steps, fixed-length limbs, counterattacks and short contact holds. Dodges have whooshes without false impact cues; particles originate at actual contacts. The brief held shot uses a 2D camera push. The eight related dot demos use volumetric Amber/Teal characters baked from the same choreography, including separate attack and defence clips. The real 3D orbit holds their contact pose. The 38.4-second treatment comparison gives each operation 4.8 seconds and places its reference beside the result. The flying courier and poster artwork are also original. The 2D dot technique demos sample a checked-in RGBA sequence; the true 3D demos load an animated GLB and keep surface samples attached to the deforming skeleton. A full camera orbit uses that actual geometry. Luminance-derived 2.5D relief is explicitly labeled and does not claim to reconstruct hidden anatomy. These films demonstrate the techniques rather than duplicating the reference posts' characters or footage.
npm run build:studio-assets # rebuild GLB, verified source clip and RGBA frames
npm run build:studio # all three films, with sound, posters and GIFsThe checked-in assets make the first command optional for normal film builds.
Use video/import-clip.py for your own footage. The implementation extends the
existing Three.js stack with GLTFLoader, GLTFExporter, SkeletonUtils and
BufferGeometryUtils, plus AnimationMixer and a stable skin-aware surface sampler.
No Blender, AE, image generator or depth model is required to reproduce the
shipped demos. Source paths, frame APIs, presets and limitations are documented
in the studio guide.
Optical film · Optical player · Image distortion, collisions and particles · Three-effect timeline · Effect settings
The six optical effects are moiré interference, slit-scan type, a folding paper
ribbon, caustic light, holographic foil and arbitrary-path morphing. Load Flubber
0.4.2, fx/optical-effects.js and fx/pack-optical.js after fx/demos.js to use
optical-moire, optical-slit, optical-ribbon, optical-caustic, optical-foil
and optical-morph. Caustic light and foil are visual approximations; the ribbon
is projected geometry rather than cloth physics. Flubber handles outer contours,
without holes.
The three additional effects use PixiJS 8.22.0 for a filtered poster, Rapier
0.21.0 for 48 colliding dominoes, and three.js r180 / TSL for 65,536 GPU-computed
particles. All three implement await frame(t) and participate in the same
verified video pipeline as other effects. Physics and particle state advances
in fixed 120 Hz steps; seeking backward or changing simulation parameters resets
and replays the initial state. Camera motion, attraction and dispersion are
scripted from time. No pointer input, wall-clock recording, or prerecorded frame
atlas is used for film export.
Use the pinned import map from examples/stacks.html, then load
fx/stack-effects.js and fx/pack-stacks.js for gallery IDs stack-pixi,
stack-rapier, and stack-gpu. The r180 modules coexist with the original r128
effects. See the frame API and settings.
bash video/build.sh examples/optical.html
npm run render:stacks
npm run build:showcaseThe last command renders the three material films, all three studio films, the three-effect film, optical film and intro, each with audio, every-frame comparison, delivery checks, posters and GIFs. The recorder automatically serves repository pages over loopback HTTP for ES modules. Pinned CDN dependencies require internet access. The WebGPU film needs a real supported adapter; missing or lost devices fail the render without a WebGL substitute. The parameter preview page is an authoring aid; viewers receive an ordinary MP4.
Run REQUIRE_WEBGPU=1 npm run test:effects on a supported GPU to require actual
compute-buffer checks. Tests cover optical seeks and audio, all three new frame
APIs, backward simulation seeks, gallery registration, native intro rendering,
asynchronous screenshot ordering, rejection cleanup, all fourteen studio demos, source-frame indexing, alpha, stable point identities and independent pose/camera clocks. Gallery checks render all 97 IDs, verify undistorted 16:9 frames, filter characters and enforce the technique/variant/showcase mapping. Failure tests preserve existing films and asset bundles; the rebuilt battle is checked for independent instances and repeatable attack frames. Python 3, ffmpeg and
Chromium are needed. There is no CI yet.
When a showreel does something this library cannot, we rebuild the effect from scratch in this repo's code, with our own content and character, and the tile names the video under "Seen in". No code or artwork was taken from these videos.
| Video, on Threads | What we rebuilt | Original demo IDs |
|---|---|---|
| A TixFox promo by Berlin (@ox8erlin) | A character rendered in flat colour and light, then dithered to three inks with a 4×4 Bayer matrix, and used across a whole video: standing, holding up cards, peeking over a logo, narrating from a badge | 4 |
| Two motion-design reels in one post by @designer.riven | Squash and stretch with its graph, variable-font type, polar morph, Bauhaus rhythm, ridgelines, code rain, card flip, data chart, ray-marched clay, domain repetition, five shader styles, GPU particles, reaction–diffusion, rigid bodies | 14 |
Other effects in those videos were already here and were not added twice: liquid glass, glow orbs, particles that form words, a chrome logo, the highlighter, scan and check, and whip-in letters. Of those 18 original demo IDs, only the rigid bodies needed a new library. They now follow the technique/variant/showcase grouping above; these historical totals are not extra gallery cards.
Detected rendering discrepancies or failed delivery checks stop publication and preserve the previous bundle. These checks establish repeatability and delivery constraints; 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, because they do not produce the same pixels. - 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. GPU edge noise stays well under both limits; a missing line of text or a frame from the wrong time does not. On a mismatch the recorder exits with code 3, keeps the frames in
video/mismatch/, andbuild.shrenders again, up to 5 times. - Both renders must hold every frame. The audio must reach −14 LUFS by a linear gain after limiting, measured on the final file with its true peak. 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. - Files are made and checked in a scratch folder and moved into
media/only when every check has passed.
| 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 |
Workflow and licence references checked on 2026-10-03: Remotion licence, HyperFrames documentation and licence, and Adobe scripting documentation.
Parallel renders used to disagree now and then; about half of the intro renders were rejected. The first cause was SVG text. Chromium lays SVG text out for its on-screen size and did not always lay it out again when an ancestor's CSS transform changed, so a label laid out before a pop (which starts at scale 0.6) kept that size in one browser and not in another. The one earlier case we measured, a label at 95 units instead of 158, is 0.60 of its size. Rendering 10.0 s and then 11.4 s of the intro as it was in commit 83b2727, in one browser, reproduced it every time. video/engine.js now sets text-rendering: geometricPrecision on SVG text in the stage; that reproduction then matches a fresh render, and the next intro render verified on its first attempt.
The second was a one-pixel column at the edge of a tile while it scaled in: frame 326 of that intro in 2 renders of 3, then frames 1258 and 1264 of the next intro in 5 renders of 5. Every tile holds a canvas or 3D content, which Chromium draws on a layer of its own. Our first fix, will-change: transform on such elements, passed three GPU renders but moved the problem: on SwiftShader the SVG text inside a tile at rest then differed between the two renders (frames 1297 to 1365). Our reading, not confirmed, is that the layer is drawn at a scale that depends on what that browser drew before, as with the SVG text. So the engine no longer scales these elements at all: an element it would scale that holds a canvas, a video or a hosted tile slides and fades in instead. The intro then verified on its first attempt on the GPU, in a second GPU render, and on SwiftShader.
build.sh still renders again, up to 5 times, when two renders disagree, and refuses publication if they still disagree. A repeatable authored mistake can pass this comparison; the disagreeing frames stay in video/mismatch/. If you hit one, please open an issue with those frames.
- 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. On macOS these are Apple Color Emoji, Lucida Grande and PingFang, so a render on Linux or Windows looks slightly different.
- The main gallery uses three.js r128; the newer studio, material and compute modules pin r180. The gallery retains r128, which still ships the single-file UMD build and
examples/js(removed in r161 and r148). Upgrading means re-tuning every colour, light and shader. - The original Helvetiker subset is Latin-only. Solid CJK lettering uses 50 bundled outline glyphs; adding other characters requires rebuilding that asset.
- The reaction–diffusion tile gives the same pixels every time on one GPU, but we saw a different pattern on SwiftShader than on Metal. It runs 900 steps a second on half-float textures, so small rounding differences between the two backends are the likely cause. The recorder never mixes the two in one run.
- Automated browser regressions cover effect contracts, editable chart data, gallery framing, asynchronous recording, publication rollback, intro playback and all 22 retained media bundles. There is no CI yet; complete video builds and visual review are performed locally. Bundle metadata checks do not prove visual freshness by themselves.
- 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. It tells us people want more of it.
0BSD: use, copy, modify and distribute for any purpose, with or without attribution. The Helvetiker glyph subset and the adapted TSL example retain their notices; three.js, Matter.js, Flubber, PixiJS, Rapier and the fonts load from public CDNs under their own licences; see THIRD_PARTY.md.







