The other end of the pipe: a TypeScript engine that loads the module, decodes the bytecode and replays it through Skia.
minato is what a host page or app embeds. Given a canvas and the URL of a compiled module, it instantiates the WebAssembly, drives a render loop, and replays each frame’s instruction stream onto CanvasKit — Google’s Skia compiled to WebAssembly — managing the paths, shaders, image filters, fonts, images and interned strings the stream refers to.
Interaction runs in both directions. DOM pointer events feed the module’s input state, and separately the engine hit-tests them against the hit regions the scene registered, tracking hover enter and exit and calling the module’s handlers by node id. A Rust animation can therefore respond to a tap without the host knowing anything about what it drew.
It also honours the dirty-skip contract: a frame whose buffer reports zero commands is never decoded or drawn. And because not every module has a scene, minato doubles as a host for headless ones — which is how the C++ interpreter runs inside the same pipeline with no canvas at all.
Point it at a canvas and a module URL. Everything else is on the player.
import { createPlayer } from '@tomomi/minato';
const player = await createPlayer({
canvas: document.getElementById('stage'),
wasmUrl: '/wasm-modules/pendulum/1.2.0.wasm',
});
// What the module was built against: { izumi, tonari, module }
console.log(player.getVersions());
// Apply whatever the lesson author saved.
for (const def of player.listVariables()) {
player.setVariable(def.name, saved[def.name] ?? def.default);
}
player.play();// Modules with no scene need no play() — just call them.
const result = player.runCode(source, { lang: 'pseudocod', stdin });
// { status: 'ok' | 'compile_error' | 'runtime_error' | 'timeout',
// stdout, error, errorRo }
// Trace mode adds a per-statement snapshot of variable state.
const trace = player.traceCode(source, { lang: 'cpp' });
for (const step of trace.steps) {
console.log(step.line, step.vars, step.stack);
}Surface and DPI management, the render loop with dirty-skip, and the interpreter that reads the buffer and dispatches each opcode to a CanvasKit call or a hit-region registration.
Pointer events are translated into the module’s input state and, in parallel, tested against registered regions using Skia path containment — tracking hover enter and exit and invoking the module’s callbacks by node id.
Paths, shaders, image filters, images, fonts and interned strings live host-side and are referenced by id from the stream, so the bytecode stays compact and a resource is uploaded once rather than per frame.
A calling convention for modules that return data rather than draw: the module writes a length-prefixed UTF-8 JSON body into WebAssembly memory, the engine decodes it and frees the buffer. This is what carries interpreter results and traces across.