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
| Asset | Read at | Used for |
|---|---|---|
catalog.json | index.html:66 | Every edition and ship, for the grid and the select |
ship.json | lib/yodacon-ship-pack.js:266 | Frame grid and name, per element |
sheet-alpha.png | lib/yodacon-ship-pack.js:270 | CSS 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.
index.html:71–8471for (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).
index.html:93–9993function 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.
lib/yodacon-ship-pack.js:262–274262async #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)
lib/yodacon-ship-pack.js:283–293283#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
- Add one module script tag pointing at the library.
- Write
<yodacon-ship ship="dart" edition="conex" heading="90" scale="2">. - Set
heading(attribute or property) from your code; listen forloadanderrorevents 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
- Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.
- Yodacon Ship Pack, “A ship in plain HTML and CSS,” Tutorial Note 2, examples/dom/.
- Yodacon Ship Pack, ship.json and catalog.json, v2.0.0, 2026.