Skip to content

david-sling/equanim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Equanim

A declarative, JSON-based animation specification where every visual property is a math expression. Specs are designed to be written by humans, generated by AI, and rendered by anything.

Double Pendulum

{
  "spec": "equanim/0.1",
  "meta": { "title": "Double Pendulum", "duration": 30, "width": 600, "height": 600, "fps": 60, ... },
  "variables": {
    "L1": { "label": "Arm 1 length (m)", "default": 2.5, "min": 0.5, "max": 5.0 },
    "g":  { "label": "Gravity (m/s²)",   "default": 9.8, "min": 1.0, "max": 30.0 }
  },
  "scene": {
    "objects": [
      {
        "id": "phys", "type": "ode_system",
        "state": { "th1": 2.0, "w1": 0.0, "th2": 2.5, "w2": 0.0 },
        "derivatives": {
          "th1": "w1",
          "w1":  "(-g*(2*m1+m2)*sin(th1) - m2*g*sin(th1-2*th2) - 2*sin(th1-th2)*m2*(w2^2*L2+w1^2*L1*cos(th1-th2))) / (L1*(2*m1+m2-m2*cos(2*(th1-th2))))",
          "th2": "w2",
          "w2":  "(2*sin(th1-th2)*(w1^2*L1*(m1+m2)+g*(m1+m2)*cos(th1)+w2^2*L2*m2*cos(th1-th2))) / (L2*(2*m1+m2-m2*cos(2*(th1-th2))))"
        },
        "solver": "rk4", "step": 0.005
      },
      {
        "id": "arm1", "type": "line",
        "equations": {
          "x1": "0", "y1": "110",
          "x2": "L1*ppm*sin(phys_th1(t*d))", "y2": "110 - L1*ppm*cos(phys_th1(t*d))"
        },
        "timeline": { "start": 0.0, "end": 1.0 }
      }
    ]
  }
}

What it is

Equanim separates what an animation does from how it's rendered. A spec is pure data — no code, no runtime dependencies, no renderer assumptions. You hand it to a renderer and it plays.

The key idea: every number in every frame is computed from a math expression evaluated at time t. Want a circle that grows? r = 50 * t. Want a wave that decays? y = amplitude * exp(-decay * t) * sin(k * s - omega * t). The entire animation is a function.

