@openbone/export
Image, video, archive and sprite-sheet encoders for OpenBone exports, in plain TypeScript.
OpenBone is open-source 2D skeletal animation: Spine-style rigging,
Rive-style state machines and an MCP server for AI agents. This package turns rendered frames into files: PNG, APNG,
GIF, layered PSD, AVI and MOV (Motion-JPEG, optional PCM sound), ZIP, sprite sheets and ready-to-import engine packs
for Unity, Godot, Flutter and generic engines. It does not render. Pose frames with
@openbone/runtime and draw them with a renderer such as
@openbone/render-canvas, then pass the pixels here. The
editor’s Export dialog, the MCP server and openbone export in the
CLI all use these encoders. If you only need files, the CLI does the
rendering and encoding in one command.
The encoders have no native dependencies (GIF uses gifenc, MIT). They run in
browsers and in Node 20 or later. PNG and ZIP reading use the standard CompressionStream / DecompressionStream.
Install
Section titled “Install”npm install @openbone/exportpnpm add @openbone/exportyarn add @openbone/exportTo render frames in Node as in the quick start, also install @openbone/core, @openbone/runtime,
@openbone/render-canvas and a canvas implementation such as @napi-rs/canvas.
Quick start
Section titled “Quick start”Render one second of the Knight’s walk in Node and save it as a GIF, an APNG and a Godot sprite-sheet pack:
import { readFileSync, writeFileSync } from "node:fs";import { createCanvas, loadImage } from "@napi-rs/canvas";import { parseDocument, textureAssets } from "@openbone/core";import { type Frame, buildSpriteSheets, encodeApng, encodeGif, encodePng, enginePackZip } from "@openbone/export";import { CanvasRenderer } from "@openbone/render-canvas";import { OpenBoneInstance } from "@openbone/runtime";
const doc = parseDocument(readFileSync("knight.obone"));const images = new Map();for (const img of textureAssets(doc)) images.set(img.id, await loadImage(readFileSync(img.src)));
const scale = 0.5;const width = Math.round(doc.width * scale);const height = Math.round(doc.height * scale);const canvas = createCanvas(width, height);const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;const renderer = new CanvasRenderer(ctx, (id) => images.get(id));
const knight = new OpenBoneInstance(doc);knight.animationState.setAnimation(0, "walk", true, 0);const fps = 30;const frames: Frame[] = [];for (let i = 0; i < fps; i++) { knight.advance(i === 0 ? 0 : 1 / fps); renderer.render(knight.skeleton, { view: { x: 0, y: 0, zoom: scale }, background: null }); frames.push(ctx.getImageData(0, 0, width, height)); // RGBA pixels: a Frame}
writeFileSync("walk.png", await encodePng(frames[0]));writeFileSync("walk.apng", await encodeApng(frames, { delayMs: 1000 / fps }));writeFileSync("walk.gif", encodeGif(frames, { delayMs: 1000 / fps, transparent: true }));
const sheets = buildSpriteSheets([{ name: "walk", fps, loop: true, frames }], { origin: { x: doc.originX * scale, y: doc.originY * scale }, // the pivot of every frame});writeFileSync("knight-godot.zip", await enginePackZip("godot", sheets, { name: "knight" }));Image src paths in the example files are relative to the .obone file; run this next to it, or resolve them
yourself.
Frames
Section titled “Frames”Every image encoder takes Frame objects:
interface Frame { width: number; height: number; data: Uint8Array | Uint8ClampedArray; // RGBA, 8 bits per channel, not premultiplied, rows top to bottom}All frames passed to one call must have the same size. A 2D canvas ImageData has this layout, so
ctx.getImageData(...) can be passed as a frame directly.
| Export | Output |
|---|---|
encodePng(frame) |
Promise<Uint8Array>: a PNG file (8-bit RGBA) |
encodeApng(frames, { delayMs, loops? }) |
Promise<Uint8Array>: an animated PNG. loops 0 (default) repeats forever |
encodeGif(frames, { delayMs, loops?, transparent? }) |
An animated GIF, 256 colours per frame. With transparent, pixels under 50% alpha become transparent; otherwise frames should be opaque |
encodePsd(layers) |
A layered Photoshop file from { name, frame }[]: the first layer is visible, the rest hidden |
encodeAviMjpeg(jpegs, { width, height, fps, audio? }) |
An AVI with a Motion-JPEG video stream and optional 16-bit PCM sound |
encodeMovMjpeg(jpegs, { width, height, fps, audio? }) |
The same as a QuickTime MOV |
floatToPcm(channels, sampleRate) |
PcmAudio for the video encoders, from planar float channels (-1..1) |
createZip(entries, date?) |
A ZIP of { name, data }[] with stored (uncompressed) entries, e.g. a PNG sequence |
readZip(bytes) |
Promise<ZipEntry[]>: the files of a ZIP (stored or deflated entries) |
buildSpriteSheets(clips, options?) |
Packs frames into sheets (below) |
enginePackFiles(engine, set, options) / enginePackZip(...) |
Sprite-sheet packs for "unity", "godot", "flutter" or "generic" (below) |
webPackFiles(o) / webPackZip(o) / webIndexHtml(o) |
A website export around <openbone-player> (below) |
webViewPackFiles(platform, o) / webViewPackZip(...) / webViewHtml(page) |
WebView packages for "flutter", "android" and "ios" (below) |
texturePackerHash, texturePackerArray, phaserMultiAtlas, unitySheetJson, godotSpriteFrames, godotScene |
The individual metadata writers used by the engine packs |
trimBounds, cropFrame, frameName, safeName, crc32, zlibDeflate, toBase64 |
Small helpers |
The video encoders take finished JPEG files, one per frame, so you choose the JPEG encoder: canvas.toBlob(..., "image/jpeg") in a browser, or canvas.toBuffer("image/jpeg") with @napi-rs/canvas. Sound is interleaved with
the video, one chunk per frame.
import { writeFileSync } from "node:fs";import { encodeAviMjpeg, encodeMovMjpeg, floatToPcm } from "@openbone/export";
// jpegs: Uint8Array[], one JPEG file per frame; left, right: Float32Array samples at 44.1 kHzconst audio = floatToPcm([left, right], 44100);writeFileSync("clip.avi", encodeAviMjpeg(jpegs, { width: 450, height: 300, fps: 30, audio }));writeFileSync("clip.mov", encodeMovMjpeg(jpegs, { width: 450, height: 300, fps: 30 }));To get the sounds an animation plays with their times, use animationAudioSchedule() from @openbone/runtime.
Sprite sheets
Section titled “Sprite sheets”buildSpriteSheets(clips: SheetClipInput[], options?: SheetOptions): SpriteSheetSetEach clip is { name, fps, loop, frames }. Frames are named <clip>_0001, <clip>_0002, and so on.
| Option | Default | What it does |
|---|---|---|
trim |
true |
Trim transparent borders (the offset is kept in spriteSourceSize) |
padding |
2 |
Pixels between frames and around the sheet edge |
maxSize |
2048 |
Largest sheet size; more frames go on more sheets |
powerOfTwo |
false |
Round sheet sizes up to powers of two |
dedupe |
true |
Store pixel-identical frames once |
origin |
frame centre | Skeleton origin in frame pixels, used as every frame’s pivot |
alphaThreshold |
0 |
Alpha at or below this counts as transparent when trimming |
The result has pages (each with an RGBA image), frames (rect, trim and pivot per frame, in the TexturePacker
convention) and clips (frame names in play order).
Engine packs
Section titled “Engine packs”enginePackFiles(engine: "unity" | "godot" | "flutter" | "generic", set: SpriteSheetSet, options: EnginePackOptions): Promise<PackFile[]>enginePackZip(engine, set, options): Promise<Uint8Array> // everything inside a <name>/ folderOptions: name (file base name), scale (recorded in metadata, default 1), godotRoot (default
res://openbone/<name>/) and pixelsPerUnit (Unity, default 100).
| Engine | What the pack contains |
|---|---|
unity |
Sheet PNGs, TexturePacker JSON, a .unity.json description, an editor importer script (OpenBoneSpriteSheetImporter.cs) and a README |
godot |
Sheet PNGs, a SpriteFrames resource (.tres), a scene with an AnimatedSprite2D (.tscn), TexturePacker JSON and a README |
flutter |
Sheet PNGs, TexturePacker JSON, a Dart sprite-sheet loader and a Flame component, and a README |
generic |
Sheet PNGs, TexturePacker JSON (hash and array), a Phaser multi-atlas and a README |
Web and WebView packs
Section titled “Web and WebView packs”webPackZip and webViewPackZip wrap an exported document with the <openbone-player> script: a website (a ZIP
with a demo page and embed snippets, or one self-contained HTML file), or a WebView package for Flutter, Android or
iOS. You provide the document bytes and the player bundle source (playerCode or bundle); the editor and the MCP
server build that bundle with @openbone/player/bundle. See
engine exports for what each package
contains and how to use it.
Documentation
Section titled “Documentation”- Engine exports: sprite-sheet and WebView packs, engine by engine
- Architecture, exports: how the editor, CLI and MCP server share one export path
- MCP server: the
export_filetool - The editor: desktop app or run it from the repository
- Related packages:
@openbone/runtime,@openbone/render-canvas,@openbone/player,@openbone/cli,@openbone/core
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.