Project JSON Schema

An Animot project is a JSON file with a fixed shape. The editor exports and imports this format; the AI skill writes it directly. This page documents the parts you'd touch when authoring or inspecting projects by hand.

Top-level project

{
  "schemaVersion": 1,
  "id": "<random-id>",
  "name": "My Presentation",
  "createdAt": 1709000000000,
  "updatedAt": 1709000000000,
  "mode": "presentation",
  "settings": {
    "defaultCanvasWidth": 1920,
    "defaultCanvasHeight": 1080,
    "defaultTransition": { "type": "fade", "duration": 500, "easing": "ease-in-out" },
    "defaultSlideDuration": 4000
  },
  "slides": [ /* ... */ ]
}
FieldDescription
schemaVersionInteger. Currently 1. Bumped when breaking changes ship.
modeOptional. "presentation" (default), "flow", or "cinema".
settings.worldWidth / worldHeightCinema mode only. Size of the virtual canvas the camera moves over.
settings.loopMode"reset" (default) or "transition". How looping back to slide 0 behaves.
settings.narrationEnabledBoolean. When true, per-slide voice-over (slide.narration) plays in presentation mode and on shared links — and autoplay is suppressed so the viewer's first click unlocks the browser audio context. Pro feature. Default: false.

Slide

{
  "id": "slide-1",
  "name": "Slide 1",
  "duration": 4000,
  "transition": { "type": "none", "duration": 500, "easing": "ease-in-out" },
  "camera": { "x": 0, "y": 0, "width": 1920, "height": 1080, "rotation": 0 },
  "canvas": {
    "width": 1920,
    "height": 1080,
    "background": {
      "type": "gradient",
      "gradient": {
        "type": "linear",
        "angle": 135,
        "colors": ["#7c3aed", "#ec4899"]
      }
    },
    "elements": [ /* ... */ ]
  }
}
FieldDescription
durationHold time in ms. Affects autoplay speed and export length.
transition.type"none" | "fade" | "slide-*" | "zoom-*" | "flip". Use "none" for per-element morphing (the magic).
cameraCinema mode only — viewport over the world canvas.
notesSpeaker notes shown on the presenter view (/present?presenter=1). Plain text — never rendered onto the audience canvas.
narrationOptional { src, duration }. src is a base64 audio data URL (audio/webm or audio/mpeg). Plays when this slide is shown if settings.narrationEnabled is true. Pro feature.
canvas.background.type"solid" | "gradient" | "image" | "transparent".
canvas.background.gradient.type"linear" | "radial" | "conic".
canvas.background.gradient.stopsOptional array of {color, position} for explicit positions; otherwise spaced from colors.

Element (base)

Every element shares the same base shape:

{
  "id": "shape-a",
  "type": "shape",
  "position": { "x": 100, "y": 100 },
  "size": { "width": 200, "height": 200 },
  "rotation": 0, "skewX": 0, "skewY": 0,
  "tiltX": 0, "tiltY": 0, "tiltOrigin": "center",
  "zIndex": 1,
  "visible": true,
  "shapeType": "circle",
  "fillColor": "#7c3aed",
  "strokeColor": "#ffffff",
  "strokeWidth": 0,
  "opacity": 1,
  "borderRadius": 0,
  "depth": 0,
  "decorations": {
    "glow": { "enabled": true, "color": "#a78bfa", "intensity": 0.6, "speedMs": 2400 }
  },
  "animationConfig": {
    "order": 0, "delay": 0, "duration": 600, "easing": "spring"
  }
}

Common keys

