# Animate's project format

The file Animate downloads (`.slopanim.json`) and keeps in your browser. Generated from world/plans/animate.md by animate/scripts/build.mjs.

## Project format (version 1)

A project is one JSON object (`.slopanim.json` when exported; IndexedDB `slopped-animate` / store `projects` keyed
by `id` when saved locally).

```js
{
  format: 'slopped-animate', version: 1,
  id: 'p-…', name: 'Untitled', created: ISO, modified: ISO,
  width: 640, height: 360, fps: 24, duration: 4,     // seconds
  background: '#171815' | 'none',                                         // 'none': see-through
  song: null | { source: 'lamewire', id, title, artist, url, bpmHint?, gain }   // id gen:<song>:<engine>; bpmHint: the tempo it was asked for
               | { source: 'notes', id, title, bpm, offset: 0.1 },       // a house note song (music engine; 0.1 s of quiet first)
  beats: null | { bpm, offset, times: [seconds…], detected?, confidence? }, // detected or known; offset: beat 0, seconds
  layers: [Layer…]                                                        // bottom to top
}
Layer = {
  id, name, type: 'rect'|'ellipse'|'text'|'path'|'image'|'group'|'rig',
  visible: true, locked: false, parent: null | groupId,
  props: { x, y, scale, rotation, opacity, fill, stroke, strokeWidth, trim },   // base values
  tracks: { <prop>: [{ t, v, ease }] },          // keyframes, sorted by t (seconds); ease applies to the segment AFTER the key
  // per type:
  w, h            // rect, ellipse, image
  text, size, font  // text; font 'head' | 'sans' | 'serif' | 'mono'
  d               // path: SVG path data in layer-local units
  src             // image: a URL on *.slopped.site or a data: URL
  rig: { kind, opts }   // rig, see "Rig interface"
}
ease = 'linear' | 'hold' | 'in' | 'out' | 'inout' | [x1, y1, x2, y2]   // a cubic-bézier, like CSS
```

Colours are `#rrggbb` and interpolate in linear sRGB. `trim` is 0..1 of the path drawn (paths and shape outlines).
Unknown fields are kept on import; a `version` above 1 is refused with a message.

## Rig interface

This is the interface the studio's draw loop calls for every rig layer. It is implemented by the adapter
`animate/public/rig-layer.js` (the studio's file), which wraps the rig builder's module `animate/public/rig/index.js`
(its own interface is in `rig/README.md`; see "Rig" below). The studio loads the adapter lazily with
`import('/rig-layer.js')`; if that import fails, rig layers draw as a labelled placeholder and the Add menu says
"Rig (coming)".

```js
// animate/public/rig/index.js — an ES module, no build step, no third-party CDN (vendor anything into rig/vendor/)
export const RIG_VERSION = 1
export const KINDS = [ { id: 'stick', name: 'Stick figure', dims: '2d' }, … ]   // what the Add › Rig menu lists
export function createRig (kind, opts = {}) → Rig      // opts: the layer's saved rig.opts (unknown keys ignored)

Rig = {
  kind, dims: '2d' | '3d',
  params: [ { id, label, type: 'number'|'colour'|'select'|'boolean', min?, max?, step?, options?: [{ value, label }], default } ],
                                    // the studio shows these in the inspector and saves values in layer.rig.opts
  set (opts),                       // the visitor changed a param (or a move)
  pose (t, beats),                  // pose for time t (seconds on the project timeline). Pure in t: scrubbing back
                                    // and forth gives the same pose, so export can render any frame in any order.
  draw (ctx),                       // draw the current pose into a CanvasRenderingContext2D. The studio has already
                                    // translated/rotated/scaled/alpha'd ctx to the layer's transform, so (0, 0) is the
                                    // rig's anchor (its feet/centre) and 1 unit = 1 project pixel at scale 1.
                                    // 3D rigs render into their own WebGL canvas (rig.canvas) and composite it here
                                    // with ctx.drawImage, centred on (0, 0) — the studio never touches WebGL.
  bounds () → { x, y, w, h },       // local box for selection and hit tests
  resize? (w, h),                   // 3D: the size of rig.canvas in project pixels (the studio calls it on add and
                                    // when the layer's box changes)
  dispose? ()                       // free WebGL resources when the layer is deleted
}

beats = {
  bpm, offset,                      // offset: time of beat 0, seconds
  times: [seconds…],                // every beat in the song (may be empty: then derive from bpm/offset)
  index,                            // the beat number at t as a float (2.25 = a quarter past beat 2); -1 before offset
  phase,                            // index % 1, 0..1
  bar, beatInBar,                   // 4/4 assumed
  energy                            // 0..1 loudness at t when the studio knows it (Lamewire waveform), else 0.5
}
```

Rules: a rig never plays sound, never reads the network except its own files under `/rig/`, never touches the
document outside its own canvas, and works with reduced motion set (the studio only calls `pose` while the visitor's
animation plays or scrubs, so a rig needs no motion check of its own). Rig option values must be JSON.
