animation
Author animated technical explainer diagrams as .anim.json files for Nimbalyst's Animation editor. Use when the user wants to animate a diagram, show how a system/protocol/algorithm behaves over time, build a motion explainer, or turn a static architecture diagram into something that plays.
git clone --depth 1 https://github.com/nimbalyst/nimbalyst /tmp/animation && cp -r /tmp/animation/packages/extensions/animation/claude-plugin/skills/animation ~/.claude/skills/animationSKILL.md
# Animation - step-based explainer diagrams
`.anim.json` files open in Nimbalyst's Animation editor: a named scene plus an ordered list of steps that assign states to the scene's parts. You write plain JSON. Any agent can author or edit one with `Write` and `Edit` -- there is no binary format and no tool call required.
## When to use this
- Explaining how a system behaves **over time**: a request crossing a network, a consensus round, a cache filling, a build pipeline, a queue draining.
- Turning a static architecture diagram into something that plays.
- Showing a failure and a recovery: the retry, the rollback, the rejected review.
**Do not use it** when a static diagram says the same thing. If nothing changes between the first frame and the last, you want Excalidraw or a Mermaid block, not an animation.
## The mental model
Three rules drive every decision in this format:
1. **The document says what is true when, never how to tween.** There are no keyframes, easing curves, or property tracks. You assign a *state* to a part, and CSS transitions interpolate. Adding motion means naming a state, not scripting a timeline.
2. **States are cumulative.** A step asserts only what changes; every part it does not mention keeps whatever the previous step left it in. Write deltas. **To turn something off you must explicitly set it back** -- it will not decay on its own.
3. **Ids are names, not handles.** `store`, `title-card`, `queueTask01`. You will reference them constantly in `set` blocks; make them readable.
Times are **integer milliseconds**. Never frame indices, never floats.
## Document structure
```json
{
"version": 1,
"stage": { "width": 1200, "height": 640, "fps": 25 },
"parts": { "<id>": { "type": "node" | "edge" | "label" | "shape", ... } },
"steps": [ { "id": "...", "duration": 800, "caption": "...", "set": { ... } } ]
}
```
### stage
| Field | Notes |
| --- | --- |
| `width`, `height` | Clamped to 16..8192. The stage scales to fit the pane, so these set the aspect ratio and the coordinate system, not the pixel size. |
| `fps` | Only affects frame snapping and the readout. Use 25 unless you have a reason. Whole-millisecond frame rates: 10, 20, 25, 50. |
| `background` | Optional override. Omit it and the stage uses the theme surface, which is what you want -- the scene then follows the user's light/dark theme. |
1200x640 is a good default. Landscape, room for a header row and a bottom rail.
### parts
Part ids are the keys. All four types share `label`, `tone`, and `state` (their *baseline*, before any step runs).
**`node`** -- the workhorse. A titled card with an optional subtitle and key/value rows.
```json
{ "type": "node", "label": "Object store", "x": 740, "y": 118, "w": 240, "h": 176,
"subtitle": "SHA -> BYTES",
"rows": [ { "key": "f7a9", "value": "commit 182 B" }, { "key": "e816" } ] }
```
- `label` is **uppercased automatically**. Write `"Merge gate"`, it renders `MERGE GATE`. Falls back to the id.
- `subtitle` is a small mono line under the header. Keep it short and caps-ish; it is where model names, worktrees, and units go.
- `rows` render as boxed key/value pairs. `value` is optional. Key is left-aligned, value right-aligned.
- **A row that would spill past the bottom is silently dropped.** Size the node to its rows (see Geometry).
**`edge`** -- a line between two parts, optionally carrying packets.
```json
{ "type": "edge", "from": "client", "to": "store", "text": "GET <sha>", "packets": 4 }
```
- `from`/`to` are part ids. Drawn centre-to-centre and trimmed to the box borders, so stacked and side-by-side both look right.
- **A dangling `from`/`to` renders nothing at all** -- it looks like a broken renderer, not a broken document. Check your ids.
- `packets` is how many squares travel the line while it is flowing (default 3). Set `0` for an edge that means a *relationship* rather than traffic. One trip takes 1.6s.
- `text` draws a caption at the midpoint **on an opaque background plate** roughly `max(40, len × 7.6 + 16)` px wide. It will punch a hole through anything behind it. Only put `text` on an edge whose gap is wider than the plate.
**`label`** -- free-standing text.
```json
{ "type": "label", "x": 56, "y": 48, "text": "COMMIT DAG", "align": "start", "caps": true }
```
- `align`: `start` | `middle` | `end` (the anchor, at `x`).
- `caps: true` gives the faint, tracked-out micro-caption style used for section headings.
- **There is no font-size control.** Every label is 12px. Do not try to build a large title; build hierarchy with `caps`, tone, and position instead.
**`shape`** -- a plain rect or circle, with optional centered text.
```json
{ "type": "shape", "shape": "rect", "x": 89, "y": 438, "w": 40, "h": 18, "tone": "accent" }
```
Shapes are how you show **quantity**, because text never changes (see Hard constraints). A grid of small shapes that go `hidden` one group at a time is a queue draining, a battery discharging, a work list being claimed.
**`html`** -- freeform markup, for the things the primitives above cannot express: real typography, a type scale, flow layout, a UI that has to look like a real product rather than like a diagram of it.
The markup comes from one of two places:
```json
{ "type": "html", "x": 55, "y": 108, "w": 1090, "h": 534,
"htmlFile": "./partials/app-window.html",
"vars": { "title": "acme-api", "branch": "main" } }
```
- **`htmlFile`** is a path to a `.html` file next to the document. Relative only; `..` is allowed, absolute is refused.
- **`html`** is markup inline. Right for a few lines, wrong for a widget.
`vars` fills `{{name}}` placeholders in whichever source won. Values are **HTML-escaped**, so a var is text and cannot change the structure of the partial it lands in. An unfilled placeholder resolves to empty, never to its own name. That is the entire template language -- no conditionals, no loops, no expressions.
Three things about `html` parts that will otherwise cost you a build:
-Run E2E tests in a dev container for isolated, reproducible test execution. Use proactively when asked to run Playwright tests, E2E tests, or when in a worktree. Handles the full Docker container lifecycle automatically.
Prepare and execute an Android release (patch/minor/major)
Gather details and draft an actionable bug report for developers.
Create a git commit with concise, bullet-point commit message
Create a new plan document for tracking work.
Run E2E tests in a dev container (isolated environment)
Execute a plan document while keeping progress synchronized.
Investigate a Nimbalyst performance problem (freeze, lag, idle CPU, slow op, memory growth) with measured evidence before proposing a fix.