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 mascot without any custom motion. 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.

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.
  • 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.