@openbone/canvaskit
OpenBone runtime for CanvasKit (Skia compiled to WebAssembly): a renderer and a player.
OpenBone is open-source 2D skeletal animation: Spine-style rigging,
Rive-style state machines and an MCP server for AI agents. This package draws OpenBone skeletons on any CanvasKit
canvas: a browser WebGL or software surface, or a headless surface in Node. CanvasKitPlayer adds a render loop,
fit, pointer input, state machines, events, skins, scripts and sound for an HTML canvas. Playback comes from
@openbone/engine-kit; drawing follows the batches of the
reference renderer, @openbone/render-webgl.
Install
Section titled “Install”npm install @openbone/canvaskit canvaskit-wasmcanvaskit-wasm (BSD-3-Clause) is a peer dependency (>=0.39.0). Its canvaskit.wasm file must be reachable by
CanvasKitInit’s locateFile. For the Node example, also install @openbone/runtime.
Quick start: in a page
Section titled “Quick start: in a page”import CanvasKitInit from "canvaskit-wasm";import { CanvasKitPlayer } from "@openbone/canvaskit";
const ck = await CanvasKitInit({ locateFile: (file) => `/canvaskit/${file}` }); // where you serve canvaskit.wasmconst canvas = document.querySelector<HTMLCanvasElement>("#stage")!; // give it a CSS size
const player = await CanvasKitPlayer.create(ck, canvas, "/knight.obone", { stateMachine: "knight", fit: "contain" });player.setInput("walk", true);player.on("stateChange", (e) => console.log(e.layer, e.from, "->", e.to));new ResizeObserver(() => player.resize()).observe(canvas);knight.obone is in the repository’s examples/ folder. The player draws at the device pixel ratio, fits the
artboard into the canvas and sends pointer events over it to the state machine.
Quick start: a PNG in Node
Section titled “Quick start: a PNG in Node”import { readFile, writeFile } from "node:fs/promises";import { pathToFileURL } from "node:url";import CanvasKitInit from "canvaskit-wasm";import { CanvasKitRenderer, loadOpenBone } from "@openbone/canvaskit";import { OpenBoneInstance } from "@openbone/runtime";
const ck = await CanvasKitInit();
// Node's fetch has no file: URLs, so hand loadOpenBone a fetch that reads files.const readFetch = (async (url: string | URL | Request) => new Response(await readFile(new URL(String(url))))) as typeof fetch;const file = pathToFileURL(process.argv[2] ?? "knight.obone");const asset = await loadOpenBone(ck, file, { fetch: readFetch }); // images resolve next to the file
const knight = new OpenBoneInstance(asset.doc);knight.play("idle");knight.advance(0.5);
const scale = 0.5;const surface = ck.MakeSurface(asset.doc.width * scale, asset.doc.height * scale)!;const renderer = new CanvasKitRenderer(ck, (id) => asset.images.get(id));const canvas = surface.getCanvas();canvas.clear(ck.TRANSPARENT);renderer.render(canvas, knight, { x: 0, y: 0, zoom: scale });surface.flush();await writeFile("knight-ck.png", surface.makeImageSnapshot().encodeToBytes()!);Over HTTP(S) no custom fetch is needed: loadOpenBone(ck, "https://cdn.example.com/knight.obone").
| Export | |
|---|---|
CanvasKitPlayer |
await CanvasKitPlayer.create(ck, canvas, src, options) plays a document on an HTML canvas. |
CanvasKitRenderer |
new CanvasKitRenderer(ck, images, { fonts? }); render(canvas, skeletonOrInstance, view?) draws one frame on any CanvasKit Canvas. |
loadOpenBone(ck, src, options) |
Loads a document and decodes its images (atlas pages when packed) with Skia. Works in browsers and Node. |
OpenBoneAsset |
{ doc, baseUrl, images } (CanvasKit images by image or atlas page id). Shareable by many players. |
PlayerOptions, CanvasKitRendererOptions, LoadOptions, View, DriverOptions, InputValue, OpenBoneSource |
Option and value types. |
src is a URL, bytes (ArrayBuffer or Uint8Array of a .obone or .obonb file), a parsed document or, for
CanvasKitPlayer.create, an OpenBoneAsset.
CanvasKitRenderer
Section titled “CanvasKitRenderer”render(canvas, target, view):viewis{ x, y, zoom }in canvas pixels (the artboard’s top-left lands atx, y). The canvas’s current matrix applies on top, so you can also position the skeleton withcanvas.concat. It does not clear the canvas.CanvasKitRenderer.matrix(doc, view): the skeleton-to-canvas matrix for a view.drawCalls:drawVerticescalls in the last render.dispose()frees shaders, paints and cached images.fonts(family, weight): return aTypefaceto draw text attachments natively with Skia. Without it, text is rasterised with a 2D canvas where one exists (so in Node, text is skipped unless you passfonts).
CanvasKitPlayer options
Section titled “CanvasKitPlayer options”| Option | Default | |
|---|---|---|
stateMachine / animation / loop |
first state machine, else first animation | What to run. |
skin |
default skin | Skin (outfit) by name or id. |
scripts |
"quickjs" when the document has scripts |
"none" or your own ScriptHost. |
audio |
true |
false or "muted". Sound unlocks on the first click or key press. |
fit |
contain |
contain, cover or none. |
background |
the document’s | A CSS colour; null for transparent. |
surface |
webgl |
webgl (falls back to software when WebGL is unavailable) or software. |
autoplay |
true |
Start the render loop right away. |
fonts |
As for the renderer. | |
baseUrl, fetch, resolveImageUrl |
Loading, as in loadOpenBone. |
|
audioContext, audioBaseUrl, resolveAudioUrl, unlockTarget, onEvent, onStateChange |
As in engine-kit. |
CanvasKitPlayer methods
Section titled “CanvasKitPlayer methods”| Member | |
|---|---|
setInput(name, value), getInput(name), fire(name) |
State machine inputs and triggers. |
playAnimation(name, loop?) |
Play an animation instead of the state machine. |
setSkin(name), setText(slot, text) |
Outfit, and text for a slot’s text attachment (null restores it). |
play(), pause(), playing |
The render loop; pausing also pauses sounds. |
resize() |
Call after the canvas changed size (the player does not observe it). |
draw() |
Draw the current pose once. |
boundingBoxes(), clientToSkeleton(clientX, clientY) |
Bounding-box queries in skeleton space (y up), and page coordinates to skeleton space through the fit: player.boundingBoxes().containsPoint(...player.clientToSkeleton(e.clientX, e.clientY)). |
on(name, fn) |
Listen to "event", "stateChange" or "audio". Returns an unsubscribe function. |
driver, instance, stateMachine, renderer, asset, ck, canvas |
Lower-level access (playback speed is driver.speed). |
dispose() |
Stops the loop and sounds, removes pointer listeners and frees the surface and renderer. |
How it draws
Section titled “How it draws”One drawVertices per render-webgl batch: positions, texture coordinates (scaled to the image) and per-vertex
colours modulating the image shader, so meshes, weights, tints, sequences and particles match the reference renderer.
- Two-colour tint: slots with a
darkcolour draw through a colour-matrix filter (rgb = dark + (light − dark) × rgb), onedrawVerticesper run of items with the same light and dark colour. Batches without one are untouched. - Blend modes map to Skia’s
SrcOver,Plus,MultiplyandScreen. - Clipping masks are anti-aliased clip paths.
- Premultiplied alpha: premultiplied atlas pages are labelled as premultiplied images, Skia’s own format, so they blend correctly without conversion.
- Particles use the built-in soft sprite.
From a checkout: pnpm --filter @openbone/examples engines, then open http://localhost:5190/canvaskit/
(?example=campfire, rocket, crash…).
Documentation
Section titled “Documentation”- Architecture: runtime model and rendering
- File format
- Demo source
- Related packages:
@openbone/engine-kit,@openbone/pixi,@openbone/phaser,@openbone/three,@openbone/render-canvas
License
Section titled “License”MIT, © UX Century and OpenBone contributors. CanvasKit is © Google, BSD-3-Clause. See LICENSE.
OpenBone is MIT licensed and made by UXCentury. The example art, voices and sounds are credited in the repository.