FieldDescription
idCritical: elements with the same id across slides auto-morph.
typetext | code | shape | image | video | arrow | icon | svg | chart | counter | motionPath.
position / size / rotation / skew / tiltStandard transform stack. Animatable.
floatingAnimationIdle motion with named preset: float, breathe, pulse, wiggle, sway, drift, bob, tilt. Or legacy direction + amplitude + speed.
decorationsGlow, shimmer, gradient shift, RGB split. See decorations panel in the editor.
depthCinema mode parallax. -1 (back) to +1 (front). 0 = neutral.
animationConfigOrder, delay, duration, easing for the morph TO this slide. Also holds entrance / exit / emphasis presets and their per-lane durations + emphasisDelay.
animationConfig.entranceHow the element appears: fade, slide-up/down/left/right, pop, scale-in, rotate-in, blur-in, rise, flip-x/y, bounce-in, or none.
animationConfig.exitHow the element leaves: fade, slide-*, pop, scale-out, rotate-out, blur-out, sink, flip-*, or none. Setting a non-fade exit opts the element out of cross-slide morphing.
animationConfig.emphasisAlways-on micro-motion: pulse, shake, flash, wobble, glow-flash, bob-once, tada, or none.
motionPathId / motionPathConfigBind to a motion-path element to follow a Bezier curve.

Element snippets

Text

{
  "id": "title",
  "type": "text",
  "content": "Hello World",
  "fontSize": 96,
  "fontWeight": 700,
  "fontFamily": "Plus Jakarta Sans Variable",
  "color": "#ffffff",
  "textAlign": "center",
  "animation": {
    "mode": "handwriting",
    "duration": 2000,
    "loop": false
  }
}

Animation modes (16 total): instant, typewriter, fade-words, fade-letters, bounce-in, handwriting, scramble-in, slot-machine, drop, glitch, marquee, blur-in, stretch, slide-words, wave, typewriter-erase.

Code

{
  "id": "demo-code",
  "type": "code",
  "code": "const x = 1;\nconst y = 2;",
  "language": "typescript",
  "theme": "github-dark",
  "filename": "example.ts",
  "showLineNumbers": true,
  "highlightedLines": [],
  "animation": { "mode": "typewriter", "typewriterSpeed": 30 }
}

Animation modes: instant, typewriter, highlight-changes. Themes from Shiki: github-dark, dracula, nord, etc.

Arrow (with flow markers)

{
  "id": "arr-1",
  "type": "arrow",
  "startPoint": { "x": 0, "y": 0 },
  "endPoint": { "x": 200, "y": 0 },
  "controlPoints": [],
  "color": "#22d3ee",
  "strokeWidth": 3,
  "headSize": 12,
  "style": "dashed",
  "showHead": true,
  "animation": { "mode": "flow", "duration": 1000, "loop": true },
  "flowMarkers": {
    "enabled": true,
    "shape": "circle",
    "color": "#22d3ee",
    "size": 10,
    "count": 3,
    "speedMs": 2400,
    "direction": "forward",
    "easing": "linear"
  }
}

Animation modes: none, grow, draw, undraw, draw-undraw, flow (marching dashes). Flow markers travel along the arrow path — Flow mode's signature effect.

Video

{
  "id": "demo-video",
  "type": "video",
  "src": "data:video/mp4;base64,...   // local file (exportable)
        OR https://youtube.com/watch?v=...   // embed (live only)",
  "volume": 1,
  "muted": true,
  "autoplay": true,
  "loop": true,
  "playbackRate": 1,
  "objectFit": "cover",
  "borderRadius": 12,
  "opacity": 1,
  "showControls": false,
  "startTime": 0,
  "endTime": 30
}
YouTube/Vimeo URLs render as iframes via the IFrame Player API but cannot be captured into GIF/MP4 exports. Local data: URLs export frame-accurately.

Chart

