← Examples · Ship Pack · How this page works

<yodacon-ship>

Attributes

Every ship, spinning

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

Ships as HTML: How the <yodacon-ship> Example Uses the Pack

Abstract—The web-component example builds a playground and a full fleet from catalog.json using the library's <yodacon-ship> element. This note traces the page's source, then the element's own loading, rendering and spin logic in the library.

Index Terms—Web components, custom elements, shadow DOM, CSS sprites.

I. Introduction

Importing the pack's library registers a custom element, <yodacon-ship>, that shows any ship at any heading from HTML alone. This page demonstrates it twice: a live element driven by a form, and every ship in the catalogue spinning. This note covers how the page uses the element, then how the element uses the assets inside the library [1].

II. One Script Tag

The page loads the library as a module (index.html:35). From then on, markup such as index.html:44 is a ship. The element shows its light-DOM children until the ship loads, so a plain <img> of frame 00 inside it is a good fallback.

III. Assets Used

Table I. What the page and the element take from the pack
AssetRead atUsed for
catalog.jsonindex.html:66Every edition and ship, for the grid and the select
ship.jsonlib/yodacon-ship-pack.js:266Frame grid and name, per element
sheet-alpha.pnglib/yodacon-ship-pack.js:270CSS background in the shadow DOM

IV. Building From the Catalogue

The page never lists ships itself. It walks catalog.json and creates one spinning element per ship (Listing 1), so ships added to the pack appear here with no edit. Scale is chosen per ship to keep whole numbers: 1 for frames wider than 64 pixels, else 2.

Listing 1. The grid, index.html:71–84
71for (const ed of catalog.editions) {72  const h3 = Object.assign(document.createElement("h3"), { textContent: `${ed.title} · ${ed.year}` });73  const grid = Object.assign(document.createElement("div"), { className: "grid" });74  for (const s of ed.ships) {75    const fig = document.createElement("figure");76    const el = document.createElement("yodacon-ship");77    el.setAttribute("ship", s.slug);78    el.setAttribute("edition", ed.key);79    el.setAttribute("scale", s.frame_width > 64 ? "1" : "2");80    el.toggleAttribute("spin", true);81    fig.append(el, Object.assign(document.createElement("figcaption"), { textContent: s.name }));82    grid.append(fig);83  }84  document.getElementById("all").append(h3, grid);

V. Attributes Are the Interface

The form writes attributes; the element observes them and re-renders (Listing 2). Changing ship or edition reloads, while heading, scale and spin only redraw (lib/yodacon-ship-pack.js:240).

Listing 2. Form to attributes, index.html:93–99
93function apply() {94  const [edition, ship] = form.ship.value.split("/");95  live.setAttribute("edition", edition);96  live.setAttribute("ship", ship);97  live.setAttribute("heading", form.heading.value);98  live.setAttribute("scale", form.scale.value);99  live.toggleAttribute("spin", form.spin.checked);

VI. Inside the Element

The element's shadow root holds one <div> painted with the sheet, the DOM technique of Tutorial Note 2 [2] packaged as a component. Loading fetches only ship.json in script; the sheet is left to the browser as a CSS background (Listing 3). A load counter discards answers to superseded requests when attributes change quickly.

Listing 3. Loading, lib/yodacon-ship-pack.js:262–274
262async #load() {263  const id = ++this.#loadId;264  const base = this.getAttribute("base") ?? PACK_BASE;265  try {266    const ship = await loadShipMeta(this.edition, this.ship, base);267    if (id !== this.#loadId) return; // a newer attribute change won268    this.#ship = ship;269    this.#frame = frameIndex(this.heading);270    this.#sprite.style.backgroundImage = `url("${ship.urls.sheet}")`;271    this.setAttribute("aria-label", ship.name);272    this.#sprite.hidden = false;273    this.shadowRoot.querySelector("slot").hidden = true;274    this.#render();

Rendering sizes the box to one frame times the scale and positions the scaled sheet (Listing 4), with the rectangle from frameRect():

(x, y) = ((f mod 6) · w, ⌊f / 6⌋ · h)(1)

Listing 4. Rendering, lib/yodacon-ship-pack.js:283–293
283#render() {284  if (!this.#ship) return;285  const { w, h, columns, frames } = spriteOf(this.#ship);286  const s = this.scale;287  if (!this.#spinning()) this.#frame = frameIndex(this.heading);288  const r = frameRect(this.#ship, this.#frame);289  const st = this.#sprite.style;290  st.width = `${w * s}px`;291  st.height = `${h * s}px`;292  st.backgroundSize = `${columns * w * s}px ${Math.ceil(frames / columns) * h * s}px`;293  st.backgroundPosition = `${-r.x * s}px ${-r.y * s}px`;

VII. Spinning Responsibly

A spin element advances one frame at about 12 fps, a full turn in three seconds. Two checks keep a page of spinning ships cheap and polite: an IntersectionObserver pauses elements that are off screen (lib/yodacon-ship-pack.js:227), and prefers-reduced-motion stops spinning altogether (lib/yodacon-ship-pack.js:298).

VIII. Using It in Your Project

  1. Add one module script tag pointing at the library.
  2. Write <yodacon-ship ship="dart" edition="conex" heading="90" scale="2">.
  3. Set heading (attribute or property) from your code; listen for load and error events if you need them.

The pack page itself uses the element for every ship card, which is the best demonstration of it at scale.

IX. Conclusion

The element turns the pack into HTML vocabulary: a catalogue read once, one JSON file per ship and one CSS background, with the frame arithmetic hidden behind a heading attribute.

References

  1. Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.
  2. Yodacon Ship Pack, “A ship in plain HTML and CSS,” Tutorial Note 2, examples/dom/.
  3. Yodacon Ship Pack, ship.json and catalog.json, v2.0.0, 2026.