Motion graphics as a pure function of time. You write scenes as draw(ctx, t) in plain Canvas 2D. They render in headless Chrome with real motion blur, get encoded with ffmpeg, and are scored with sound synthesized in Python on the same beat grid. There's no timeline editor, no keyframes and no plugins, which makes it a good fit for coding agents: describe the video, review the storyboard, get an mp4.
Frames from examples/hello: kinetic type → dot field → 3D landscape → shape morph → spring UI → glass end card.
- Deterministic. Every frame depends only on
t, so any frame can be rendered, inspected and fixed in isolation. - Real motion blur. Each output frame averages 6 subframes across a 180° shutter, and hard cuts stay hard.
- Sound locked to picture.
audio/ftsynth.pybuilds drums, pads, UI foley, whooshes and risers from numpy on the same clock. Risers land exactly on the drop. Mastering targets -14 LUFS. - Built for agents.
prompts/VIDEO_BRIEF.mdis a complete brief-to-delivery prompt (research → concepts → beat-grid storyboard → build → visual QA → sound → render).CLAUDE.mdgives the house rules.
- Node.js 18+
- Python 3.10+ with
numpyandscipy(pip install -r requirements.txt) - ffmpeg on your
PATH - Google Chrome, Chromium or Microsoft Edge (auto-detected, or set
CHROME_PATH)
npm install
pip install -r requirements.txt
node ft.mjs preview examples/hello # live preview in your browser
node ft.mjs sheet examples/hello 12 # contact sheet → examples/hello/out/sheet.png
python examples/hello/sound.py # soundtrack → examples/hello/out/audio.wav
node ft.mjs render examples/hello # → examples/hello/out/hello.mp4Start your own:
node ft.mjs new launch-teaser # copies templates/blank → examples/launch-teaser
node ft.mjs preview examples/launch-teaser- Open this repo in Claude Code (or another coding agent).
- Paste
prompts/VIDEO_BRIEF.md(Turkish:VIDEO_BRIEF.tr.md), fill in the brief, and attach your logo, screenshots and website URL. - Pick a concept and approve the storyboard. The agent builds, checks its own frames, scores the sound and renders.
// examples/my-video/scene.js
import { prog, rgba, EASE, spring, setFont, TAU } from '../../engine/core.js';
export default {
draw(ctx, t, api) {
const { W, H } = api;
ctx.fillStyle = '#0c0c0e'; ctx.fillRect(0, 0, W, H);
const p = EASE.expo(prog(t, 0.1, 0.9)); // 0→1 between 0.1 s and 0.9 s
setFont(ctx, 700, 120, 'Inter', -4); ctx.textAlign = 'center';
ctx.save(); ctx.beginPath(); ctx.rect(0, H / 2 - 130, W, 175); ctx.clip();
ctx.fillStyle = '#f0f0ec'; ctx.fillText('Hello.', W / 2, H / 2 + 40 + (1 - p) * 150);
ctx.restore();
const s = spring(t - api.at(1), 16, 7); // springs in on bar 1
ctx.fillStyle = '#ff6a3d'; ctx.beginPath(); ctx.arc(W / 2, H / 2 + 160, 18 * s, 0, TAU); ctx.fill();
},
};project.json sets the canvas, timing and fonts:
{
"name": "my-video", "width": 1080, "height": 1080, "fps": 60, "duration": 15,
"bpm": 160, "speed": 1, "subframes": 6,
"fonts": { "css": ["@fontsource/inter/700.css"], "preload": ["700 40px Inter"] }
}bpmdefines the grid:api.at(bar, step)gives scene time, and at 160 BPM one bar is 1.5 s.speedstretches the whole choreography.0.75turns a 15 s cut into 20 s, and the sound follows.- Fonts: any
@fontsource/*package (npm i @fontsource/<font>), or CSS files inside the project folder.
| Command | What it does |
|---|---|
node ft.mjs new <name> |
scaffold examples/<name> |
node ft.mjs preview <project> |
live player: space play/pause, ←/→ frame, shift+←/→ second, b motion blur, &t=3.5 to freeze |
node ft.mjs stills <project> 0,90,2.5s |
full-size PNGs at frames or seconds |
node ft.mjs sheet <project> [n] |
n evenly spaced stills tiled into out/sheet.png |
node ft.mjs render <project> |
mp4 (H.264, yuv420p, faststart; muxes out/audio.wav if present) |
node ft.mjs anim <flags> |
the hand-painted character kit in animation/ (see its README) |
node ft.mjs lemo setup · lemo <command…> |
the vendored film-style library in lemo/ (see its README) |
Options: --lang xx (passed to the scene as api.lang), --sub N (motion-blur subframes), --crf N, --out name.mp4.
engine/core.js helpers + motion-blur runtime
engine/fx.js transitions, glitch, bloom, aberration, HUD, code window
engine/recipes.js tunnel, dot sphere, easing graph, ripple dots
engine/three.js three.js bridge: offscreen WebGL, cel shading, ink outlines, sweep tubes
engine/brand.js brand kit: palette from 3 colours, safe areas, text fitting, logo / monogram
engine/player.html loads a project's fonts + scene (preview and render)
ft.mjs CLI: static server, headless Chrome, ffmpeg
audio/ftsynth.py synthesis, timeline mixer, reverb, sidechain, mastering
audio/ftextras.py stamp, counter ticks, echo, glitch burst
templates/blank/ starting point for `new`
examples/hello/ reference scene + soundtrack
examples/reel/ 15 s showcase reel: wipes, glitch, tunnel, dot sphere, easing graph, typed code (TR / EN)
examples/edit-demo/ an edit request, its real git diff and the before / after renders side by side
examples/cat-crossing/ 3D cartoon short (three.js): a cat crossing a busy street
examples/chat-commerce/ conversational-commerce promo: chat demo, manifesto, inbox, dots → logo, glass end card
examples/motion-principles/ kinetic manifesto: code → dot landscape → timing, rhythm, contrast, squash, morph
examples/earthquake/ 30 s Turkish explainer Reel (how an earthquake happens), narrated, word-by-word captions
examples/dinosaur/ 30 s Turkish story Reel (the dinosaur extinction), narrated
examples/dotkesk/ shared kit of the dotkesk daily series: rebuilt logo, hero glass, captions, chat UI, end card, voice, sound
examples/dotkesk-NN/ the series' episodes (01–07): 9:16 English shorts for dotkesk.com
examples/locale-games/ 24 s landing-page showcase for X (16:9): the page played like a game, designer credits
animation/ second engine: hand-painted character animation (p5 + p5.brush, Clawd), from ClaudeAnimationBase (MIT)
lemo/ vendored Lemo-Opuscar library: 51 named film styles + production tools (MIT)
docs/TECHNIQUES.md recipes: timing, type, dot fields, morphs, UI, glass, impacts, sound
docs/sound-and-tempo.html interactive explainer: how the sound is synthesised, how motion locks to the beat (EN / TR)
prompts/ brief-to-video prompt templates (EN / TR)
A scene can build a three.js world in setup() and pose it from t in draw(); engine/three.js renders it into an offscreen WebGL canvas and blits it onto the frame, so motion blur, post() and 2D overlays keep working. import * as THREE from 'three' and three/addons/… resolve from node_modules through the player's import map. Headless renders use SwiftShader (software WebGL), which is deterministic but slow (roughly 0.5–1 s per subframe at 1080p), so lower subframes for heavy 3D projects. See examples/cat-crossing and the three.js section in docs/TECHNIQUES.md.
examples/chat-commerce and examples/motion-principles are 20-second promos (also templates in ft-studio). Everything a brand changes sits at the top of scene.js: the COPY dictionary (TR / EN, pick with --lang tr) and BRAND (three colours, display font, optional logo file). engine/brand.js turns those into a palette with guaranteed contrast, a safe area for any aspect ratio (change width / height in project.json for 9:16, 4:5 or 16:9) and a logo, with a monogram when there is no file. Both are choreographed on a 15 s clock and play at "speed": 0.75; set 1 for the original 15 s tempo or 0.6 for 25 s, and the soundtrack follows.
node ft.mjs sheet examples/chat-commerce 12 --lang tr
python examples/chat-commerce/sound.py
node ft.mjs render examples/chat-commerce --lang trexamples/reel is a 15 s, 128 BPM reel that puts engine/fx.js and engine/recipes.js to work: nine scenes with skew / bars / disc wipes, a glitch tear, a tunnel that punches on the kick, a dot sphere, an easing-curve explainer, a rippling dot grid, kinetic type, and finally the reel's own source typing itself, rendering, and playing in a preview inside the preview. Copy is in COPY (--lang tr / --lang en), and the sound track (sound.py, using audio/ftextras.py) mirrors every camera hit.
How the sound is made and how the picture locks to the tempo is explained with live demos in docs/sound-and-tempo.html (English, with a Turkish toggle). Open it in a browser: a loop you can play and re-tempo, the kick, hat, pluck and riser formulas with sliders, the three mixer moves (sidechain, reverb, tanh drive), and the reel's real hit list scrubbed against three motion recipes. It is a single static file that needs no build step.
node ft.mjs sheet examples/reel 16 --lang en
python examples/reel/sound.py
node ft.mjs render examples/reel --lang enBecause a scene is code, a revision is a diff you can read. examples/edit-demo walks through one real round: the request "Speed the reel up to 150 BPM and change the accent colour from coral to electric blue. Keep the sound in sync.", the draft PR that answered it (saved as case/change.patch), and the before and after renders side by side. One number in project.json retimed the picture and the sound. The rest of the diff is what that did not reach on its own: seven helpers that default to the old colour, a hard-coded 128 BPM label, copy quoting 900 frames, and one sound event at an absolute second.
The 30 s side-by-side video is itself an ft-motion project (the panel is drawn from case/, and compose.mjs overlays the two renders). See its README to make one for your own edit.
examples/dotkesk-01 … dotkesk-07 are 19–25 s vertical (1080×1920) English shorts for Instagram Reels and YouTube Shorts, made for dotkesk.com, an AI agent for WhatsApp, Instagram and web-chat commerce. Each one shows one real feature from the site: answering at 2 a.m., order tracking, chat channels wired to ikas / Shopify / WooCommerce, setup in minutes, cart recovery with one-time codes, refunds with human handoff, and web chat. Each ends on the same card: the logo assembles, Follow @dotkesk is tapped, and a "Book a free demo" button.
Everything the episodes share lives in examples/dotkesk:
- Brand from the site, not invented. The colour tokens, Inter, the green pill CTA and the orbit logo are rebuilt from dotkesk.com. The logo is redrawn dash by dash from the PNG and checked against it with a difference overlay (
logo-check/). The background is the landing hero's shader (Swirl + ChromaFlow + FlutedGlass + FilmGrain), rebuilt for Canvas 2D; its green light follows the action instead of the cursor. Light (default) and dark looks are switched with"theme"inproject.json. - A free, local, consistent narrator.
voice_lib.pyclones one reference clip with VoxCPM2 (Apache-2.0) on the GPU, makes three takes per line, and keeps the take that Whisper hears word for word. It then lays the lines on the timeline and writesschedule.js; scenes, captions and sound all key off those word times. - Highs kept in check.
sfx.pyuses sine-based, low-passed UI sounds and dark hats. It de-esses the voice, ducks the music under every word, and masters to -14 LUFS through a gentle high shelf. It prints the 5–10 kHz and >10 kHz energy share on every mix. - Never a still frame. Every section has a slow push-in, the glass keeps drifting, and the end card keeps orbiting.
- A new episode is mostly copy.
stage(),chatCard(),turnLogo(),payoff()andseries_score()carry the structure. An episode writes its lines, its middle scenes and its UI sounds.POSTS.mdholds a caption and the placeholder list for each one.
C:\vox\Scripts\python.exe examples/dotkesk-02/voice.py # a Python with torch (CUDA), voxcpm, transformers
python examples/dotkesk-02/sound.py
node ft.mjs render examples/dotkesk-02The series keeps a 5 % film grain to match the site's hero, against the tip below. That costs about 40 % in file size (13–17 MB per episode).
examples/locale-games is a 24 s, 16:9 video for X and a UI/UX portfolio. It presents the Locale Games landing page, designed by Şimal Su Erdoğan. The page is rebuilt in code inside a browser window, so every headline, card and field can move on its own. It is played like a game: PRESS START, the hook's two glowing eyes become the hero robot's eyes, each section is a level with a HUD (level, progress, score), the cards land on eighth notes for +100 each, a short design-system beat shows the tokens, the contact form fills and sends, and the footer's sunset ends on LEVEL COMPLETE and the designer's credits.
python examples/locale-games/sound.py
node ft.mjs render examples/locale-gamesThe images in its assets/ folder belong to the designer and are not covered by the MIT license.
- Leave film grain out. It multiplies file size and every platform re-encodes it into blotches.
- Square (1080×1080) reads best in phone feeds. For Reels/Stories use 1080×1920 and keep about 250 px clear at the top and bottom.
- On Windows, keep the repo in a short path. Very long paths break some git and ffmpeg operations.
MIT (see LICENSE). Fonts are distributed under their own licenses (Inter, JetBrains Mono, Barlow Condensed and Exo 2: SIL Open Font License). The images in examples/locale-games/assets/ are the designer's work and are not covered by the MIT license.