{
  "id": "revenue-chart",
  "type": "chart",
  "chartType": "bar",        // bar | line | area | pie | donut
  "title": "Q1 2026",

  // Single-series shorthand:
  "data": [
    { "label": "Jan", "value": 120 },
    { "label": "Feb", "value": 180 },
    { "label": "Mar", "value": 240, "color": "#ec4899" }
  ],

  // — OR — multi-series (overrides data):
  // "series": [
  //   { "id": "rev",   "name": "Revenue", "color": "#6366f1",
  //     "data": [{ "label": "Jan", "value": 120 }, ...] },
  //   { "id": "costs", "name": "Costs",
  //     "data": [{ "label": "Jan", "value": 80 },  ...] }
  // ],

  "barLayout": "grouped",      // grouped | stacked (multi-series bars)
  "useKitPalette": false,      // pull colors from active brand kit
  "colors": ["#6366f1", "#ec4899", "#06b6d4"],
  "revealStagger": 80,         // ms between bars/points on entrance
  "animationDuration": 1200,

  "valueFormat": {
    "prefix": "$",
    "suffix": "",
    "decimals": 0,
    "abbreviate": true         // 1500 → 1.5K, 2_300_000 → 2.3M
  },

  "yAxis": {
    "min": 0,                  // omit for auto-fit
    "max": 300,
    "showLabels": true
  },

  "showLabels": true,
  "showValues": true,
  "showGrid": true,
  "showLegend": true,
  "backgroundColor": "#1e1e2e",
  "textColor": "#ffffff",
  "fontSize": 12,
  "padding": 16,
  "borderRadius": 8
}

Chart values morph between slides — keep the same chart id across slides and the renderer tweens bar heights / line points / pie sweeps from the prior values to the new ones. Points are matched by label first, then by index, so re-orderings or inserts still tween cleanly.

Use data for single-series charts. Add series[] for multi-series (revenue vs costs, grouped or stacked bars, multiple lines/areas) — when present it overrides data. useKitPalette: true pulls colors from the active brand kit's primary/secondary/accent so a kit swap re-skins charts deck-wide without per-element edits.

Progress

{
  "id": "completion",
  "type": "progress",
  "shape": "bar-horizontal",        // bar-horizontal | bar-vertical | ring
  "value": 65,
  "max": 100,
  "fillColor": "#6366f1",
  // Optional gradient fill — same shape as canvas-background gradient.
  // "fillGradient": { "type": "linear", "angle": 90, "colors": ["#6366f1", "#ec4899"] },
  "showTrack": true,
  "trackColor": "rgba(255, 255, 255, 0.12)",
  "roundness": 50,                  // 0 = square, 50 = pill (bars only)
  "thickness": 24,                  // bars: pixel height/width; ring: stroke in viewBox units
  "direction": "ltr",               // ltr/rtl horizontal · btt/ttb vertical · cw/ccw ring
  "label": {
    "position": "inside",           // inside | top | bottom | left | right | none
    "format": "percent",            // percent | value | value-of-max | custom
    "fontSize": 12, "fontWeight": 600, "color": "#ffffff"
  },
  "animationDuration": 1200,
  // Indeterminate "loading" mode — animated diagonal stripes.
  // "indeterminate": true, "stripeAngle": 45, "stripeWidth": 14, "stripeSpeed": 1200
}

Three shapes: horizontal bar, vertical bar, and ring (circular). Same id across slides with different value tweens the fill smoothly during the slide transition — natural for "stat grows" reveals. Fill accepts either a solid color or a gradient (works on bars and rings — rings use an SVG <linearGradient> internally).

indeterminate: true swaps the value-driven fill for an animated diagonal stripe sweep — useful for "loading…" decks where the actual percentage doesn't matter. stripeAngle, stripeWidth, and stripeSpeed tune the look.

Container (auto-layout)

{
  "id": "card-row",
  "type": "container",
  "position": { "x": 100, "y": 200 },
  "size": { "width": 1720, "height": 400 },
  "childIds": ["card-a", "card-b", "card-c"],
  "direction": "row",                // row | column
  "gap": 12,                         // px between children
  "padding": 16,                     // number (uniform) or { top,right,bottom,left }
  "align": "center",                 // start | center | end | stretch (cross-axis)
  "justify": "space-between",        // start | center | end | space-between | space-around | space-evenly
  "fillColor": "#0f172a",
  "borderColor": "#334155",
  "borderWidth": 1,
  "borderRadius": 12
}

Holds an ordered list of child element ids and arranges them via flex-style rules. Children remain individually selectable and morph across slides via the usual id-matching. The layout pass runs whenever the container's size, position, direction, gap, padding, align, justify, or child set changes — and whenever any child resizes — so siblings reflow automatically.

Sticky note

