Yo Odin ยท Appendix C

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

  1. An opcode constant in batch/batch.odin (pick an unused byte).
  2. A public encoder in batch/batch.odin that writes the opcode + its args via put_*.
  3. A case in web/batch.js's batch_cmd_flush switch 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 CanvasGradient keyed 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. Ops 0x50โ€“0x54 in Batchiness (linear_gradient, radial_gradient, add_color_stop, use_gradient_fill, use_gradient_stroke). The JS side keeps a Map<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 a Map<id, {canvas, ctx}> and swaps which context ops target.
  • Filters: set ctx.filter to a CSS filter string ("blur(8px)") for real, cheap Gaussian blur, scoped with save/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.