SVGMotion help
Prompt → editable SVG scene → per-layer actions → state machine → saveable, scriptable, embeddable. This page documents every feature with a worked example. There's also an in-app guided tour (☰ Menu → ▶ Vector tour) that builds the lighthouse below live while explaining each feature — each studio's tour is its own (Character Studio: ▶ Studio tour — basics / Full studio tour; Asset Studio: ▶ Tour in the header). Building characters? See the Character Studio guide.
Quickstart
npm run dev # → http://localhost:4173 (static files only) ollama serve # optional: local AI provider ollama pull qwen3:8b # default model
AI is BYOK — the browser calls your provider directly; no server is involved. Configure it in ☰ Menu → Settings… → AI provider. No provider? Everything except AI generation still works fully offline — including Sidekick's instant edit verbs.
| Provider | Default endpoint | Default model |
|---|---|---|
| Ollama (local) | http://127.0.0.1:11434 | qwen3:8b |
| OpenRouter | https://openrouter.ai/api/v1 | anthropic/claude-3.5-sonnet |
| OpenAI | https://api.openai.com/v1 | gpt-4o-mini |
PORT env | 4173 | Dev-server listen port |
Core concepts
| Term | Meaning |
|---|---|
| Scene | The whole artwork — SVG markup + layers + actions + transitions + ambient CSS. Saved as one self-contained .svg. |
| Layer | A top-level <g> grouping shapes. The unit of selection, posing, and animation. |
| Action | A named target pose for layers (e.g. spin, hover) — callable like a function: SVGMotion.call('spin'). |
| Pose | A layer's animated properties in an action: x, y, rotation, scale, opacity + pivot px, py. Applied on top of the base transform. |
| Keyframe | A pose stamped at a fraction t of an action's duration. Gold diamonds on the timeline. |
| Transition | An edge from →to fired by a trigger, with duration + easing. The runtime blends every layer from its live pose. |
| Trigger | What fires a transition: click, pointer enter/leave/down/up, auto, complete. |
| ⚛ Recipe | A physics spec (phys) on a layer pose — the solver bakes it into ordinary keyframes. Authoring-time only; no engine ships in exports. |
| Bake-then-play | The physics pipeline: a recipe is simulated once at authoring time and written as ordinary keyframes — every export just plays keys, so no physics engine ships (SMIL needs no JavaScript at all). |
| Idle | The rest state. Editing in idle changes the scene's base layout; editing in any other action records into that action. |
Sidekick — the AI bar
The floating bar at the bottom-center of the canvas is the app's one AI surface — no separate dock, no "which bot" choice. It's always visible (drag it anywhere by its ⠿ grip); the rail's ✦ AI button, /, or ⌘K expands its drawer and focuses the input. Type a scene idea and ↵ generates it; type an edit and it edits. The router decides internally.
Generate
Sends your prompt to the configured provider, which returns SVG markup + layers + actions + transitions + optional CSS — validated and repaired in-browser. If a scene is already loaded, the current scene is sent along so follow-ups modify it ("make the beam wider", "add a blink action") instead of replacing it.
✨ Enhance
Rewrites your rough idea into an SVG-generator-friendly prompt — the ✨ Enhance chip in the expanded drawer. It fills the input — never auto-submits — so you can edit, then send. While it runs, ✨ pulses — tap it again to stop. Or skip it entirely; generation works on raw prompts.
a tree✨ returns:
a tree with a brown trunk as a vertical rect and green leaves as a circle on top. on pointerenter the trunk rotates 5 degrees, on pointerleave it returns to idle, and on click the leaves scale up to 1.2 then back to normal.
Commands & edits
The same input runs scene commands: play sweep fires an action, make the ball red edits, draw a house generates. While a command runs the ↑ button becomes a blinking red ■ stop and the chat shows typing dots — tapping ■ cancels even mid-generation.
idle, sweep, glow. You type make it sweep → the AI calls sweep(), the beam rotates. Type draw a house → it routes to generation.
✦ Smart routing
The floating command bar at the bottom-center of the canvas is Sidekick — a single AI input, so you never choose which "bot" to talk to. Type anything and it routes internally: common verbs (make it blue, bigger, radius 40) run instantly with no model at all; scene edits and action calls go to the tool pipeline (the local WebGPU engine first when enabled, your provider otherwise); scene asks (draw a rocket, generate a bouncing ball) go to generation — all from the same box. The ✦ icon before ✨ Enhance is the escape hatch: tap it to pop the routing picker up — auto (default), local (edits stay on-device; generation is refused with a pointer), or cloud (skip the local engine) — plus links into provider and engine settings. Every result is tagged in the log with which tier answered — instant / local / provider. The router is scene-aware: it checks what actually exists before deciding, so "spin the dragon" when there's no dragon generates one instead of failing on a missing layer — and if an edit still lands on a missing target, it hands off to generation automatically. Select anything and a mint badge next to the input names the target (▣ ball, ● circle · ball, or 2 shapes — the placeholder echoes it); type make it blue, bigger, move it right, radius 40, spin it, or play drop and it edits the scene directly — shapes, not just whole layers. Common verbs run instantly — colors, size, move, rotate, fade, and geometry are parsed locally with no model call, so they work even with no AI configured; anything more complex routes to the model (local engine first, then your provider). Every edit is undoable and lands in the inspector immediately. Any reply that changed the scene carries a ↶ undo button right on it — one click reverts the whole run, even multi-step ones (e.g. a physics drop that left the object posed at the artboard edge). To keep the action but return objects to their rest pose, tap the idle action row instead. The ⌃ button expands the chat transcript — your commands as bubbles on the right, Sidekick's replies on the left with ✓/✗ and which tier answered (instant / local / provider) — click a bubble to reuse the command; the transcript scrolls for earlier exchanges; it auto-opens while a model call runs and collapses on Esc. While the input is empty, a strip of try suggestions floats above it — edit verbs when something's selected, scene ideas when not — and ✨ Enhance sits inside the field next to send. A status pill flashes the result — ✓ done / ✗ failed — and while a model call is in flight the ↑ button becomes a blinking red ■ stop that cancels the command and quietly reverts to ↑ (local generation is interrupted; provider requests are aborted on the wire — a stop leaves no trace). The ⠿ grip drags the bar anywhere on the stage. The ✦ icon is color-coded by which AI answers — mint for local (the in-browser WebGPU engine), lavender for provider, orange when nothing's configured — and its popover links into Settings where the local engine is enabled and loaded.
Chips
Mint chips are prompt suggestions. After generating, the row becomes recent scenes — click one to restore that generation from cache instantly.
⚙ Settings
☰ Menu → Settings… opens two tabs. AI provider: Ollama (local), OpenRouter, or OpenAI — set the server/base URL, model, and — for the cloud providers — the API key. BYOK: the key is stored in this browser's local storage and requests go straight from this page to the provider — nothing passes through a SVGMotion server. Test connection verifies it (a real 1-token call for cloud providers, which also checks the model exists) and fills the model dropdown. With no key saved, Sidekick's drawer shows a setup hint — its ✦ icon turns mint where WebGPU is available (the in-browser engine is on by default and downloads ~600MB on first use, then stays cached) or orange where it isn't — the rest of the editor keeps working offline. The Advanced tab's Local engine toggle is the opt-out. Appearance: System / Light / Dark theme — System follows your OS live; the others apply instantly and are remembered. Only the editor chrome re-themes; your artwork's colors never change.
https://openrouter.ai/api/v1 → API key sk-or-… → Model anthropic/claude-3.5-sonnet → Test (verifies + lists models) → Save. All Sidekick generation/✨/edit calls now go straight from this page to OpenRouter.
The two modes
| Mode | Key | For |
|---|---|---|
| Canvas | 1 | The artboard — draw, select, pose layers, keyframe on the timeline. Everything lives here. |
| Connect | 2 | State-machine graph — actions as nodes, transitions as labeled edges. |
The layout is canvas-first: the Layers/Actions panel starts open so the scene tree is always visible (it doubles as the recovery surface after AI moves an object), while the inspector and timeline start collapsed. B toggles Layers/Actions, I the inspector, M the timeline (⇧M toggles the tall keyframing layout), G the canvas grid + scale rulers, F maximizes the canvas. Panel edges drag to resize; sizes and the timeline's collapsed/expanded state persist across sessions. Under 1000px wide, panels overlay the canvas instead of shrinking it.
Chrome lives in fixed zones — a docked bar across the top of the stage carries the draw tools, a centered “working locally” pill, and the zoom/grid/max HUD; a slim transport strip under the canvas carries the playhead readout, playback controls, the ● Record cluster (a labeled pill + the posed action's name + ✕ — armed it reads ● Recording in solid red with a red edge around the stage; disarmed it reads # action · edits won't save; ✕ stops recording first, then drops back to idle), Preview toggle, and the timeline switch. A single status chip under the toolbar reports transient context — Preview mode, where an armed draw tool will land, or the floating readout during a drag.
Canvas keeps its tools always — drawing and animating share one mode, so the toolbar never hides (no more "where did my tools go"). Connect swaps the stage for the state graph: canvas toolbar, HUD, transport, and timeline all hide; the sidebar lists only Actions (they're the nodes) and the inspector shows just the graph sections — Action, Transition, and Inputs. Each mode remembers how you last arranged its panels.
Canvas & tools
| Tool | Key | Does |
|---|---|---|
| ➤ Select | V | Click selects a layer; drag moves; corner handles scale; top dot rotates; mint ✛ sets the rotate/scale origin. |
| ✋ Hand | H | Drag pans the artboard — pairs with the HUD zoom (dropdown: Fit / 50–200% / Actual size). |
| ▭ Rect / ◯ Ellipse / △ Polygon / ☆ Star / ╱ Line / → Arrow / ✎ Pen | R E Y S L W P | Drag to draw. Polygon sides and star points/inner % live in 🖌 style; arrow draws tail → tip. Shapes land as layers (or inside the selected layer — see ⧉). |
| ⬠ Points | U | Click/tap vertex-by-vertex to trace a freeform polygon — a live segment follows the cursor, and a mint ring marks the first vertex once the shape can close. Tap the ring, double-click, or press Enter to finish; Esc cancels, ⌫ removes the last vertex. Finishing with too few points keeps the draft going instead of deleting it. |
| ⤾ Path | A | Bezier authoring — click = corner anchor, drag = smooth anchor with symmetric handles. Esc / double-click finishes, Delete removes the last anchor. |
| 🅃 Text | T | Click to drop a <text>, type inline, Enter commits. Double-click existing text to re-edit. Font, size, weight, spacing, and content live in the inspector. |
| ⧉ Into-layer | — | On: draw inside the selected layer (inherits its transform & animation). Off: each shape is its own layer. |
| 🎨 color | — | Primary color for the armed tool — fill for shapes/text, stroke for pen/line/path. |
| 🖌 style | — | Tool-style popover: fill + outline color/width for shapes, stroke color + brush presets (pen / marker / brush / dash / dot) for stroked tools. Choices persist across sessions. |
| ↶ ↷ | ⌘Z ⌘⇧Z | Undo / redo — every edit, keyframe, draw, AI scene. |
| ▶ Preview | — | Lit: canvas is the runtime — hover/click fire triggers, editing pauses. Unlit (edit): gestures only edit. |
Clipboard: ⌘C copies the selected shape (or the whole layer in layer mode) — it also puts real SVG markup on the OS clipboard. ⌘V pastes into the selected layer, or as a new layer — SVG markup copied from other apps pastes straight in. ⌘X cuts a shape.
Preview methods bar: while Preview is lit, a play. chip row under the canvas lists every action as a callable method — tap sweep() to run it. The tooltip shows the exported-player equivalent (player.go('sweep')) plus which triggers reach it — a live demo of the API surface your exports expose.
layer-…) → turn ⧉ on → draw rects into it → draw a circle with ⧉ off → it becomes its own ellipse-… layer → Actions + → pose the layers → ⌘S saves it all.
The posing model — how actions are edited
Whichever action is selected is what you're editing. Select an action → it plays → the transport's ● Record cluster shows # name · edits won't save (press ● Record to start writing into it — the pill turns red and a red edge frames the stage; ✕ stops recording, or drops back to idle when you're not recording) → while recording every canvas gesture writes into that action's pose, and a live readout floats beside the cursor showing the exact x · y · ° · % being recorded:
| Gesture | In idle | In a non-idle action |
|---|---|---|
| Drag layer | Base transform (layout) | pose.x / pose.y |
| Corner handles | baseScale (geometry) | pose.scale |
| Top dot | rotate() in base | pose.rotation |
| Mint ✛ pivot | data-mf-piv (layer origin) | pose.px / pose.py — keyframeable |
| Inspector fields | Idle pose | Action pose |
| Shape drag/handles | Shape data-mf-xf (base geometry) | shapes[key].x/y/r/s — animates |
| Shape fields | Base fill/stroke/opacity attrs | shapes[key].fill/stroke/sw/o — animates |
Everything you record lands in the inspector's editing ‹action› list — one row per posed layer plus a ▸ row per posed shape, showing the stored values (click a row to jump to that layer). If you can see it there, it's in the action.
✛ Pivot — the mint crosshair inside the selection box is the layer's rotation/scale origin. Drag it anywhere — it snaps near the box's 9 anchors (corners, edge-midpoints, center); double-click resets to center. Rotating (top dot) and scaling (corner handles) then pivot around it: put it on a corner and the layer tips over that corner instead of spinning about its middle. It works in both modes — idle edits the layer's base data-mf-piv, a selected action records px/py into the pose (keyframeable, interpolated, falls back to the base pivot when a key omits it), and the exported player honors it. The shape box has its own pivot too.
↻ Pin-rotate — the direct gesture. The selection shows small mint vertex dots at every vertex (corners of a rect, real vertices of a polygon/pen path, endpoints of a line, axis extrema of a circle/ellipse). Grab a dot and orbit: that vertex pins as the origin and the shape swings about it — hold a corner of a rect and it rotates around the corner, tipping forward. ⌥-press anywhere on the selection pins that exact point instead (⌥+corner-handle does it too — the corner stays put while you swing). A plain click on a vertex pins the origin without rotating. The pinned point persists as the pivot, so the ✛ lands on it and later rotations reuse it.
⇄ Roll link (the gold toggle beside the inspector's Rotation field): while on, horizontal travel derives the rotation automatically — θ = distance ÷ radius × 57.3, a no-slip roll. Drag a ball right and it turns exactly as far as it travels. Works for any layer — balls, wheels, logs.
x +115 · rot +330° and the rotation is physically correct for the ball's size. No math, no second gesture.
⚛ Physics — the inspector's ▼ Physics section bakes real simulated motion into the selected layer of the current action. The section is contextual — it appears only while you're posing a non-idle action with a layer selected (physics recipes live inside the action). Hit the ⚛ button in the timeline header to jump straight to it — off-context it tells you what to select instead. The section header names the live target (body · hover = layer · action). It works in shape mode too (physics always bakes the whole layer). Pick a recipe (drop & bounce, rebound — perfectly elastic, never stops, launch arc, roll, slide, tumble edge-to-edge, swing, draw on/off, follow path), tune its params (gravity, bounce, friction, velocity, direction, duration…), hit Simulate. Follow path rides any picked shape's outline — a drawn stroke, pen scribble, or bezier — with optional tangent orientation; draw on reveals a stroked shape with measured stroke-dash keyframes. A deterministic solver samples the motion and writes ordinary keyframes — pivot switches included, so a rect genuinely tips corner-over-corner — then previews it. The baked keys are normal keyframes: retime them on the timeline, blend them on interruption, export them. That's bake-then-play: simulate once in the editor, then the exported .svg just plays keyframes — no physics engine inside. Physics-baked actions play linear (the dynamics are already in the keys — easing would distort them). ✕ clears the recipe and its keys. Recipes persist with the action and re-bake on scene load.
Keep it moving — friction is honest: a roll or slide decelerates and stops. Set the friction slider to 0 and it never decays — the layer keeps rolling or sliding for the whole duration (off the canvas if it needs the room). To repeat forever, turn on 🔁 Loop in the Action section of the inspector (or the action editor) — when the action ends it replays itself, exactly like a complete self-edge but one click instead of wiring a graph loop. Looping re-blends from the resting pose back to the first key — for a seamless wrap, position the layer so the motion starts and ends off-canvas. Looping works for any action, physics-baked or hand-keyed, and ships in the exported player.
tip, layer crate: pick tumble edge-to-edge, direction +1, Simulate. The solver tips the crate over its bottom-right corner, lands on the next edge, keeps going — real torque and impact loss, ~5 keys, zero hand-drawn frames.
Layers & actions panels
The sidebar's Get started section keeps the entry points one click away: ✦ Create with AI, New blank canvas, Open… a saved project, the ▶ Tour, plus a row of your recent scenes (cached generations — click to restore).
Layers — one tree for layers and their shapes: a ▸ expander on each row opens the layer's shape subrows — click one to select that shape (gold box on canvas), ⇧-click multi-selects. Layer rows: click selects, the eye toggles visibility (hidden rows dim), hovering reveals ✕ delete, + adds, inspector ↑/↓ reorder and 🗑 deletes the selected one. Actions — click plays + selects for posing, + adds, hover shows ✎ (editor: name — which is also the callable method player.play.<name>() — plus duration and per-layer poses) and ✕ (delete); a ⚛ on the row marks an action carrying a physics recipe. idle can't be deleted.
Shapes inside a layer
When the selected layer holds shapes, the inspector's Shape section lists them as chips — a dashed layer chip first, then one per shape. Layer mode (the layer chip, the default) edits the whole layer; shape mode edits one shape. Selection is bidirectional: clicking a chip gives that shape a gold box with its own move/scale/rotate handles on canvas, and pressing a shape inside the already-selected layer selects its chip. In shape mode, dragging on the canvas moves just that shape — its siblings and the layer stay put; corner handles scale it, the top dot rotates it. Arrow keys nudge whatever is selected: ←↑↓→ moves it 1 unit (⇧ = 10 units) — the active shape in shape mode (⇧-picked siblings come along), the whole layer in layer mode. In idle the nudge writes the base transform; while posing an action it writes the pose (keyframe-aware). A burst of presses is one undo step. With nothing selected — or keys selected on the timeline — ←/→ keep their classic job of hopping the playhead between keys. Per shape you also get Position / Rotation / Scale / Opacity / Fill / Stroke fields and 🗑 delete — all undoable.
Shape props are animatable. While a non-idle action is selected, those same fields (and canvas drags on the shape) write the action's shape pose instead of the base geometry — the posing into ‹action› hint appears above the fields. A shape can slide, grow, spin, fade, or change color inside one action while the layer itself does something else — Rive-style nested-object animation. Shape poses key on a stable data-mf-sh id, so they survive reordering, grouping, and copy/paste; they keyframe per-key like layer poses and blend on interrupted transitions.
Grouping shapes
Shift-click shapes on the canvas or chips to multi-select — every picked shape gets a dashed gold outline. Then ⧉ group or ⌘G wraps them in a <g> subgroup inside the layer. The group appears as a g chip — selecting it (a press on any shape inside it works too) lets you move / scale / rotate / recolor / delete the whole group at once. ungroup or ⌘⇧G dissolves it, baking the group's transform into each child so nothing shifts visually.
dog layer (a rect body, circle head, two ellipses for ears). Select dog → chips appear → click circle 2 (the head) → the gold box hugs it → drag it up on canvas, or scale it with the corner handles — the body stays put. Shift-click ellipse 3 + ellipse 4 → ⧉ group → now the ears move together as one g chip; ungroup splits them back out.
Vertex & path editing
Double-click a shape (or the ⬡ points button in the shape inspector) to enter vertex edit: orange dots mark every point — drag to move, ⊕ on an edge midpoint inserts, ⌦ deletes the selected point, Esc exits. Works on polygons, polylines, lines, and <path>s — paths additionally expose their bezier in/out handles (drag a handle dot to reshape the curve). Rects convert to polygons and ellipses to paths on first edit. All undoable.
Paint — gradients & filters
The shape fill control has a solid | linear | radial kind select. Picking a gradient kind creates (or reuses) a <linearGradient>/<radialGradient> in defs — the panel below offers a shared-gradient picker, an angle field (linear) or center/radius (radial), and editable stops (offset slider + color + ✕, plus + stop). Editing a gradient shared by several shapes clones it so only your shape changes. The filter select applies bounded presets — blur, drop shadow, glow — with live parameter fields, on a single shape or a whole layer.
Clones, alignment & clipping
⧉ clone (shape inspector) creates a <use href="#…"> linked instance — transform it freely and it mirrors every edit made to the source. With multiple shapes selected, the Align row snaps them to edges/centers or distributes them; a single layer can align to the artboard. While dragging, pink smart guides appear when edges or centers line up (~6px snap). The layer inspector's Clip select masks the layer by another layer's shapes through a live <clipPath> — animate the source and the window moves with it.
Timeline & keyframes
One track per layer, an ms ruler, a playhead — docked under the transport strip in Canvas mode. Drag the tracks to scrub; poses interpolate through keyframes live. The timeline's top edge drags to resize — pull it up for more track room, drag it all the way down to collapse (the ▸ transport button or M brings it back). ⇧M toggles the tall keyframing layout — collapsed/expanded state and both heights are remembered.
Keyframes
A layer's pose in an action is either flat ({x,y,rotation,scale,opacity} = one implicit key at t=1) or keyed ({keys:[{t, …pose, e}]}). Gold diamonds are explicit keys.
- Add:
◆+for the selected layer — ⇧+◆+keys every layer — or just scrub to a time, then pose the layer; a key lands there. - Select: click a diamond (also selects the layer + seeks there); ⇧-click after a click range-selects within a track, ⇧-drag on empty track space rubber-bands every key inside; Esc clears.
- Retime: drag a diamond — it snaps to other keyframes (mint tick on the ruler). Undoable.
- Keyframe menu: right-click a diamond — outgoing easing (incl. hold), duplicate, copy, paste-at-playhead, reverse, delete.
- Clipboard: ⌘C/⌘V/⌘D copy/paste/duplicate selected keys (paste lands at the playhead); ⌥←/→ nudges ±1%; ⌫ deletes;
◆−deletes the key under the playhead. - Dragging a flat pose's implicit key converts it to keyframed.
Per-key easing & hold
Each key carries an outgoing easing (key.e) for the segment to the next key: linear (default), easeOut, easeIn, easeInOut, easeOutBack, spring, or hold — hold freezes the pose until the next key then jumps (the diamond gets a square corner). Set it from the right-click menu or the Keyframe inspector card. The editor, the embeddable player, the SMIL export (keySplines + duplicated keyTimes for holds), and the Lottie export (i/o tangents + h:1) all honor it identically — what you scrub is what ships.
Navigate, snap & zoom
▶/Space plays from the playhead (⇧+click restarts at 0). ⏮/⏭ or ←/→ hop the playhead between key times. The ruler is a seek strip — click or drag anywhere on it. Scrubbing and key-dragging snap to keyframes within ~6px and flash a mint tick. The head's zoom slider spreads the tracks horizontally (up to 8×) so dense physics bakes stay readable — labels keep a fixed column.
Work area
The two lavender tabs on the ruler are the work-area in/out handles — drag them to bound playback: ▶ plays (and 🔁 loops) only inside the band; everything outside dims. Scrubbing still roams the full ruler — the band only gates playback, never editing.
Onion skin & motion path
◐/O overlays editor-only guides inside the scene: silhouette ghosts of every keyframed layer at its own keyframes before (blue) and after (orange) the playhead, plus a dashed motion path tracing the selected layer's trajectory sampled through every eased segment (dots at key positions, a bright dot at the live pose). It follows the playhead as you scrub — perfect for spacing arcs and checking overlap. Editor chrome only: hidden in preview mode and the Connect view, never exported.
sweep, layer beam: scrub to 0.5 → rotate beam to −30° (key appears) → scrub to 1.0 → rotate to 55°. Result: 0° → −30° → 55° over the action duration.
Transport & header
The transport strip under the canvas: 0.32s · 19f readout (seconds + frames @60fps) · ⏮ ⏭ keyframe jumps · ▶/⏸ play-from-playhead · 🔁 loop · 0.5/1/2× speed · ▶ Preview edit/runtime switch · ▸ timeline toggle. The slim timeline head holds dur (action.duration, ms) · easing (the incoming transition's) · ◆+/◆− key ops · ⚛ physics jump · ◐ onion skin · the zoom slider.
Connect mode — the state machine
Actions are nodes; transitions are labeled edges. Drag nodes to arrange the graph — positions persist with the scene. Click a node to play it, double-click to edit its props inline in the inspector (name — which is the player.play.<name>() method — duration, triggers, poses), click an edge to edit that transition inline (from/to/trigger/duration/easing/target/when — it highlights mint), + adds an edge and lands it selected in the inspector card — every field commits live, no modal. Dashed amber ring = orphan (no way in — it can never play). Self-loops and parallel edges render distinctly.
Build the machine on the canvas. Every node carries a connect port — a small dot that appears on its right edge when you hover. Drag from the port onto another node (or back onto itself for a self-loop) to wire a transition with a live preview curve; the new edge lands selected so the inspector is already open on it. Double-click empty canvas drops a new action node at that point. The + button creates an edge with defaults — the inspector card edits everything: from/to/trigger/duration/easing plus target and when.
Edit on the canvas too. Right-click an edge for edit / duplicate (⌘D) / reverse direction / delete (⌫ also works on the selected edge). Right-click a node for play / edit props / duplicate / delete action. The selected edge grows a mint re-aim handle on its arrowhead — drag it onto another node to re-target the edge. Esc deselects. Every edit is undoable.
Read the machine at a glance. Edge labels carry badges: ◈layer = targeted trigger, ⚖ = guarded by a when expression, ⚠ = a validation warning (a target naming no layer, a guard reading an undeclared input, or a duplicate same-route same-trigger edge — the tooltip explains). A mint pulse travels an edge the moment its transition fires — watch the machine run live. Nodes show 🔁 when the action loops and ! when orphaned.
Big graphs fit. Drag empty canvas to pan, scroll the wheel (or −/+) to zoom around the cursor — the zoom % shows in the title bar and persists with the layout. ⤾ resets everything: nodes re-arrange in a circle and the view refits (undoable).
| Trigger | Fires when |
|---|---|
pointerenter / pointerleave | Pointer enters / leaves the canvas (preview or exported runtime) |
pointerdown / pointerup | Press / release on the canvas |
click | Click on the canvas |
auto | Shortly after arriving at the from action |
complete | The from action's animation finishes — chains sequences |
scroll | The artwork scrolls into the viewport (IntersectionObserver, once per entry) |
Easings: linear, easeOut, easeInOut, easeOutBack, spring.
Targets & guards. A transition's target field scopes its trigger to one layer — a click only counts when the pointer lands on that layer's artwork (unset = anywhere; a targeted edge wins over a canvas-wide one). The Inputs inspector section (Connect mode) declares named bool/num inputs; a transition's when expression (armed && count > 2, operators ! == != > < >= <= && ||) must hold for it to fire — set inputs from the inspector or via setInput, which re-checks pending auto/complete edges. Both work identically in the exported player.
idle →sweep on click, sweep →idle on auto: clicking the canvas sweeps the beam once and it returns to rest on its own. To loop forever instead, point the second edge back with complete → sweep again.
Ambient CSS
Scenes can carry optional CSS — @keyframes/animation rules for continuous decoration (spin, pulse, shimmer, blink) that the pose engine doesn't model. It renders live in the canvas, is editable in the inspector, and embeds inside saved .svg files so they animate standalone.
Sandboxed: rules can't set transform/opacity/display on layer ids (#beam) — the pose engine owns those — but inner elements (#beam .ray) are unrestricted. No url(), @import, or scriptable content.
@keyframes beamPulse { 50% { opacity: .3 } }
#beam .ray { animation: beamPulse 2.4s ease-in-out infinite }
@keyframes starTw { 50% { opacity: .25 } }
#stars circle { animation: starTw 1.9s ease-in-out infinite }
Files, save/load, exports
| Action | Result |
|---|---|
| ⌘S / Save | The OS save dialog — pick a name + location; the filename becomes the title. Afterwards ⌘S writes through to that file silently. (Browsers without the File System API name the file, then download it.) |
| ⇧⌘S / Save as… | Always shows the save dialog for a new file and switches to it; the original file is untouched. |
| ⌘O / Open file… | The OS open dialog — SVGMotion .svg projects, generic SVGs (wrapped as layers, styles preserved), or legacy .json. Open saved project… in the same group lists browser-stored projects (open or ✕ delete). |
| Download .svg file | One self-contained .svg: artwork + <metadata id="svgmotion-scene"> with layers/actions/transitions/css (files saved before the rename use motionforge-scene — both open). Still a valid animating image. |
| Export runtime | Standalone .svg with an embedded script that plays the state machine on interaction. |
| Export embed player | .html embed player exposing player.call(id), player.play.<id>(), player.fire(trigger, layer?), player.setInput(name,v), player.actions(). |
| Export SMIL animation | A no-JavaScript .svg for one action — poses become <animate>/<animateTransform> with values+keyTimes. Two flavors: plays in <img> autoplays on load; tap to play starts on a click when the .svg is opened as a document (static inside <img> — no events there). One action per file, linear key steps, no state machine — physics arrives pre-baked as plain key steps (bake-then-play), so no player is needed. |
| Export Lottie | Bodymovin JSON — all actions concatenated on one timeline with markers. Layers → shape layers, bezier paths/gradients/clips map natively; filters and text are dropped (the export toast reports them). |
| → Asset Studio… | Sends the scene to Asset Studio (new tab) as a framed icon/asset doc — layers preserved, auto-fitted to the safe zone. Inside Asset Studio: ☰ → From Vector Editor ▸ pulls any saved project, and ☰ → Edit in Vector Editor sends it back for path/vertex work — pushing again updates the same asset in place. |
| Autosave | Every edit persists locally (IndexedDB) and restores on reload. |
| Recents | Generated scenes cache per-prompt — the chip row restores them instantly. |
JavaScript API
The editor exposes window.SVGMotion; exported embed players expose the same surface as player.
SVGMotion.list() // ['idle','sweep','glow']
SVGMotion.call('sweep') // run an action by id
SVGMotion.play.sweep() // Proxy — every action is a function
SVGMotion.fire('click') // fire a trigger
SVGMotion.fire('click','beam') // as if the event hit that layer
SVGMotion.setInput('armed', true) // set a named input (re-checks guards)
SVGMotion.current // 'idle'
Sidekick's command route uses the same mechanism — natural language in, a function call out.
Worked example — the lighthouse
Everything above, end to end. Prompt: a lighthouse whose beam sweeps the sky — click rotates the beam, hover makes it glow. The result (live, pure CSS — exactly what a saved .svg does standalone):
…and the scene JSON the model returns (trimmed):
{
"layers": ["stars","beam","shadow","base","tower"],
"actions": [
{ "id":"idle", "layers":{} },
{ "id":"sweep", "duration":900,
"layers":{"beam":{"rotation":35}} },
{ "id":"glow", "duration":500,
"layers":{"beam":{"scale":1.15,"opacity":1}} }
],
"transitions": [
{"from":"idle","to":"sweep","trigger":"click"},
{"from":"sweep","to":"idle","trigger":"auto"},
{"from":"idle","to":"glow","trigger":"pointerenter"},
{"from":"glow","to":"idle","trigger":"pointerleave"}
],
"css": "@keyframes beamPulse{50%{opacity:.3}}\n #beam .ray{animation:beamPulse 2.4s infinite}"
}
In the editor: click the canvas (preview mode) → beam sweeps, returns on auto; hover → it glows. From code: SVGMotion.call('sweep'). Saved as .svg: the file itself plays the pulse + twinkle with no JS.
Keyboard shortcuts
Every key and pointer gesture has its own searchable page: shortcuts.html → — tools, views, panels, clipboard, grouping, timeline and canvas gestures. In-app, ? or the rail's ? Help button opens these same pages in a windowed overlay — the Guide / Shortcuts tabs in its title bar switch between them.
Troubleshooting
| Symptom | Fix |
|---|---|
Cannot reach Ollama toast | Run ollama serve; check the Server URL in ⚙ Settings. Off-localhost hosting needs OLLAMA_ORIGINS on the Ollama side. |
model not found | ollama pull qwen3:8b (or the model set in ⚙ Settings). |
| Sidekick shows the setup chip | ⚙ Settings → add an OpenRouter/OpenAI key, or switch to Ollama. Manual editing needs no AI. |
| Generation times out / fails validation | ~2min is normal on a local model; retry — validation errors are model flakes, the pipeline retries with stricter compactness. |
| Scene loads but won't animate | Check the action's poses exist (the timeline shows its tracks — M opens it); check a transition reaches it (Connect mode, dashed ring = unreachable). |
| Canvas press triggers an action while editing | You're in Preview mode — unlight the ▶ Preview button on the transport strip. |
| Saved .svg doesn't animate standalone | Poses/transitions need the runtime — only ambient CSS animates with no JS. Use Export runtime for a self-playing file. |
| ⚛ Simulate says no drawable geometry | The selected layer is hidden (eye icon) or empty — unhide it or draw into it. |
| ⚛ Simulate says no motion | The layer is already at rest — e.g. "Drop & bounce" on a shape sitting on the floor. Raise a velocity/angle param or move it up. |
| ⚛ Simulate is greyed out | Needs both a selected layer and a non-idle action — the hint under the card says which is missing. |
SVGMotion — prompt → SVG → actions → state machine. Local-first via Ollama. · Keyboard shortcuts · Character Studio