@openbone/runtime
The reference OpenBone runtime: poses skeletons, mixes animations and runs state machines. It does not render.
OpenBone is open-source 2D skeletal animation: Spine-style rigging,
Rive-style state machines and an MCP server for AI agents. This package takes a document loaded with
@openbone/core and turns it into a pose every frame: bones,
constraints (IK, transform, path, physics), animation tracks with cross-fades, state machines with inputs and pointer
listeners, events, particles, text and audio commands. The result is a renderer-agnostic draw list. Draw it with
@openbone/render-webgl,
@openbone/render-canvas or an engine package
(PixiJS, Phaser,
Three.js, CanvasKit),
or use @openbone/player, which does all of it for you.
The runtime has no DOM, audio or Node dependencies. It runs the same in browsers, Node and workers. The native runtimes (C++, C#, Java, Swift, Flutter, Haxe) are ports of this one and are tested against its output.
Install
Section titled “Install”npm install @openbone/core @openbone/runtimepnpm add @openbone/core @openbone/runtimeyarn add @openbone/core @openbone/runtimeAdd @openbone/scripting if your documents use scripts.
Quick start
Section titled “Quick start”import { readFileSync } from "node:fs";import { parseDocument } from "@openbone/core";import { OpenBoneInstance } from "@openbone/runtime";
const knight = new OpenBoneInstance(parseDocument(readFileSync("knight.obone")));
knight.play("walk"); // loops by defaultfor (let i = 0; i < 30; i++) knight.advance(1 / 60);
const items = knight.drawList(); // back to front: world vertices, uvs, indices, colour, blend modeconsole.log(items.length, items[0].slot.name);
const head = knight.skeleton.findBone("head")!;console.log(head.worldX, head.worldY, head.worldRotation);In a browser, load the bytes with fetch instead: parseDocument(new Uint8Array(await res.arrayBuffer())).
| Export | What it does |
|---|---|
OpenBoneInstance |
One skeleton driven by an AnimationState or a state machine. The usual entry point |
Skeleton |
Bones, slots, skins and constraints; solves world transforms and builds the draw list |
AnimationState / TrackEntry |
Plays animations on numbered tracks with cross-fades and a queue |
StateMachineInstance |
Runs a state machine: inputs, layers, transitions, listeners, scripts |
AudioDirector, AudioCommand |
Turns sound triggers into play / stop / fade commands for an audio backend |
ScriptHost, ScriptApi, trustedFunctionHost |
The script engine interface, and an unsandboxed host for trusted content |
applyAnimation, seekAnimation, eventsBetween |
Low-level timeline sampling and deterministic posing |
animationAudioSchedule |
Every sound an animation plays, with times (for rendering audio offline) |
ParticleEmitter, PARTICLE_DOT_TEXTURE, particleDotAlpha |
Particle simulation and the built-in soft dot sprite renderers draw |
drawTextBox, cssFont, cssColor |
Helpers for renderers that draw text attachments on a 2D canvas |
solvePose, poseChainFrom |
Drag-to-pose IK used by editor tools |
| Types | DrawItem, FiredEvent, TextDraw, ClipRegion, StateMachineOptions, … |
OpenBoneInstance
Section titled “OpenBoneInstance”new OpenBoneInstance(doc: OpenBoneDocument, options?: { scriptHost?: ScriptHost | null })| Member | What it does |
|---|---|
play(animation, loop = true, track = 0) |
Play an animation by name or id (stops the state machine); returns the TrackEntry |
useStateMachine(name?) |
Start a state machine by name or id (default: the first one); returns the StateMachineInstance |
advance(dt) |
Step time by dt seconds and pose the skeleton |
pointer(type, x, y) |
Forward "down", "up" or "move" in skeleton space to the state machine |
artboardToSkeleton(px, py) |
Convert artboard pixels (y-down, top-left origin) to skeleton space (y-up) |
drawList() |
The DrawItem[] for the current pose |
on(type, fn) |
Subscribe to "event", "stateChange" or "audio"; returns an unsubscribe function |
dispose() |
Stop every sound and release the state machine’s scripts |
skeleton, animationState, stateMachine, doc |
The parts it drives |
Time stepping
Section titled “Time stepping”Call advance(dt) once per frame with the elapsed seconds. Each frame the runtime resets to the setup pose, applies
the animation tracks or state-machine layers, then script overrides and input bindings, solves bones and constraints,
and builds the draw list. Physics constraints and particles run at fixed steps inside advance, so the same
sequence of dt values always gives the same result.
Animations and mixing
Section titled “Animations and mixing”import { readFileSync } from "node:fs";import { parseDocument } from "@openbone/core";import { OpenBoneInstance } from "@openbone/runtime";
const inst = new OpenBoneInstance(parseDocument(readFileSync("knight.obone")));const state = inst.animationState;state.defaultMix = 0.25; // cross-fade seconds (default 0.2)
state.on("complete", (entry, track) => console.log("done", track, entry.animation.name));state.setAnimation(0, "idle");state.addAnimation(0, "attack", false, 1); // after 1 s on track 0state.addAnimation(0, "idle"); // once attack completes
for (let i = 0; i < 180; i++) inst.advance(1 / 60);setAnimation(track, anim, loop = true, mix = defaultMix)replaces what a track plays, cross-fading overmixseconds.addAnimation(track, anim, loop = true, delay = 0, mix = defaultMix)queues an animation. It starts afterdelayseconds, or when the current one completes (or finishes a loop) ifdelayis 0.- Higher tracks are applied on top of lower ones.
clearTrack(track)andclearTracks()stop them. TrackEntryhasspeed,alpha,time,localTime,durationandisComplete.AnimationStateevents:start,complete,eventandaudio.
State machines and inputs
Section titled “State machines and inputs”import { readFileSync } from "node:fs";import { parseDocument } from "@openbone/core";import { OpenBoneInstance } from "@openbone/runtime";
const walker = new OpenBoneInstance(parseDocument(readFileSync("walker.obone")));const sm = walker.useStateMachine("character");
sm.on("stateChange", ({ layer, from, to }) => console.log(`[${layer}] ${from} -> ${to}`));walker.on("event", (e) => console.log("event", e.name, e.time));
sm.setInput("walk", true);for (let i = 0; i < 60; i++) walker.advance(1 / 60);sm.fire("jump");for (let i = 0; i < 90; i++) walker.advance(1 / 60);console.log(sm.activeStates); // { "Layer 1": "locomotion" }
// A click at artboard pixel (400, 300): listeners hit-test slots in skeleton space.const [x, y] = walker.artboardToSkeleton(400, 300);walker.pointer("down", x, y);walker.pointer("up", x, y);walker.advance(1 / 60);StateMachineInstance member |
What it does |
|---|---|
setInput(name, value), setBool, setNumber, setString |
Set an input by name or id. Values are coerced to the input’s type |
fire(name) |
Fire a trigger input for the next advance |
getInput(name), inputs |
Read input values (inputs is keyed by input id) |
activeStates |
Current state name per layer |
pointer(type, x, y) |
Queue a pointer event in skeleton space; handled on the next advance |
setText(slot, text) |
Show fixed text or a template in a text attachment; null restores its own |
setEmitter(slot, emitting) |
Switch a particle emitter on or off |
clearOverrides() |
Clear bone and slot overrides set by scripts |
| Events | stateChange, inputChange, event, audio, log and error (script output and failures) |
Inputs are bool, number, string or trigger. A state’s entry can set inputs too, so set an input after the
first advance if the entry state also sets it. Throws when the named input does not exist.
Scripts only run when you pass a scriptHost. Without one, they are skipped and the runtime prints a warning once per
document; pass scriptHost: null to run without scripts on purpose, with no warning. Use
createQuickJsHost() from @openbone/scripting for any content
you did not write. trustedFunctionHost runs scripts with new Function in your own realm and is not a sandbox.
Events and audio commands
Section titled “Events and audio commands”Events come from animation keys, listeners and scripts as FiredEvent: { name, id, time, int, float, string, animation, audio?, volume? }.
The runtime never plays sound. It emits AudioCommands that an audio backend turns into sound (the
@openbone/player package has a Web Audio one):
{ type: "play", id, sound, asset, bus, volume, rate, loop, fadeIn, source }{ type: "stop", id, fade }{ type: "fade", id, volume, duration }
volume does not include the bus and master gain; the backend applies those from doc.audio. Each track entry and
state owns the sounds it starts, and its looping sounds stop when it ends.
walker.on("audio", (cmd) => { if (cmd.type === "play") console.log("play", cmd.sound, "from", cmd.source); // e.g. "key:jump", "event:land"});Skins, text, particles and physics
Section titled “Skins, text, particles and physics”import { readFileSync } from "node:fs";import { parseDocument } from "@openbone/core";import { OpenBoneInstance, Skeleton, seekAnimation } from "@openbone/runtime";
// Text: templates such as "{multiplier:0.00}x" read the running state machine's inputs by name.const crash = new OpenBoneInstance(parseDocument(readFileSync("crash.obone")));const game = crash.useStateMachine();crash.advance(0); // enter the entry state, which sets its own input valuesgame.setNumber("multiplier", 2.5);game.setText("button-label", "CASH OUT");crash.advance(1 / 60);for (const item of crash.drawList()) if (item.text) console.log(item.slot.name, item.text.content); // "2.50x", ...
// Particles advance in real time with advance(dt); bursts can come from code.const rocket = new OpenBoneInstance(parseDocument(readFileSync("rocket.obone")));rocket.play("launch");rocket.skeleton.emitParticles("sparks", 20);for (let i = 0; i < 30; i++) rocket.advance(1 / 60);
// Deterministic posing at any time (exports, scrubbing): the same particles for the same time.const doc = parseDocument(readFileSync("rocket.obone"));const sk = new Skeleton(doc);seekAnimation(sk, doc.animations.find((a) => a.name === "launch")!, 1.5);
// Placement, skins and physics.sk.x = 100;sk.scaleX = -1; // mirrorsk.setSkin(null); // a skin by name or id; null = only the default skinsk.resetPhysics(); // next frame starts at rest on the animated poseEach DrawItem holds a slot, world vertices, uvs, indices, color, optional dark (two-colour tint),
blend, the texture to bind (image or atlas page), and optional clip (clipping mask), colors and particles
(particle batches) and text (text attachments).
Bounding boxes
Section titled “Bounding boxes”instance.boundingBoxes() (or skeleton.boundingBoxes()) is a snapshot of the active bounding boxes, the slots
whose current attachment is a bounding box, in draw order, in skeleton space (y up: the coordinates of pointer()
and the draw list). Take a new one, or call update(skeleton) on it, after each advance.
const hero = new OpenBoneInstance(doc);hero.advance(1 / 60);const boxes = hero.boundingBoxes();for (const b of boxes.boxes) console.log(b.slot.data.name, b.attachment, b.vertices); // world x,y pairsconsole.log(boxes.minX, boxes.minY, boxes.maxX, boxes.maxY); // overall AABB (all 0 with no box)
const [x, y] = hero.artboardToSkeleton(px, py); // artboard pixels (y down) → skeleton spaceboxes.containsPoint(x, y); // first box containing the point, or null (containsPointAll: every one)boxes.intersectsSegment(x1, y1, x2, y2); // first box the segment crosses or lies in (intersectsSegmentAll)boxes.aabbIntersects(minX, minY, maxX, maxY); // overall AABB against a rectangleboxes.intersectsBounds(enemy.boundingBoxes()); // [mine, theirs] for the first overlapping pair, or nullBounding boxes can be weighted to bones (autoWeights, setWeights) like meshes and paths, so they bend with the
rig. Listeners in state machines use a wider hit area (they also fall back to a slot’s region or mesh).
Skeleton without OpenBoneInstance
Section titled “Skeleton without OpenBoneInstance”sk.setToSetupPose();// apply animations here, e.g. applyAnimation(sk, anim, time, alpha)sk.update(dt); // feeds physics and particlessk.updateWorldTransform(); // bones, then constraints in their orderconst items = sk.drawList();Documentation
Section titled “Documentation”- Runtime specification: the exact per-frame order, maths and state-machine rules
- Architecture: runtime model, rendering, audio and scripts
- File format: what a document contains
- The editor: desktop app or run it from the repository
- Related packages:
@openbone/core,@openbone/scripting,@openbone/player,@openbone/render-webgl,@openbone/render-canvas
License
Section titled “License”MIT, © UX Century and OpenBone contributors. See LICENSE.
OpenBone is MIT licensed and made by UXCentury. The example art, voices and sounds are credited in the repository.