{
  "id": "note-1",
  "type": "sticky",
  "position": { "x": 80, "y": 80 },
  "size": { "width": 220, "height": 220 },
  "text": "Ship the thing",
  "bgColor": "#fac431",
  "textColor": "#1a1206",
  "fontSize": 22,
  "fontFamily": "Inter Variable",
  "fontWeight": 600,
  "textAlign": "left",           // left | center | right
  "padding": 20,
  "borderRadius": 6,
  "opacity": 1,
  "shadow": true                 // lifted-paper drop shadow
}

A FigJam-style colored note with auto-fitting text, a paper sheen and a peeled corner. Add one from the toolbar, then click it and type. Pick the color, font, size, alignment and shadow from the properties panel.

Freehand drawing

{
  "id": "ink-1",
  "type": "draw",
  // points are [x, y, pressure?] in the element's own 0,0-origin space
  "points": [[0, 0, 0.5], [12, 8, 0.6], [30, 10, 0.5]],
  "color": "#a842ff",
  "opacity": 1,
  "brush": "pen",                // pen | marker | highlighter
  "strokeWidth": 8,
  "thinning": 0.6,               // pressure → width effect (pen)
  "smoothing": 0.5,
  "streamline": 0.5,
  "taperStart": true,
  "taperEnd": true
}

Hand-drawn ink from the pen, marker or highlighter tool. points are [x, y, pressure?] in the element's own coordinate space; the renderer builds a smooth outline and scales it to the element box. Usually drawn interactively rather than written by hand. The shape tool reuses the same capture but snaps the stroke to a shape element instead.

Per-element keyframes (within slide)

{
  "id": "title",
  "type": "text",
  "content": "Hello",
  "position": { "x": 100, "y": 400 },
  "size": { "width": 800, "height": 100 },
  "keyframes": [
    { "id": "k1", "time": 0,    "position": { "x": 100, "y": 400 }, "opacity": 1, "easing": "ease-out" },
    { "id": "k2", "time": 600,  "position": { "x": 200, "y": 380 }, "rotation": -3, "easing": "ease-in-out" },
    { "id": "k3", "time": 1500, "position": { "x": 100, "y": 400 }, "color": "#fbbf24", "easing": "spring" }
  ]
}

Optional <code>keyframes</code> array on any element. Each keyframe is a partial state snapshot at a given time (ms from slide-display start) — only listed properties are interpolated, the rest fall back to the element's base. Layered on top of slide-to-slide morphing: morphs use the LAST keyframe as the exit pose and the FIRST as the next slide's entry pose, so motion stays continuous across cuts.

Animatable per keyframe: position, size, rotation, opacity, skew, tilt, borderRadius, fontSize, fillColor, strokeColor, strokeWidth, and the four CSS filters (blur/brightness/contrast/saturate/grayscale). backgroundColor and text color snap at each keyframe (no smooth interpolation). Easing per keyframe: linear | ease | ease-in | ease-out | ease-in-out | spring. On SVG elements, keyframes also carry a path (the SVG d) so the geometry morphs within the slide — capture a keyframe, edit the path, capture another.

Shared id = morph

The same id across multiple slides means the editor and presenter auto-tween every animatable property between the slide values. Different ids = separate elements that fade in/out independently.

Path morphing: give the same id to an svg element on consecutive slides with different svgContent and the path geometry interpolates (circle → boat → dog). Works svg ↔ svg, svg ↔ shape and svg ↔ draw. Multi-path, multi-color SVGs morph too — each subpath is matched and interpolated independently and keeps its own fill (paired by index). Optional morphColor on any shape/svg/draw element draws the whole shape in one solid color during the morph only (real colors return when it finishes) — handy when an icon's black outlines read as a blob mid-morph. Freehand draw elements morph the same way, enabling hand-drawn / character animations. To hand-tweak a path, select an SVG and click Edit nodes in the properties panel to drag its anchor points and Bézier handles.

For full reference

The TypeScript types are the authoritative spec. See src/lib/types.ts in the presenter repo or in your node_modules/animot-presenter/dist/types.d.ts after install.