A thin wrapper over izumi that adds the one thing a compiled binary normally cannot have: knobs someone else can turn.
An animation is a program, which is exactly why it is hard to hand to the person writing the lesson. tonari closes that gap. An animation implements a second trait alongside izumi’s, declaring the variables it exposes — floats, ints, booleans, colours and strings, each with a label, a default, and a range and step where they apply.
A macro then generates the WebAssembly exports that let a host enumerate those variables, allocate memory to pass values across, and apply them. The editor renders a form from the declaration, the saved values ride along with the lesson, and the player applies them at load. The Rust is never touched, and the module is never rebuilt.
Modules are versioned and reported alongside the izumi and tonari versions they were built against, so a host can tell exactly what it is running. Beyond that, tonari carries the shared visual layer the lesson modules draw from — a common palette, UI helpers, and baked figure geometry for biology, with each anatomy atlas behind its own cargo feature so a module links only the geometry it actually renders.
Implement TonariApp alongside izumi’s App, then swap the macro. Everything else is generated.
use tonari::prelude::*;
use tonari::variables::{TonariApp, VariableDef, VariableType, VariableValue};
impl TonariApp for MyScene {
fn module_version() -> &'static str { "1.2.0" }
fn list_variables() -> Vec<VariableDef> {
vec![
VariableDef {
name: "speed",
label: "Animation speed",
var_type: VariableType::Float,
default: VariableValue::Float(1.0),
min: Some(0.1),
max: Some(4.0),
step: Some(0.1),
},
VariableDef {
name: "accent",
label: "Accent colour",
var_type: VariableType::Color,
default: VariableValue::Color(0x50B4FF),
min: None, max: None, step: None,
},
]
}
// Called by the host whenever a value changes; marks the scene dirty.
fn set_variable(&mut self, name: &str, value: VariableValue) {
match (name, value) {
("speed", VariableValue::Float(v)) => self.speed = v,
("accent", VariableValue::Color(v)) => self.accent = unpack(v),
_ => {}
}
}
}
tonari_main!(MyScene);[
{ "name": "speed", "label": "Animation speed", "type": "float",
"default": 1.0, "min": 0.1, "max": 4.0, "step": 0.1 },
{ "name": "accent", "label": "Accent colour", "type": "color",
"default": "#50B4FF" }
]Five types, each with a label for the UI and bounds where they mean something. Colours cross the boundary as a packed integer; strings and everything else as UTF-8 through an allocated buffer the host frees when it is done.
One macro expands to every izumi export plus allocation and free, a versions blob, the variable declaration list, and the setter — which applies the value and marks the scene dirty so the next frame actually redraws.
The module reports its own version alongside the izumi and tonari versions it was compiled against, so a mismatch between an editor’s expectations and a deployed module is visible rather than mysterious.
The common palette and UI helpers every lesson module draws from, plus baked anatomy geometry for biology. Each figure sits behind its own cargo feature, all off by default, so a module’s binary carries only the geometry it renders.