The bottom of the animation pipeline: a retained-mode scene graph in Rust that emits drawing as bytecode instead of performing it.
izumi is the layer an animation is actually written against. It gives you a tree of nodes — rectangles, rounded rectangles, circles, lines, paths, images, text and groups — each with a transform, a paint, an opacity and an optional interactive flag. On top of that sit tweens with a full easing set, vector maths, and an input model.
What it does not do is draw. Rendering a scene walks the tree and emits a compact instruction stream: one opcode range for canvas state, one for paint, one for draw calls, one for resources and strings, one for hit regions. Something else replays it. That separation is what lets the same compiled animation run identically in a browser, on an iPhone and on an Android tablet.
It also means an idle scene is genuinely free. If nothing was mutated since the last render, izumi emits a header with a zero command count and the host skips decode and draw entirely.
Implement the App trait — construct in new, mutate in update, hand back the scene — then let the macro generate the WebAssembly exports.
use izumi::prelude::*;
struct Bouncer {
scene: Scene,
dot: NodeId,
t: f32,
}
impl App for Bouncer {
fn new(display: DisplayInfo) -> Self {
let mut scene = Scene::new();
let dot = scene.add(Shape::Circle {
cx: display.width / 2.0,
cy: 0.0,
radius: 24.0,
});
scene
.node_mut(dot)
.set_paint(Paint::new().set_color(Color::rgb(0x50, 0xB4, 0xFF)).clone())
.set_interactive(true);
Self { scene, dot, t: 0.0 }
}
fn update(&mut self, ctx: &FrameContext, _input: &InputState) {
self.t += ctx.dt;
let y = 120.0 + (self.t * 3.0).sin().abs() * 160.0;
self.scene.node_mut(self.dot).set_position(0.0, y);
}
// Nodes marked interactive emit hit regions keyed by their NodeId,
// so the host can route a tap straight back into Rust.
fn on_click(&mut self, _id: NodeId, _x: f32, _y: f32) {
self.t = 0.0;
}
fn scene(&mut self) -> &mut Scene {
&mut self.scene
}
}
izumi_main!(Bouncer);cargo build --target wasm32-unknown-unknown --releaseNodes carry a shape, transform, paint, opacity, visibility and an interactive flag, and nest arbitrarily through groups. Paint covers fill and stroke style, caps and joins, shaders, image filters and animated dash effects.
A growable buffer with a header of command count and byte length, written by a low-level emitter. Opcode ranges are partitioned by concern — canvas state, paint, draw, resources, strings, hit regions — so a decoder can dispatch on a single byte.
Tweens with from/to, duration, ping-pong and loop, over the standard easing family — linear through quad, cubic, sine, expo, bounce, elastic and back — plus lerp, clamp, remap and a small vector type.
The narrow surface an animation can call out through: logging, image and font requests, text measurement, the clock, sound playback, and a network hook used by multiplayer lesson games. Pointer state comes back the other way.