Skip to content

Guides

Interactive canvases

State machines that map your app's events to your mascot's motions, blended at runtime.

A canvas is a small state machine that lives in the mascot's bundle. Each state plays an animation, inputs are values your app sets (booleans, numbers, one-shot triggers), and transitions move between states when their conditions hold, blending poses so nothing snaps. Your code never picks animations directly; it just reports what is happening.

lumorig companion

idle
$ claude "fix the flaky auth test"

The ai_agent template, running live. Press a button: the demo sets status 1, 2, 3 or 4 as a build progresses.

How canvases work#

  • The canvas starts in its initial state.
  • Whenever an input changes, transitions are checked in order; the first one whose from matches the current state (or is *) and whose conditions all hold wins. A transition into the state you are already in is skipped unless it is a trigger.
  • Triggers are momentary: fire("greet") is true for that one evaluation, then clears.
  • A non-looping state plays its clip once, then moves to its then state with a 0.2 s blend.
  • Each transition's blend (0–2 seconds) cross-fades from the pose the mascot was in, so interruptions look intentional.

Templates#

Three ready-made graphs cover the common cases. They only use presets, so they work on every rigged mascot without any custom motion, and on a clip mascot with the clips of those names. List them with GET/canvas-templates (no key needed).

ai_agent: AI coding agent#

Idle, working, waiting for you, done, failed. Wire it to Claude Code hooks or any agent loop.

InputTypeDefault
statusnumber0
StatePlaysThen
idle (initial)idleloops
workingworkingloops
waitingwaveloops
donecelebrateonce, then idle
failedsadonce, then idle

Transitions: idle when status = 0 · working when status = 1 · waiting when status = 2 · done when status = 3 · failed when status = 4.

chatbot: Chat assistant#

Idle, listening while the user types, thinking, talking while the reply streams.

InputTypeDefault
listeningbooleanfalse
thinkingbooleanfalse
talkingbooleanfalse
greettrigger—
StatePlaysThen
idle (initial)idleloops
listeninglistenloops
thinkingthinkloops
talkingtalkloops
greetingwaveonce, then idle

Transitions: greeting when greet fired · talking when talking is true · thinking when thinking is true and talking is false · listening when listening is true and thinking is false and talking is false · idle when listening is false and thinking is false and talking is false.

onboarding: Onboarding guide#

Waves hello, idles alongside the user, celebrates each completed step, and looks sad on errors.

InputTypeDefault
hellotrigger—
successtrigger—
errortrigger—
StatePlaysThen
idleidleloops
hello (initial)waveonce, then idle
successcelebrateonce, then idle
errorsadonce, then idle

Transitions: hello when hello fired · success when success fired · error when error fired.

Your own art: canvases on clip mascots#

A mascot made from your upload is a set of video clips of your exact art, and it runs the same canvases. Each state plays one of its clips. Every clip starts and ends on the rest frame it was drawn from, so a switch is a cut there (with a short fade through it), never a jump; a held clip (talk, think, sleep: enter, a loop held while the state is active, exit) stays as long as its state does and plays its exit on the way out.

  • The templates run as they are, with two substitutions, since no clip motion is called that: working plays the think clip, and listen rests on the idle (a clip mascot listens by being attentive at rest).
  • A canvas that plays clips the character doesn't have yet is refused with 422 clips_needed; error.details.missing names them and credits says what they cost (60 each). Make them, then create the canvas; in the studio's Interact tab one press does both. The canvas is accepted as soon as the clips exist, even while they are still being made: until a clip lands, its state rests on the idle.
  • A looping state on a one-shot clip (a wave while "waiting for you") plays the clip again at each rest frame. A transition's blend only decides urgency here: above 0 switches now, 0 lets the playing clip reach its own rest frame first.

Create a canvas#

POST/mascots/{id}/canvases

NameTypeDescription
namerequiredstring1–60 characters. This is the name you pass to the player's canvas attribute.
templatestringai_agent, chatbot or onboarding.
graphCanvasGraphA custom graph (see Graph format). If you send both, the graph wins.

One of template or graph is required. Canvases are free: no model call, no credits. The response is the canvas with its full graph.

curl -X POST https://lumorig.com/api/v1/mascots/msc_…/canvases -H "Authorization: Bearer $LUMORIG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "assistant", "template": "chatbot" }'
# 201 → { "id": "cnv_…", "object": "canvas", "name": "assistant", "template": "chatbot", "graph": { … } }