Four time/duration variables are always available:

  • t — local normalised time, 0→1 over the object's own timeline window. Default choice: opacity = t always fades in over whatever duration the object is given.
  • d — local duration in seconds (length of the object's own window). Multiply to get local seconds: t * d.
  • root_t — global normalised time, 0→1 over the full animation. Use to sync effects across objects regardless of their individual windows.
  • root_d — total animation duration in seconds. Multiply to get global seconds: root_t * root_d.

Variables let template authors expose parameters as interactive controls. A renderer can show these as sliders, inputs, or anything else. Changing a variable updates every expression that references it — live, without reloading.


Repository layout

equanim/
├── spec.md              ← The spec. Source of truth for the JSON format.
├── renderer/            ← Reference renderer (TypeScript + Vite + mathjs)
│   ├── src/
│   │   ├── types.ts     ← TypeScript interfaces mirroring the spec schema
│   │   ├── evaluator.ts ← mathjs expression compiler
│   │   ├── render.ts    ← Coordinate transforms, per-frame draw logic
│   │   ├── player.ts    ← RAF loop, play/pause/seek state machine
│   │   └── main.ts      ← Entry point, variable sliders, drag-and-drop
│   └── specs/           ← Example spec JSON files
└── README.md

spec.md is the canonical document. If there's ever a conflict between the spec and the renderer, the spec wins.


Running the renderer

cd renderer
npm install
npx vite

Open http://localhost:5173. The renderer loads specs/double-pendulum.json on startup and loops automatically. Drag and drop any .json spec file onto the canvas to load it.


Running the tests

Each core module has a companion test file. Run them individually with npx tsx:

cd renderer
npx tsx src/evaluator.test.ts
npx tsx src/render.test.ts
npx tsx src/player.test.ts
npx tsx src/ode-solver.test.ts

279 assertions, all passing. Tests are plain TypeScript — no test framework dependency. If you add a feature, add tests.


Writing a spec

The full format is documented in spec.md. The short version:

  1. Every object has a timeline: { start, end } — fractions of the total duration, both in [0, 1].
  2. Every visual property is a string expression evaluated by mathjs.
  3. Expressions have access to t (local 0→1), d (local seconds), root_t (global 0→1), root_d (total seconds), s (parametric domain), any params or variables you define, and named functions.
  4. A variables block defines user-controllable parameters with default, min, max, and optional step.

The simplest possible spec:

{
  "spec": "equanim/0.1",
  "meta": {
    "title": "Dot",
    "duration": 2.0,
    "width": 400,
    "height": 400,
    "fps": 60,
    "coordinate_system": "cartesian",
    "origin": "center"
  },
  "scene": {
    "id": "root",
    "objects": [
      {
        "id": "line",
        "type": "line",
        "style": { "stroke": "#ffffff", "stroke_width": 2 },
        "equations": {
          "x1": "-100 * (1 - t)",
          "y1": "0",
          "x2": "100 * t",
          "y2": "0"
        },
        "timeline": { "start": 0, "end": 1 }
      }
    ]
  }
}

How to contribute

The project is in early development. The spec is not stable yet — we're deliberately keeping it loose while we add example specs and find rough edges.

The most useful contributions right now:

  • New example specs. Write a spec that does something interesting and surfaces a gap or awkwardness in the format. Good candidates: a bouncing ball, a text fade, a Lissajous figure, a bar chart animating in.
  • New primitive types. circle, rect, and text are the obvious next ones. See spec.md for the planned list. Adding a primitive means: updating spec.md, adding the TypeScript interface to types.ts, adding a prepare + draw path in render.ts, and adding tests.
  • Spec design feedback. If something in the format feels wrong or limiting — open an issue and explain what you were trying to express and what got in the way.
  • Alternative renderers. The TypeScript renderer is a reference implementation, not the only one. A Python renderer, a Swift renderer, a server-side PNG exporter — all useful, all welcome.

Before submitting a PR:

  • Run all three test files and confirm 0 failures.
  • If you changed the spec format, update spec.md first.
  • If you changed renderer behaviour, update or add tests. The test count going up is a good sign.
  • Keep PRs focused. One thing per PR is easier to review than three.

Opening issues:

There's a list of open design questions at the bottom of spec.md. If you have a view on any of them, an issue is the right place to hash it out before anyone writes code.


Design principles

Data, not code. A spec is JSON. It has no executable content. A renderer should be able to validate a spec's structure without running any of its expressions.

Renderer-agnostic. The spec says what the animation is. It doesn't say how to render it. The coordinate system is documented so any renderer can implement it consistently.

AI-friendly. The format is designed to be generated by a language model given only spec.md as context. Expressions are strings, not ASTs. The schema is flat and regular. This is intentional.

t-first. The default time variable is t (local, 0→1). This makes specs portable — changing an object's timeline.start and timeline.end changes when and how fast the animation plays, not just whether it's visible. When you need real seconds, multiply: t * d for local, root_t * root_d for global.


Roadmap

Phase 1 — Harden the spec (in progress)

The goal of this phase is to find where the format is weak before locking anything down. More example specs are the primary tool: each one should try to express something the existing examples don't, and surface gaps or awkwardness in doing so. Good candidates: Lissajous figure, orbiting system, bar chart animating in, easing demo.

  • Reference renderer — TypeScript + Vite + mathjs, canvas-based
  • parametric_path primitive
  • line primitive
  • circle primitive
  • Time variable system — t, d, root_t, root_d
  • Example specs — dampened wave, bouncing ball, double pendulum
  • ode_system node — RK4 numerical integration; state variables exposed as interpolators to sibling objects
  • More example specs — stress-test the format across different animation types
  • rect primitive — x, y, width, height as equations
  • Spec validator — structured errors on malformed specs, reserved identifier check
  • AI generation test — give spec.md to an LLM cold and see what it gets wrong

This phase ends when an LLM can generate a valid, interesting spec from spec.md alone without hand-holding.

Phase 2 — Composition

  • group primitive — container with its own timeline, exposing <group_id>_t and <group_id>_d to children
  • equanim-utils package — expression builder helpers (piecewise, lerp, easing functions) that output valid mathjs strings; separate from the renderer

Phase 3 — Export and distribution

  • Headless renderer — Node.js + canvas, frame-accurate offline rendering
  • Lottie exporter — compile Equanim specs to Lottie JSON for iOS, Android, and web playback
  • SVG path primitive — accept a static SVG d string as the curve shape, with equations controlling position, scale, and rotation; bridges the "designed in a tool" and "expressed mathematically" worlds

Phase 4 — Ecosystem

  • Community spec library — curated, searchable collection of open spec files (animations, templates, experiments)

  • Individual registry hosting — anyone can publish and share specs at a stable URL, importable by renderers and tools directly

  • equanim-utils package (may move earlier depending on community demand) — expression builder helpers for piecewise, lerp, and easing

  • text primitive — position, content, font size as equations; deferred because font metrics and layout are complex and don't stress-test the core expression system, but needed for completeness


Status

equanim/0.1 — early, unstable, evolving. The spec will change. Contributions that help stress-test the format are more valuable right now than contributions that polish the renderer.

About

A declarative, JSON-based animation specification where every visual property is a math expression. Specs are designed to be written by humans, generated by AI, and rendered by anything.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors