← Examples · Ship Pack · How this page works

Plain DOM + CSS sprite

No canvas: a <div> whose background is sheet-alpha.png, moved with background-position. Rotate with the buttons or the ← → keys.

Try ?edition=override&ship=callisto or ?edition=conex&ship=dart.

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 2 · October 2026

A Ship in Plain HTML and CSS: How the DOM Example Uses the Pack

Abstract—The DOM example renders a pre-rotated pack ship with one <div> and CSS custom properties: ship.json supplies the frame grid, sheet-alpha.png becomes a background, and the heading moves background-position. This note walks through the page's own source to show each step and how to reuse it.

Index Terms—CSS sprites, background-position, custom properties, pixel art.

I. Introduction

This page shows a ship with no canvas, no library and no framework: a single <div> whose CSS background is the ship's sprite sheet. It is the smallest complete use of the pack, and every rendering technique in the other examples is this one with a different drawing call [1]. The page is one file, examples/dom/index.html; this note cites its lines.

II. Assets Used

Table I. What the page takes from the pack
AssetRead atUsed for
ship.jsonindex.html:75Frame size, columns, name
sheet-alpha.pngindex.html:79The pixels, as a CSS background

The pack's location is one URL, PACK (index.html:67); point it at your own copy or at a pinned jsDelivr release and nothing else changes. The ship's folder follows from the edition and slug (index.html:72).

III. Reading the Frame Grid

Frame sizes vary from ship to ship, so the page reads them from ship.json rather than assuming one (Listing 1). They become CSS custom properties on the element, together with a whole-number scale: 1997 pixel art only stays square at integer scales.

Listing 1. Metadata to CSS properties, index.html:75–84
75const meta = await (await fetch(new URL("ship.json", dir))).json();76const { frame_width: w, frame_height: h, columns: cols } = meta.sprite;7778const el = document.getElementById("ship");79el.style.setProperty("--sheet", `url("${new URL("sheet-alpha.png", dir)}")`);80el.style.setProperty("--w", w);81el.style.setProperty("--h", h);82el.style.setProperty("--cols", cols);83el.style.setProperty("--scale", w > 64 ? 2 : 3);   // whole numbers only84el.setAttribute("aria-label", meta.name);

IV. The Sprite in CSS

The element is exactly one frame in size. The background is the whole sheet, scaled by the same factor, and shifted so that the wanted cell sits in the box (Listing 2). With column c and row r of frame f, scale s and frame width w and height h:

background-position = (−c · w · s, −r · h · s)(1)

The size of the background must be scaled too, or the offsets in (1) would land in the wrong place; auto height keeps the sheet's proportions. image-rendering: pixelated (index.html:33) keeps the enlarged pixels sharp.

Listing 2. The sprite rule, index.html:24–33
24.ship {25  width: calc(var(--w) * var(--scale) * 1px);26  height: calc(var(--h) * var(--scale) * 1px);27  background-image: var(--sheet);28  background-repeat: no-repeat;29  background-size: calc(var(--w) * var(--cols) * var(--scale) * 1px) auto;30  background-position:31    calc(var(--col) * var(--w) * var(--scale) * -1px)32    calc(var(--row) * var(--h) * var(--scale) * -1px);33  image-rendering: pixelated;      /* keep the 1997 pixels square */

V. Heading to Frame

The frame for a heading θ (degrees clockwise from up) is

f = ⌊θ / 10⌋ mod 36,   c = f mod 6,   r = ⌊f / 6⌋(2)

and show() writes c and r to the element (Listing 3). Turning never rotates the element; it only moves the background, so the 1997 pre-rendered lighting stays correct at every angle.

Listing 3. Turning, index.html:87–95
87let heading = 0;88function show() {89  const frame = Math.floor(heading / 10) % 36;90  el.style.setProperty("--col", frame % cols);91  el.style.setProperty("--row", Math.floor(frame / cols));92  document.getElementById("readout").textContent =93    `${meta.name} · heading ${heading}° · frame ${String(frame).padStart(2, "0")}`;94}95const turn = (deg) => { heading = (heading + deg + 360) % 360; show(); };

VI. Accessibility

The sprite is a picture, so it carries role="img" and the ship's name as its label (index.html:50, index.html:84); the readout is a polite live region (index.html:55) so screen readers hear each new heading.

VII. Using It in Your Project

  1. Copy the pack (or use a pinned CDN URL) and set PACK.
  2. Copy the .ship rule and the script.
  3. Set the heading from your own game state and call show().

Many ships are many such elements. For dozens of moving ships prefer Canvas [2]; for a drop-in element that does all of this for you, see <yodacon-ship> [3].

VIII. Conclusion

One fetched JSON file, one background image and Equations (1) and (2) are a complete sprite renderer. Every other example replaces only the last step.

References

  1. Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
  2. Yodacon Ship Pack, “Canvas 2D example,” Tutorial Note 3, examples/canvas/.
  3. Yodacon Ship Pack, “The yodacon-ship element,” Tutorial Note 4, examples/web-component/.
  4. Yodacon Ship Pack, ship.json and catalog.json, v2.0.0, 2026.