Updating and deleting#

EndpointDoes
GET/mascots/{id}/canvasesEvery canvas on the mascot.
PATCH/mascots/{id}/canvases/{canvasId}Rename (name) or replace the graph. The new graph is validated.
DELETE/mascots/{id}/canvases/{canvasId}Remove it from the mascot and its bundle.

Drive it from your app#

Every canvas ships inside the mascot's bundle, keyed by name. Set the canvas attribute on the player, then call setInput and fire as things happen. Listen for statechange if your UI wants to follow along.

chat.ts · Chat assistant
const pip = document.querySelector("lumorig-player");
pip.setAttribute("canvas", "assistant");

pip.fire("greet");                                   // waves once, back to idle
input.addEventListener("focus", () => pip.setInput("listening", true));
chat.on("request", () => pip.setInput("thinking", true));
chat.on("token", () => pip.setInput("talking", true));
chat.on("done", () => {
  pip.setInput("thinking", false);
  pip.setInput("talking", false);
  pip.setInput("listening", false);
});

pip.addEventListener("statechange", (e) => console.log(e.detail.state));
or declaratively
<lumorig-player src="…/bundle" canvas="assistant"></lumorig-player>

pip.canvas = "assistant" and pip.setAttribute("canvas", "assistant") are equivalent; the property mirrors the attribute.

AI agents and the terminal#

The ai_agent template takes a single number, status: 0 idle, 1 working, 2 waiting for you, 3 done, 4 failed. That is exactly what the terminal companion sets from Claude Code hooks, and you can set it from any agent loop the same way.

Graph format#

A CanvasGraph is plain JSON. Names (inputs and state ids) are snake_case. This is the onboarding template in full:

CanvasGraph
{
  "canvasVersion": 1,
  "inputs": [
    {
      "name": "hello",
      "type": "trigger",
      "default": null
    },
    {
      "name": "success",
      "type": "trigger",
      "default": null
    },
    {
      "name": "error",
      "type": "trigger",
      "default": null
    }
  ],
  "states": [
    {
      "id": "idle",
      "animation": "idle",
      "loop": true,
      "then": null
    },
    {
      "id": "hello",
      "animation": "wave",
      "loop": false,
      "then": "idle"
    },
    {
      "id": "success",
      "animation": "celebrate",
      "loop": false,
      "then": "idle"
    },
    {
      "id": "error",
      "animation": "sad",
      "loop": false,
      "then": "idle"
    }
  ],
  "initial": "hello",
  "transitions": [
    {
      "from": "*",
      "to": "hello",
      "when": [
        {
          "input": "hello",
          "op": "fired",
          "value": null
        }
      ],
      "blend": 0.2
    },
    {
      "from": "*",
      "to": "success",
      "when": [
        {
          "input": "success",
          "op": "fired",
          "value": null
        }
      ],
      "blend": 0.15
    },
    {
      "from": "*",
      "to": "error",
      "when": [
        {
          "input": "error",
          "op": "fired",
          "value": null
        }
      ],
      "blend": 0.2
    }
  ]
}
CanvasGraph fields
NameTypeDescription
canvasVersionrequired1Format version.
inputs[]requiredobject[]name, type (boolean · number · trigger) and default (null for triggers).
states[]requiredobject[]At least one. id, animation (a preset id or a saved animation's name), loop, and then (where a non-looping state goes next, else null).
initialrequiredstringThe state id to start in.
transitions[]requiredobject[]from (a state id or *), to, when (one or more conditions, all must hold) and blend (seconds, 0–2).

Conditions#

opInput typeHolds when
is_truebooleanThe input is true.
is_falsebooleanThe input is false (or unset).
eq · gt · ltnumberThe input equals, exceeds or is below value.
firedtriggerThe trigger was just fired.

Validation rules#

Graphs are checked when you create or update a canvas. Problems come back as 400 invalid_request with one line per problem in error.details:

  • initial must be one of the states.
  • Every state's animation must exist on the mascot: a preset id, or the id or name of a saved animation; on a clip mascot, the name of one of its clips (or idle). Missing clips come back as 422 clips_needed instead, naming them (see above).
  • A non-looping state needs then, and then must be a real state.
  • Every transition's from (unless *) and to must be real states, and every condition must name a declared input.
  • fired needs a trigger input; eq, gt and lt need a number input and a non-null value.