← Examples · Ship Pack · How this page works

Canvas 2D game loop

Arrow keys: turn and thrust. The library's drawShip() picks the frame and draws it pixelated.

Ships from the Yodacon Ship Pack by Paul Richeson (ConEx, 1997), GPL-2.0-or-later — github.com/yodacon/yodacon-ship-pack

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

Table I. What the page takes from the pack
AssetRead atUsed for
ship.jsonindex.html:34Frame size, stats, name
sheet-alpha.pngindex.html:34Decoded once; drawn every frame
lib/yodacon-ship-pack.jsindex.html:31Loading 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.

Listing 1. The library's loader, lib/yodacon-ship-pack.js:156–160
156export 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.

Listing 2. Scale and stats, index.html:39–43
39const 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)

Listing 3. Update, index.html:52–65
52function 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).

Listing 4. The library's draw, lib/yodacon-ship-pack.js:182–187
182export 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.

Listing 5. Fixed-timestep loop, index.html:77–84
77const 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

  1. Import loadShip and drawShip from your copy of the library or a pinned CDN URL (index.html:30).
  2. Await one loadShip() per ship type; reuse the result for every ship of that type.
  3. Keep each ship's x, y and heading; call drawShip() 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

  1. Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
  2. Yodacon Ship Pack, “Sample projects,” projects/.
  3. Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.