Build a canvas
Define how your mascot reacts with a graph of poses, transitions and conditions.
What is a Canvas?
A state machine: each node is a pose (image or animation), and each edge is a video played when moving between nodes once its conditions match. Conditions usually check resolved inputs such as behavior::interact, behavior::attention or node::nodeTime. The Masko player plays a release of the canvas in your app.
Nodes & Edges
Nodes have a stable id, an assetId from a completed pose in this mascot (replace the example UUIDs), an itemName label and a position on the editor grid.
Edges connect a source node to a target node and can set:
- conditions: rules that trigger the transition.
- priority: the highest number wins when several edges can fire.
- speed: playback multiplier, default 1 (1.5 is faster, 0.5 slower).
- speedInput: the name of a
numberinput ingraph.inputsthat the speed follows: the clip plays atspeedtimes the value your app sets, kept between 0.2 and 4, soset('tempo', 1.3)speeds up the clips tied to it. Set it withPATCH /v1/canvases/CANVAS_ID/edges/EDGE_IDand{ "speedInput": "tempo" };nullremoves it. - duration: video length in seconds, used for generation.
- description: the animation prompt for the transition video.
An edge whose source and target are the same node is an animation that starts and ends on that pose's picture. A node can have several. The first one without conditions is the pose's loop. Each time an animation ends, the player plays a move whose conditions hold, else the first of the pose's own animations whose conditions hold, else the loop, which gives way now and then to another of its animations without conditions. An input your app holds on (behavior::working and action::reading together) plays its animation again and again; one it sends once plays it once.
- reverse: plays another edge's video backwards instead of generating a new one.
source: "*" (any state) fires from any node, for global triggers such as an error state or reset.
Conditions & Inputs
A condition has an input name (such as "behavior::interact"), an op ("==", "!=", ">", "<", ">=", "<=") and a value. Input types:
- boolean, such as
behavior::interactorbehavior::working. - number, such as
node::nodeTimeornode::loopCount. - trigger: fire-once events. New desktop graphs should use resolved behavior and action inputs instead.
Create a Canvas
Create an empty canvas with POST /v1/canvases (project_id, mascot_id, name; see the canvas guide), then PATCH its graph with the returned hash:
curl -X PATCH https://api.masko.ai/v1/canvases/CANVAS_ID \
-H "Authorization: Bearer masko_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expected_graph_hash": "GRAPH_HASH_FROM_CREATE",
"graph": {
"nodes": [
{
"id": "idle",
"x": 0,
"y": 0,
"itemName": "Idle",
"assetId": "6aec5b1a-cb7f-5f10-8d6e-235ec16b4d97"
},
{
"id": "waving",
"x": 300,
"y": 0,
"itemName": "Waving",
"assetId": "18d4fd5e-7193-57f1-8af9-f3bdff007783"
}
],
"edges": [
{
"id": "edge_001",
"source": "idle",
"target": "waving",
"duration": 4,
"description": "transitioning from idle to waving",
"conditions": [
{
"input": "behavior::attention",
"op": "==",
"value": true
}
]
},
{
"id": "edge_002",
"source": "waving",
"target": "idle",
"duration": 4,
"description": "returning from wave to idle",
"conditions": [
{
"input": "behavior::rest",
"op": "==",
"value": true
}
],
"reverse": true,
"reverseOfEdgeId": "edge_001"
}
],
"viewport": {
"x": 0,
"y": 0,
"zoom": 0.8
},
"inputs": [
{
"name": "behavior::attention",
"type": "boolean",
"default": false
},
{
"name": "behavior::rest",
"type": "boolean",
"default": true
}
],
"initialNode": "idle"
}
}'Update the Graph
PATCH replaces the complete graph (nodes, edges, inputs, viewport). Fetch the canvas first and send its graph_content_hash as expected_graph_hash; a stale hash returns 409 and leaves the newer graph untouched.
Delete a Canvas
Removes the graph; its items and generated assets stay in the mascot. A canvas with checkpoints or releases returns 409 conflict.
Example: Sleep/Wake Canvas
Two states: the mascot sleeps by default and wakes on behavior::interact, which the desktop runtime resolves from direct interaction.
curl -X PATCH https://api.masko.ai/v1/canvases/CANVAS_ID \
-H "Authorization: Bearer masko_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sleep Wake",
"graph": {
"nodes": [
{
"id": "sleeping",
"x": 0,
"y": 0,
"itemName": "Sleeping",
"assetId": "d81e7218-4d80-548c-bcfc-968ccf1d5d80"
},
{
"id": "awake",
"x": 400,
"y": 0,
"itemName": "Awake",
"assetId": "cf03e485-3d80-54a1-8e5f-4d038116c25c"
}
],
"edges": [
{
"id": "edge_wake",
"source": "sleeping",
"target": "awake",
"duration": 4,
"description": "waking up with a stretch and yawn",
"conditions": [
{
"input": "behavior::interact",
"op": "==",
"value": true
}
],
"priority": 10
},
{
"id": "edge_sleep",
"source": "awake",
"target": "sleeping",
"duration": 4,
"description": "slowly falling asleep, eyes drooping",
"conditions": [
{
"input": "behavior::rest",
"op": "==",
"value": true
}
],
"priority": 10
}
],
"viewport": {
"x": 0,
"y": 0,
"zoom": 0.8
},
"inputs": [
{
"name": "behavior::interact",
"type": "boolean",
"default": false
},
{
"name": "behavior::rest",
"type": "boolean",
"default": true
}
],
"initialNode": "sleeping"
}
}'Validate before generating
curl https://api.masko.ai/v1/canvases/CANVAS_ID/validate \
-H "Authorization: Bearer $MASKO_API_KEY"The response has data.valid, data.issues (each with code, message, severity and optional node or edge ID) and data.summary.errors / warnings. HTTP 200 means the check ran, not that the graph is valid. Fix structural errors before paid generation; missing-video issues clear once generation finishes, so validate again before publishing.
Validation checks only the stored graph. Use media readiness to know whether the character can play.
