OpenBone for Flutter and Dart
openbone is a Flutter FFI plugin that plays OpenBone documents (.obone JSON and .obonb binary): skeletal
animation, meshes, constraints, physics, particles, text, and Rive-style state machines with inputs, pointer listeners,
events, audio commands and scripts. It is a thin dart:ffi wrapper over the native C++ runtime
(runtimes/cpp, C API openbone.h), so it poses and reacts exactly like the
other runtimes: flutter test runs all 92 cases of the conformance suite through the
Dart API.
| Platforms | Android (API 24+), iOS 13+, macOS 10.15+, Linux, Windows. Not web (no dart:ffi; use @openbone/player) |
| Native build | Android, Linux, Windows: CMake (src/CMakeLists.txt builds runtimes/cpp). iOS, macOS (one shared darwin/ folder): Swift Package Manager (darwin/openbone/Package.swift; a dynamic library embedded as openbone.framework) or CocoaPods (darwin/openbone.podspec, through forwarders in darwin/Classes/) compile the same sources |
| Bindings | lib/src/bindings.g.dart, generated by ffigen (MIT) from openbone.h; dart run ffigen --config ffigen.yaml regenerates them |
| Dependencies | ffi (BSD-3). Native: nlohmann/json (MIT, vendored in runtimes/cpp), QuickJS-ng 0.10.1 (MIT) |
| License | MIT |
Use it
flutter pub add openbone
flutter:
assets:
- assets/hero/ # hero.obone and the images it references (relative paths)
The package carries the C++ runtime's sources (in cpp/, with nlohmann/json and QuickJS) and compiles them with each
app's native build, so there is nothing else to install. To track the repository instead of a release, depend on it by
git:
dependencies:
openbone:
git:
url: https://github.com/UXCentury/open-bone
path: runtimes/flutter/openbone
import 'package:openbone/openbone.dart';
final controller = OpenBoneController();
OpenBoneWidget.asset(
'assets/hero/hero.obone',
controller: controller,
stateMachine: 'main', // or animation: 'walk'; neither = first state machine, else first animation
fit: BoxFit.contain,
);
controller.setNumber('speed', 0.8);
controller.setBool('armed', true);
controller.fire('jump');
controller.play('wave', loop: false, track: 1);
controller.events.listen((e) => print('${e.name} ${e.stringValue}'));
controller.audioCommands.listen((c) => print('${c.type} ${c.sound}')); // the commands the widget plays (or your own engine)
controller.stateChanges.listen((s) => print('${s.layer}: ${s.from} -> ${s.to}'));
OpenBoneWidget plays the document's sounds itself (audio: true, the default) with
flutter_soloud (MIT): sound files are found like images (next to the document
or embedded), and voices follow the command's volume × bus × master, rate, looping, fade-in, stop fades and fades. Set
audio: false to play controller.audioCommands with your own engine, or use OpenBoneAudio(document) directly with
an OpenBoneInstance. List the sound files in your pubspec.yaml assets like the images. Requires Flutter 3.41+.
Taps, drags and mouse hover on the widget become pointer events for the state machine's listeners (interactive: false turns that off). Calls on the controller before the document has loaded are queued. OpenBoneWidget.file(path)
loads from disk and OpenBoneWidget.document(doc) takes an already loaded document; imageLoader: overrides how
texture bytes are found (default: data: URLs, then the asset bundle or files next to the document).
Without the widget (pure Dart)
package:openbone/openbone_core.dart has no Flutter imports:
final doc = OpenBoneDocument.file('hero.obone'); // or OpenBoneDocument.bytes(bytes)
final inst = doc.createInstance(); // scripts: true (QuickJS) by default
inst.events.listen(print);
inst.useStateMachine('main');
inst.pointer(OpenBonePointerType.down, x, y); // skeleton space, y up; doc.artboardToSkeleton converts
inst.advance(1 / 60);
for (final item in inst.drawList().items) { /* vertices, uvs, indices, color, dark, clip, texture… */ }
inst.dispose();
doc.dispose();
OpenBoneDocument exposes size and origin, animations (name, duration), skins, state machines and their inputs, and
the textures a renderer needs. OpenBoneInstance adds setSkin, setTransform, play / queue / clearTracks,
setBool / setNumber / setString / setInput / fire / getInput, activeStates, setText, bones, slots,
findBone, findSlot, drawList(), bounds and boundingBoxes(). Failures throw OpenBoneException with the
runtime's message.
inst.boundingBoxes() returns an OpenBoneSkeletonBounds snapshot of the current pose (like Spine's SkeletonBounds;
docs/runtime-spec.md §8.10): boxes (OpenBoneBoundingBox: index, slot, attachment, world vertices as a
Float32List, own min/max) for every slot in draw order whose bone is active and whose current attachment is a bounding
box, and aabb (null with no box). Queries in skeleton space: aabbContainsPoint, aabbIntersects, containsPoint /
containsPointAll, intersectsSegment / intersectsSegmentAll, and intersectsBounds(other) → (mine, theirs)?
between two snapshots (for example two characters). Call update(inst) or take a new snapshot after the pose changes;
dispose() frees it at once (a NativeFinalizer otherwise).
inst.advance(1 / 60);
final hits = inst.boundingBoxes();
final hit = hits.containsPoint(x, y); // null when nothing is hit
hits.dispose();
Path constraints with constantSpeed: false in the document place bones at the raw bezier parameter inside each
segment instead of by arc length (Spine's "constant speed" off); unset or true keeps arc-length placement.
Memory and threads
Documents and instances own native memory. dispose() frees it at once; anything left undisposed is freed by a
finalizer when garbage collected (a NativeFinalizer for documents; a Finalizer for instances, which also closes the
callback trampolines). Instances keep their document's native data alive, so disposing a document while instances run
is safe. Callbacks arrive synchronously inside advance on the calling isolate (NativeCallable.isolateLocal); use an
instance from one isolate at a time.
Rendering
OpenBoneWidget advances the instance from a Ticker (frame delta × controller.speed, clamped to 0.1 s), rebuilds
the draw list and paints it with Canvas.drawVertices (OpenBoneRenderer, also usable
on any canvas):
- textured items: an
ImageShaderper texture, vertex colours × texture (BlendMode.modulate); per-vertex colours for particles; the built-in particle dot is generated fromob_particle_dot_alpha; - blend modes: normal →
srcOver, additive →plus, multiply →multiply, screen →screen; - two-colour tint (
dark): a second pass addsdark × (1 − texture.rgb)(a colour-matrix filter,plus), which givesdark + (color − dark) × texturefor normal-blend items; - clipping:
canvas.clipPathwith the mask polygon around the items inside it; - premultiplied atlas pages are un-premultiplied once when loaded (the image decoder assumes straight alpha);
- shapes: filled triangles plus an optional stroke; text:
TextPainterplaced with the item's box matrix (fonts come from the app: the document's font family is used by name, falling back to the platform font).
Building and testing here
cd runtimes/flutter/openbone
flutter pub get
tool/build_native.sh # libopenbone for the host → .dart_tool/openbone_native/
flutter test # conformance (92 cases), API, widget + golden tests
flutter analyze
cd example && flutter run -d linux # the example app (knight + button)
Tests find the library through OPENBONE_LIBRARY=/path/to/libopenbone.so or .dart_tool/openbone_native/. The
golden (test/goldens/knight_idle.png) is compared on Linux only; flutter test --update-goldens regenerates it.
Scripts run in QuickJS (quickjs-ng 0.10.1, MIT), vendored in runtimes/cpp/third_party/quickjs, so every build has
them (OpenBoneNative.hasScripts).
Builds use -ffp-contract=off like runtimes/cpp (and the runtime sources disable contraction with a pragma too), so
ARM64 devices match the reference bit for bit.
Publishing
pub.dev packages cannot reach outside their folder, and this one compiles runtimes/cpp through relative paths and
symlinks, so it is published from a staged copy (publish_to: none here keeps this folder from being published):
tool/bundle.sh /tmp/openbone-pub/openbone # copies runtimes/cpp into cpp/, replaces symlinks, appends licences
cd /tmp/openbone-pub/openbone && flutter pub publish --dry-run
src/CMakeLists.txt and darwin/openbone.podspec use cpp/ when it is there and runtimes/cpp otherwise. The
Publish to pub.dev workflow (.github/workflows/publish-pub.yml) does this on a version tag.
What has been checked
- Linux x64:
flutter test(all 92 conformance cases, API tests, widget tests with a golden),flutter analyze, and the example app built withflutter build linuxand run under xvfb (--dart-define=OPENBONE_SMOKE=trueloads the button, presses it, prints a status line and exits). - The iOS/macOS source layout: the
darwin/Classesforwarders compiled with clang and the podspec's flags into a shared library pass all 82 cases too. CocoaPods / Xcode themselves were not run (no macOS here). - Android:
flutter build apkbuildslibopenbone.sofor arm64-v8a, armeabi-v7a and x86_64 (NDK 27, CMake 3.22.1, libc++ linked statically); the example ran on an API 33 x86_64 emulator (Impeller / OpenGLES) with the smoke flag and printed its status line to logcat. Not run on a physical device or an ARM device. - The Swift Package Manager package (
darwin/openbone) builds with SwiftPM on Linux, with and without QuickJS, and exports the whole C API; iOS and macOS builds run in CI (macOS runner). - Not run here: iOS, macOS, Windows (no Xcode / MSVC here; CI builds them).
Libraries
- openbone
- OpenBone for Flutter: OpenBoneWidget plays .obone / .obonb skeletal animations and state machines through the
native runtime (libopenbone, dart:ffi), drawn with Canvas.drawVertices. Re-exports the pure-Dart core
(
package:openbone/openbone_core.dart). - openbone_core
- OpenBone runtime core for Dart: documents, instances, inputs, events, audio commands and the draw list, over the
native C++ runtime (libopenbone) through dart:ffi. No Flutter dependency;
package:openbone/openbone.dartadds the widget.