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
| Asset | Read at | Used for |
|---|---|---|
ship.json | index.html:75 | Frame size, columns, name |
sheet-alpha.png | index.html:79 | The 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.
index.html:75–8475const 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.
index.html:24–3324.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.
index.html:87–9587let 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
- Copy the pack (or use a pinned CDN URL) and set
PACK. - Copy the
.shiprule and the script. - 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
- Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
- Yodacon Ship Pack, “Canvas 2D example,” Tutorial Note 3, examples/canvas/.
- Yodacon Ship Pack, “The yodacon-ship element,” Tutorial Note 4, examples/web-component/.
- Yodacon Ship Pack, ship.json and catalog.json, v2.0.0, 2026.