tiltshot
v0.2.0
Published
Cinematic 3D launch trailers from app screenshots: a studio, a CLI and an MCP server for AI agents, on one engine.
Maintainers
Readme
Tiltshot
Cinematic launch trailers from app screenshots. Every screen sits in one 3D world. A camera glides across each screen with real depth of field, then flies, whips or pushes to the next one while keeping its momentum. Shots can have a moving cursor with click ripples, typed text, spotlights and captions. Motion layers (text, callouts, shapes, images) can float above the screens with keyframes and entrance presets, and UI sound is generated to match. This is the Lanemark trailer engine, rebuilt so it works for any app.
There are four ways to use it, all running the same engine (src/engine):
| | What it is | Use it when |
|---|---|---|
| Studio (npm run dev) | Web app: drop screenshots, pick camera moves, preview, export MP4 in the browser | Making a trailer by hand, or letting other people make theirs |
| CLI (tiltshot render) | Renders a project folder to MP4 or PNG stills in headless Chrome | Scripted or repeatable renders, CI, re-rendering after a UI change |
| MCP server (mcp/server.mjs) | Tools for AI agents: capture an app, build a storyboard, review frames, render | Pointing Claude Code or another agent at your app and asking for a trailer |
| Engine (src/engine) | Framework-free TypeScript: Renderer, exportMp4, the project schema | Embedding trailer rendering in your own product |
Quick start
npm install
npm run dev # studio at http://localhost:5173 (opens with a sample project)Online: npm run build:online builds a static copy in dist-online/ for any static host (it's
also published as a claude.ai artifact). Online, videos and project files save through the viewer's save
prompt; adding screens by web address and render servers need the studio running locally.
Add from a web address: the box under "Add screens" captures a live page by its URL, already split into components (via the dev server, or a render server set under Export). Pages behind a login: use the capture extension on that tab.
Open a project folder served next to the studio with ?project=<folder>, e.g.
http://localhost:5173/?project=/out/maintax-demo/ (a folder made by the MCP server or the CLI works as is).
demos/tiltshot-trailer.mp4 is Tiltshot's own trailer, made this way: the studio captured with capture_screen,
then cut, built and rendered over MCP.
Exporting needs a browser with WebCodecs (Chrome or Edge; recent Safari mostly works). Keep the tab in front while it renders, because browsers slow down background tabs.
Studio
- Scenes (left): drop screenshots to create shots, add title cards, drag to reorder.
- Preview (centre): live WebGL render. Space plays or pauses, ←/→ step a frame (add Shift for a second), ⌘Z/⇧⌘Z undo and redo.
- Timeline (bottom): scrub, click a block to select it, drag a block's right edge to change its duration.
- Inspector (right), for a shot:
- Camera: pick a move (Tilted glide is the Lanemark look), then click the screenshot to aim key A and key B. You can add keys at the playhead and set zoom, tilt, roll, swing and easing for each one.
- Cursor: click to add stops. Each stop clicks by default, and the clicks get a ripple and a sound.
- Focus: drag a box to spotlight part of the screen. Everything else dims.
- Type: drag a box over an input field and the text types into it. Its colours match the screenshot.
- Layers: click the screenshot to place the selected motion layer.
- Motion layers: add text, callouts (pill, badge, tooltip, label), shapes (box, circle, line, arrow) or images. Pin each one to the screen, where it tilts with the screen and can float above it (Lift), or to the frame. Each layer has an entrance and an exit (rise, mask reveal, pop, blur, zoom, slide, swing, wipe, typewriter). Text can animate per letter, word or line, with a stagger.
- Caption, browser-window frame and transition.
- Project (gear icon): aspect (16:9, 9:16, 1:1, 4:5), the World layout (side by side, stacked in depth, curved wall, grid or scattered), theme colours, font, depth of field, vignette, grain, cursor style, music and UI sound.
Keyframe timeline
Press T (or Keyframes under the preview) to open the selected scene's timeline. Everything timed in the scene sits on one ruler: camera keys, cursor stops, typing, spotlights, the caption and every motion layer. Drag any of them to retime it, drag a layer bar's ends to trim it, and click the ruler to scrub.
- Expand a layer to see its properties: position, lift, scale, rotation, 3D tilt, opacity and blur.
- The stopwatch turns animation on for a property. From then on, changing its value (drag the blue number, or use the inspector's sliders) sets a keyframe at the playhead. ◆ adds or removes a keyframe at the playhead.
- Click a keyframe to select it (Shift-click to add more), drag to move all selected keys, and press Delete to remove them. With keys selected, Curve in sets the easing into them: Linear, Smooth, Snap, Accelerate, Overshoot or Dramatic.
- Click a property name to open the graph editor. It shows the value curve, and keys can be dragged in time and value (Shift locks the value, Alt locks the time). Double-click to add a key. The bezier box shapes the curve into the selected key, overshoot included.
- Animate… applies a keyframe behaviour to the selected layer: Float, Pulse, Slow push, Drift, Spin in, Bounce in, 3D flip, Lift off or Focus pull. The result is ordinary keyframes you can edit.
Transitions
Every transition keeps the camera moving. Text and captions move with the transition but stay out of the camera blur.
| | | |---|---| | Fly through | The camera travels through the 3D world from one screen to the next, pulling back mid-flight so both are in view. Needs a screen on both sides; next to a title card it becomes a push. | | Whip pan | A fast pan in the direction the outgoing shot was already drifting, with a cut at the blur peak. | | Push through | Rushes forward into the cut and keeps pushing into the next scene, with zoom blur. | | Flash | A light flash and a lens leak at the cut. | | Blur, fade, black | Dissolves, with the camera still drifting through them. |
Customisation
- Screen recordings. Drop an MP4, WebM or MOV in as a shot and it plays inside the 3D screen. Under Screen, Starts at trims the clip, Speed changes playback speed, and Fit shot to clip sets the length. If the shot runs longer than the clip, the last frame holds.
- Device frames (per shot, under Screen): browser (matching the screen, dark or light), app window, MacBook, iPhone and iPad. Tall screenshots get the iPhone automatically. Devices are framed so the whole device fits at zoom 1.
- Screens (project): corner radius, hairline border, shadow and an accent glow.
- Background (project): soft glow, solid, gradient (with angle), mesh gradient, a 3D grid floor that sits in the world and moves with the camera, or your own image (blurred and drifting). Colours come from the theme unless you set your own.
- Brand kit (project): upload a logo (it goes on the title cards), upload a font file (TTF, OTF, WOFF, WOFF2), and pull colours from the logo or any screen. Click a swatch to make it the accent, or apply a whole dark or light look.
- Templates (File → New from template): Keynote (the flagship, below), Product launch, Feature drop (9:16), Teaser and Changelog. Pick screens, write a line per feature, build. Screens, music, logo, font and colours carry over, and Undo restores the previous trailer.
- Beat sync (project → Sound): Detect beat finds the music's tempo and beats, which show as ticks on the timeline (bar lines are taller). Fit scenes to the beat moves every cut to the nearest beat, two beats, bar or two bars, and dragging a scene's edge snaps to beats. Templates built on music with a detected beat are fitted straight away.
Keynote: the flagship template
Built in the language of Linear and Apple launch films. The design rules are in the template, so every trailer made with it follows them:
- Structure: a hook in big type, then the first screen revealed on the music's drop, assembling itself. Next comes a close-up you can read, where the hero card lifts towards the camera and its line sits on a soft scrim. Then, for each further screen, a type card followed by that screen building a different way (rise, flip, pop). The last screen's primary button gets clicked, and the film ends on the name, line and button.
- Type: huge, tightly tracked headlines, a white-to-grey gradient fill, word-by-word mask reveals and a slow push-in. Headlines wrap balanced, so lines don't end in a single orphaned word.
- Restraint: cuts on the beat between type and screens, a short blur from screens into type, and a different deliberate camera move per shot.
- Light: near-black, cool space, and a soft band of light gliding across each screen with a lit top edge (
fx.sheen). Lifted pieces cast soft shadows; bare text never casts a box shadow. - Music: detect the beat first and the template starts the track so its drop (the biggest lift into a loud section) lands on the first reveal. Every cut is then fitted to the beat, counted from the drop.
- Focus: give each screen a focus point (the hero card, the key number, the main button). The close-up aims there, the card under it lifts, and on the last screen it's what gets clicked.
Screen components ("build the screen")
A screen can be broken into its components (sidebar, header, cards, panels, buttons, inputs, images and blocks of text), each cut out as its own transparent image. Each one becomes a 3D layer pinned to the screen, so the screen can build itself up piece by piece, come apart into an exploded view, or have any piece animated on its own.
- From an HTML file: drop a
.htmlfile in as a shot. It's rendered in the browser and cut exactly along its elements, with real transparency, shadows and gradients. Scripts don't run, so use a static export of the page; for live apps, capture through the CLI or MCP (below), which runs real Chrome. - From a screenshot: select the shot, open the Pieces tool and click Break into components. Pieces are found from the pixels: page colour, filled or bordered containers, then the blocks inside them. This works best on flat UI. Drag on the screen to mark a piece it missed; select a piece and click the bin to merge it back into what surrounds it.
- Build: Drop in (falls onto the screen and bounces; big pieces from lower down), Rise, Pop, Flip up (hinged at the top), Fly in, Slide, Hero lift (the chosen pieces and everything inside them rise towards the camera, hold and settle), Exploded view (the finished screen separates into floating layers by depth, then settles), or Static. The order can be containers first, top to bottom, bottom to top, from the centre, or random; a piece never lands before the thing it sits in. By default the camera frames the whole screen while it builds.
- Every piece is an ordinary image layer with keyframes (grouped under Screen pieces in Layers), so Animate…, the keyframe timeline and the graph editor all work on it. Effects on a selected piece jumps straight there.
- Lifted pieces cast a soft shadow on the screen that spreads as they rise. Underneath, the plate (the screen with the pieces taken out) shows the empty shell.
Projects autosave in the browser (IndexedDB). File → Save project file downloads a .tiltshot.zip
(project.json + assets/). The CLI renders that folder layout directly.
CLI
node cli/tiltshot.mjs init my-trailer # starter project.json + screens/
node cli/tiltshot.mjs render my-trailer # → my-trailer/out/trailer.mp4 (1080p60)
node cli/tiltshot.mjs render my-trailer -o launch.mp4 --res 4k --fps 60 --motion-blur 4
node cli/tiltshot.mjs render my-trailer --still 1.5,6,12 # PNG frames in my-trailer/out/stills/It starts a Vite server and drives your installed Chrome through Puppeteer (set CHROME_PATH to use another).
--res takes 720, 1080, 1440 or 4k. --motion-blur averages N sub-frames per frame
(4 looks smooth and takes about 4× as long).
MCP server (for AI agents)
Needs Node 18.17+ and Google Chrome. Register it with Claude Code (or any MCP client), then ask for a trailer:
claude mcp add --scope user tiltshot -- npx -y tiltshot mcpClaude Desktop, Cursor and other MCP apps take the same command in their MCP settings:
{ "mcpServers": { "tiltshot": { "command": "npx", "args": ["-y", "tiltshot", "mcp"] } } }From a clone instead: node /absolute/path/to/tiltshot/mcp/server.mjs. The studio on its own: npx tiltshot studio.
"Make a 30-second launch trailer for the app running on localhost:3000. Use the tiltshot tools."
The server also ships a launch-trailer prompt with the recommended workflow. Project paths are relative to the
server's working directory, or to TILTSHOT_PROJECTS if that's set.
| Tool | What it does |
|---|---|
| create_project | New project folder |
| open_in_studio | Opens the project in the studio, live (starts the studio if it isn't running), so you can watch the trailer come together |
| capture_screen | Screenshots a URL or a local HTML file, optionally after clicks, typing or scrolling, and records every visible element's text and position. components: true also cuts the page into its components along the DOM |
| add_screenshot | Adds an image or a screen recording (MP4/WebM/MOV) as a screen, logo or image |
| find_elements | Searches a captured screen's elements |
| break_down_screen | Splits a screenshot into its components from the pixels and returns them with a labelled preview |
| build_screen | Turns a shot's screen into 3D component layers that drop, rise, pop, flip or fly into place (or an exploded view), and returns each piece's layer id for animate_layer |
| apply_storyboard | Builds a whole trailer from an intro, beats and an outro. Beats can be asset ids, image files or URLs |
| add_shot / add_title | Adds one scene. Shots take intent rather than coordinates: cursor: ["Send quote"], typing: [{target: "Customer", text: "Acme"}], spotlights, callouts. Timing, camera framing and duration are worked out automatically |
| add_layer | Adds a text, callout, shape or image layer. Any transform value can be keyframed |
| animate_layer | Applies a keyframe behaviour (float, pulse, spin-in…) or explicit keyframes with eases or bezier curves |
| apply_template | Builds a whole trailer from a template (keynote, launch, feature-drop, teaser, changelog) with your screens, copy and a focus point per screen |
| set_brand | Logo, a brand font file, and theme colours pulled from the logo or a screen. Returns the palette |
| set_style | Theme, screen styling (radius, border, shadow, glow), background (glow, solid, gradient, mesh, grid, image), lens, world layout, sound |
| set_music / analyze_beats / fit_to_beat | Adds music, detects its tempo and beats, and moves every cut onto the beat |
| update_scene | Fine control over any scene field, including frame, clipStart and clipRate |
| remove_scene / move_scene | Edit the scene order |
| validate_project | Finds problems: missing files, events after a shot ends, captions that are too long or start too late, things placed off-screen |
| render_frames | Returns one contact-sheet image of the key moments (or specific times), so the agent can check its work |
| render_video | Writes the MP4 and reports progress |
Watching it live
open_in_studio opens the studio on the AI's project with ?live=<folder>. A green Live with AI
badge in the top bar shows it's following the folder:
- Whatever the AI changes appears in the studio straight away, without a reload. The view stays where you were, and the badge flashes Updated by AI.
- What you change in the studio is saved back to the folder (new screenshots and cut-out pieces
included), so the AI's next
get_projectsees your edits. If both change it at once, the AI's version wins and yours is one undo away. - File → Projects made by AI… lists the projects in
TILTSHOT_PROJECTS(default~/Documents/Tiltshot Projects) to follow, or stops following.
This needs the studio running on your computer (npm run dev), which serves the folder to the studio on
the same machine only. npm run test:live checks the whole loop with a real MCP client.
Wherever a tool takes a target, you can give element text ("Export"), a CSS selector from the capture, or
{"x": 0..1, "y": 0..1}. npm run smoke:mcp runs the full agent workflow against a running dev server.
Capture extension
extension/ is a Chrome extension (Manifest V3) that captures the tab you're on, including apps
you're logged into, straight into the studio, already split into its components.
From the studio: click Chrome extension in the top bar. It downloads the extension, walks through the install (Chrome only installs from its Web Store in one click, so it's three quick steps by hand) and shows Installed once the extension is there.
By hand:
npm run build:extension(buildsextension/dom-parts.js, the component finder, and packspublic/tiltshot-extension.zip, the studio's download).- In Chrome:
chrome://extensions→ Developer mode → Load unpacked → pick theextensionfolder. - With the studio open (default
http://localhost:5173/; change it in the extension's options), click the extension on any page and Capture this tab, or press ⌥⇧T.
It takes a full-resolution screenshot, finds the components in the page's DOM, takes a second
screenshot with them hidden (the clean plate under the pieces), puts the page back exactly as it
was, and hands everything to the studio, which cuts the pieces and adds a shot ready to build.
It only gets access to a page when you click it or press the shortcut (activeTab); it can reach
your studio's address and nothing else. A studio on a remote address asks for permission once.
npm run test:extension runs it end to end in headless Chrome.
Formats and render servers
One project, every format. Export → Formats renders the same project as 16:9, 9:16, 1:1 and/or
4:5. Scenes, the camera and title cards already adapt to the frame; type, callouts, scrims and
images pinned to the frame are resized for its shape (adaptAspect in the engine). From the CLI:
tiltshot render ./project --formats 16:9,9:16,1:1; over MCP: render_video with formats.
Render on a server. Browsers render in the foreground tab. To render elsewhere, run
tiltshot serve --port 8787 --token SECRETon any machine, or build the Dockerfile for a cloud host, then choose Export → Render on → A render
server and enter its address and token. The studio uploads the project file, the server renders every
format asked for (one job at a time) and the studio downloads the MP4s. API: POST /jobs?formats=…
with the project zip, GET /jobs/:id for progress, GET /jobs/:id/<file>.mp4 for the video
(cli/server.mjs documents it). Without a GPU the server renders WebGL in software (the Docker
image does): it works, but about 8× slower than a GPU (≈2 s vs 0.25 s per 720p frame), so for
production rendering run it on a machine with a GPU (a Mac or PC, or a cloud GPU instance).
Project format
A project is plain JSON. Positions on a screenshot are 0..1 from its top left. Times are seconds from the start of the scene.
{
"name": "My launch",
"aspect": "16:9",
"theme": { "background": "#050505", "text": "#F2F2F2", "muted": "#8E8E8E", "accent": "#6395FF", "font": "inter", "glow": 0.35, "logo": null },
"screen": { "radius": 14, "border": 1, "shadow": 1, "glow": 0 },
"background": { "type": "mesh", "colors": [], "intensity": 0.6, "motion": 0.5, "asset": null, "angle": 160 },
"fx": { "dof": 1, "vignette": 0.8, "grain": 0.6, "fadeOut": 0.8, "cursorScale": 1, "cursorStyle": "dark" },
"audio": { "music": "song", "volume": 0.8, "offset": 0, "sfx": true, "sfxVolume": 0.6,
"beats": { "bpm": 120, "times": [0.02, 0.52, 1.02], "source": "song@0", "downbeat": 0 } },
"assets": {
"home": { "id": "home", "name": "home.png", "mime": "image/png", "width": 2880, "height": 1800, "src": "screens/home.png" },
"song": { "id": "song", "name": "track.mp3", "mime": "audio/mpeg", "width": 0, "height": 0, "src": "track.mp3" }
},
"scenes": [
{ "type": "title", "duration": 3, "title": "Your product", "sub": "The one-line promise.", "logo": null, "backdrop": "home",
"transition": { "type": "fade", "duration": 0.8 } },
{
"type": "shot", "duration": 5, "screen": "home", "frame": "browser", "url": "app.example.com",
"camera": [ // "at" is 0..1 across the shot, transitions included
{ "at": 0, "x": 0.3, "y": 0.3, "zoom": 1.55, "tilt": 43, "yaw": 0, "roll": -25 },
{ "at": 1, "x": 0.6, "y": 0.55, "zoom": 1.7, "tilt": 38, "yaw": 0, "roll": -19, "ease": "inOut" }
],
"cursor": [{ "t": 0.8, "x": 0.62, "y": 0.6 }, { "t": 2.2, "x": 0.42, "y": 0.38, "click": true }],
"highlights": [{ "id": "h1", "start": 2.4, "end": 4.4, "x": 0.18, "y": 0.32, "w": 0.56, "h": 0.35 }],
"typing": [{ "id": "t1", "start": 1, "x": 0.2, "y": 0.22, "w": 0.24, "h": 0.04, "text": "Acme Ltd", "speed": 16,
"background": "auto", "color": "auto", "align": "left" }],
"caption": { "text": "Everything in one place.", "sub": "", "position": "bottom-left", "start": 0.5, "animation": "rise" },
"layers": [
{ "type": "callout", "space": "screen", "text": "+18% this month", "style": "pill", "icon": "up", "color": "#3DD68C",
"start": 1.1, "end": 4.5, "transform": { "x": 0.31, "y": 0.16, "z": 70 }, "in": { "preset": "pop", "duration": 0.55 } },
{ "type": "text", "space": "frame", "text": "Built for speed", "size": 96, "start": 0.4, "end": 3,
"transform": { "x": 0.5, "y": [{ "t": 0, "v": 0.55 }, { "t": 2.5, "v": 0.45, "ease": [0.16, 1, 0.3, 1] }] },
"in": { "preset": "mask-up", "duration": 0.8, "split": "word", "stagger": 0.07 } }
],
"transition": { "type": "fly", "duration": 1.2 }
}
],
"world": { "layout": "row", "spacing": 0.4, "showAll": true }
}- Transitions:
fly,whip,push,flash,blur,fade,black,cut. Each scene's transition plays into it. On the first scene it means a fade from black. - Layers:
text,callout,shapeorimage, inscreenspace (x/y are 0..1 of the screenshot andzlifts the layer off it) orframespace (x/y are 0..1 of the frame). Any number intransformcan be a keyframe list[{ t, v, ease }], whereeaseis a named ease or a cubic-bezier[x1, y1, x2, y2]. - Eases:
linear,inOut,out,in,outExpo,inOutExpo. - Fonts:
inter,manrope,space-grotesk,sora,dm-sans,instrument-serif(bundled, no network needed), orcustom:<asset id>for a font file in the project. - Shots:
frameisnone,browser,browser-dark,browser-light,window,macbook,iphoneoripad. A shot whosescreenis a video also takesclipStart(seconds into the clip) andclipRate(playback speed). - Broken-down screens carry
parts: { method, plate, items: [{ id, name, role, x, y, w, h, asset, depth, parent }] }on the screen's asset; the piece images are assets withofset to the screen. A built shot hasplateset and one image layer per piece (part= the piece id), plusbuildwith the settings used. - Text layers take
fill2(a gradient to a second colour) and shapes takefill2too (#RRGGBBAA works, for scrims).fx.sheen(0..1) is the light sweep across screens.audio.beats.dropis where the music lifts most. audio.beatsis written by beat detection; it's ignored oncesource(music asset + offset) no longer matches.- Missing fields are filled with defaults (
normalizeProject), so hand-written files can stay short.
Using the engine in your own app
import { Renderer, exportMp4, normalizeProject } from "./src/engine"; // or dist-engine after `npm run build:engine`
import "./src/fonts"; // makes the bundled fonts available to the canvas
const project = normalizeProject(json);
// live preview
const r = new Renderer(canvas, { scale: 0.5, onAsset: () => r.render(t) });
r.setProject(project);
await r.ready();
r.render(2.4);
// MP4 (H.264 + AAC) as a Blob
const { blob } = await exportMp4(project, { scale: 1, fps: 60, motionBlur: 4, onProgress: console.log });To load assets from somewhere other than asset.src, pass loadImage(asset) to Renderer and
loadImage / loadAudio to exportMp4.
How it works
timeline.ts: scene layout, transition overlaps, camera and cursor interpolation, sound events.world.ts: where every screen sits in 3D, plus camera poses in world space. A shot's keys use the original trailer's CSS-style camera (focus point, zoom,rotateX · rotateY · rotateZ, 2400px perspective) relative to its own screen. Fly transitions interpolate the world-space pose with a cubic Hermite curve, so velocity matches at both ends, and slerp the rotation.motion.ts+layers.ts: keyframes with cubic-bezier easing, entrance and exit presets, and texture atlases that pack each letter, word or line in its own cell so it can animate independently.renderer.ts+shaders.ts: WebGL2. Screens are drawn far to near, with depth of field from a packed depth buffer. Text and UI render into a separate buffer per scene. The final pass composites the two scenes of a transition and applies per-scene camera-motion blur (directional and zoom), chromatic aberration at speed, flash, light leak, vignette and grain. Export motion blur averages sub-frames.overlay.ts: titles, captions, cursor sprite, typed text and browser frame, drawn on a 2D canvas.frames.ts: device frames (browser, window, MacBook, iPhone, iPad), drawn procedurally around the screen texture.palette.ts: colour quantisation of a logo or screen, with dark and light theme suggestions.beats.ts: beat tracking (spectral flux in log bands, autocorrelation tempo, dynamic-programming beats) and beat fitting.templates.ts: whole-trailer templates built from screens and a few lines of copy.segment.ts/cutout.ts: finding components in a screenshot's pixels and cutting them out;domParts.ts: finding them in a page's DOM (runs inside the page);build.ts: the build animations.audio.ts: synthesised clicks, keystrokes and whooshes placed from the timeline, mixed with your music in anOfflineAudioContext. Preview and export play the same mix.export.ts: renders each frame, encodes it with WebCodecs and muxes withmp4-muxer.
Scripts
npm run dev/npm run build: studionpm run build:engine:dist-engine/tiltshot-engine.jsnpm run samples: regenerate the sample app screenshotsnode scripts/bench.mjs [out] [--format=webp] [--dpr=1] [--only=name]: the segmentation benchmark. Renders 28 synthetic pages (13 layouts × light/dark × bordered/shadowed/filled cards, frombench/pages.mjs), takes every tagged component as ground truth, and scores the screenshot segmenter and the DOM segmenter on them, with overlays of what was found, missed and extranpm run test:extension: loads the capture extension into headless Chrome, captures a page and checks the studio adds it in piecesnode scripts/server-test.mjs [out]: starts a render server, sends the sample project, renders 16:9 and 9:16, downloads themnode scripts/bench-seg.mjs <out> <image>...: run the screenshot segmenter on real screenshots and save annotated overlaysnpm run smoke: with the dev server running, loads the studio in headless Chrome and exports the samplenpm run mcp: start the MCP server on stdionpm run smoke:mcp: with the dev server running, drives the MCP server through a full capture → storyboard → render runnode scripts/mcp-build.mjs <dir> [page.html] [--video]: breaks a screenshot and an HTML page into components over MCP, builds both screens and renders framesnode scripts/mcp-customise.mjs <dir> [--recording f.mp4] [--music f.m4a] [--font f.ttf] [--video]: drives the customisation tools (brand kit, template, frames, background, beat sync) over MCPnode cli/tiltshot.mjs render <project> --sheet: contact sheet of the whole trailer
