Yodacon Ship Pack · Tutorial Note 3 · October 2026
A Game Loop on Canvas 2D: How the Canvas Example Uses the Pack
Abstract—The Canvas example flies a pack ship with the library's loadShip() and drawShip(), handling taken from the ship's 1997 statistics and a fixed-timestep loop. This note follows the page's source and the library code it calls.
Index Terms—Canvas 2D, sprite sheets, fixed timestep, pixel art.
I. Introduction
The Canvas example is a flyable ship in under 60 lines of script. It uses two functions from the pack's
library, loadShip() and drawShip() [1], and a fixed-timestep loop. This note
follows the page's source, examples/canvas/index.html, from loading to the drawn frame.
II. Assets Used
| Asset | Read at | Used for |
|---|---|---|
ship.json | index.html:34 | Frame size, stats, name |
sheet-alpha.png | index.html:34 | Decoded once; drawn every frame |
lib/yodacon-ship-pack.js | index.html:31 | Loading and drawing |
III. Loading
loadShip(edition, slug) fetches ship.json, then loads and decodes the sheet
(Listing 1). It resolves only when the image is ready, so the first frame never draws an empty sprite. The
query string picks the ship, so ?edition=override&ship=callisto needs no code change.
lib/yodacon-ship-pack.js:156–160156export async function loadShip(edition = "conex", slug = "yodacon", base = PACK_BASE) {157 const ship = await loadShipMeta(edition, slug, base, "ships");158 ship.sheet = await loadImage(ship.urls.sheet);159 return ship;160}
IV. Handling From the 1997 Record
The ship's original Escape Velocity numbers live in ship.meta.stats. The page maps
three of them to its own units (Listing 2): Speed caps velocity, Accel is thrust
and Maneuver is turn rate, so a Dart and a Large Pin fly differently with no per-ship code.
index.html:39–4339const SCALE = ship.sprite.frame_width > 64 ? 1 : 2; // whole numbers only4041// Handling from ship.json (see demo/demo.js for the reasoning).42const stats = ship.meta.stats ?? {};43const MAX = (stats.Speed || 300) * 0.6, ACCEL = (stats.Accel || 300) * 0.6, TURN = (stats.Maneuver || 3) * 40;
V. Update: Thrust Along the Frame Shown
Heading is continuous, but the picture changes only every 10°. The page thrusts along the heading of the frame on screen, a = ⌊θ/10⌋·10°, so the ship always moves the way it appears to point (Listing 3). With screen y pointing down, the unit vector is
d(a) = (sin a, −cos a)(1)
index.html:52–6552function update(dt) {53 if (keys.ArrowLeft) s.heading -= TURN * dt;54 if (keys.ArrowRight) s.heading += TURN * dt;55 s.heading = (s.heading + 360) % 360;56 if (keys.ArrowUp) {57 const a = (Math.floor(s.heading / 10) * 10 * Math.PI) / 180; // thrust along the frame shown58 s.vx += Math.sin(a) * ACCEL * dt;59 s.vy -= Math.cos(a) * ACCEL * dt; // screen y points down60 }61 if (keys.ArrowDown) { s.vx *= 0.96; s.vy *= 0.96; }62 const v = Math.hypot(s.vx, s.vy);63 if (v > MAX) { s.vx *= MAX / v; s.vy *= MAX / v; }64 s.x = (s.x + s.vx * dt + canvas.width) % canvas.width; // wrap around the edges65 s.y = (s.y + s.vy * dt + canvas.height) % canvas.height;
VI. Draw: One Library Call
drawShip(ctx, ship, heading, x, y, scale) (index.html:73) selects the frame
with f = ⌊θ/10⌋ mod 36, finds its cell, and copies it centred on (x, y) with
smoothing off and rounded coordinates (Listing 4). The canvas itself is scaled by CSS to fit the page, so
the page also sets image-rendering: pixelated on it (index.html:14).
lib/yodacon-ship-pack.js:182–187182export function drawShip(ctx, ship, headingDeg, x, y, scale = 1) {183 const r = frameRect(ship, frameIndex(headingDeg));184 const w = r.w * scale, h = r.h * scale;185 ctx.imageSmoothingEnabled = false;186 ctx.drawImage(ship.sheet, r.x, r.y, r.w, r.h, Math.round(x - w / 2), Math.round(y - h / 2), w, h);187}
VII. The Loop
Physics advances in fixed 1/60 s steps while drawing happens once per animation frame (Listing 5). The accumulator is capped at 0.25 s so a background tab does not return to a burst of catch-up steps.
index.html:77–8477const STEP = 1 / 60;78let last = performance.now(), acc = 0;79requestAnimationFrame(function frame(now) {80 acc += Math.min(0.25, (now - last) / 1000);81 last = now;82 while (acc >= STEP) { update(STEP); acc -= STEP; }83 draw();84 requestAnimationFrame(frame);
VIII. Using It in Your Project
- Import
loadShipanddrawShipfrom your copy of the library or a pinned CDN URL (index.html:30). - Await one
loadShip()per ship type; reuse the result for every ship of that type. - Keep each ship's
x,yandheading; calldrawShip()per ship per frame.
The flying games in projects/ (Orbital Defence, Slingshot, ConEx Courier) are this loop
grown into games [2].
IX. Conclusion
Two library calls carry the whole asset path from URL to pixels; everything else on the page is ordinary game code, which is the point: the pack stays out of your game's way.
References
- Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
- Yodacon Ship Pack, “Sample projects,” projects/.
- Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.