Appendix C: Extending the renderer
The batch library you built covers exactly what Destroids needs. The full Batchiness library it distils goes further. Every addition follows the same three-step recipe, the whole reason the design is worth learning.
The recipe: add an op in three edits
- An opcode constant in
batch/batch.odin(pick an unused byte). - A public encoder in
batch/batch.odinthat writes the opcode + its args viaput_*. - A
caseinweb/batch.js'sbatch_cmd_flushswitch that reads the same args and calls the canvas method.
Miss any one and the two ends desync. Always edit the Odin constant and the JS case together.
Worked example: stroke_rect
// 1. constant
OP_STROKE_RECT :: 0x11 // f32 x4
// 2. encoder
stroke_rect :: proc "contextless" (b: ^Buffer, x, y, w, h: f32) {
put_u8(b, OP_STROKE_RECT); put_f32(b, x); put_f32(b, y); put_f32(b, w); put_f32(b, h)
}
// 3. case
case 0x11: c.strokeRect(f(), f(), f(), f()); break;
Bigger additions in the full library
- Gradients: create a
CanvasGradientkeyed by a small integer id, add colour stops, then use it as the fill/stroke. Gradients bake in absolute coordinates, so they're recreated per frame. Ops0x50โ0x54in Batchiness (linear_gradient,radial_gradient,add_color_stop,use_gradient_fill,use_gradient_stroke). The JS side keeps aMap<id, CanvasGradient>. - Sprites / baking: draw detailed art once into an offscreen canvas, then blit it cheaply every frame with
drawImage.bake_begin(id, w, h)redirects subsequent ops to the sprite's context;bake_end()switches back;draw_sprite(id, x, y)blits. The JS side keeps aMap<id, {canvas, ctx}>and swaps which context ops target. - Filters: set
ctx.filterto a CSS filter string ("blur(8px)") for real, cheap Gaussian blur, scoped withsave/restore. One op each for set and clear. measure_text: a synchronous foreign call (not buffered): JS measures the string on a dedicated offscreen context and returns the width, so Odin can lay out text it hasn't drawn yet.
Background music (Chapter 11's aside)
To add the ambient pad the production game uses, put a synth-worklet.js next to batch.js defining an AudioWorkletProcessor, load it lazily on first toggle (await ctx.audioWorklet.addModule("synth-worklet.js")), wrap it in a gain node, and post it play/stop messages. It runs on the audio thread, independent of the frame loop and of the sound-effects mute. Keep it optional and off by default, no autoplay.
The guiding principle for all of it: Odin encodes, JavaScript replays. Keep the wire format the single source of truth and the library scales as far as the Canvas and Web Audio APIs do.