← Examples · Ship Pack · How this page works

SVG sprite

Every ship is a nested <svg> holding the whole sheet-alpha.png; its viewBox looks at one frame, and the nested viewport clips the rest. Fly with ← → ↑, or the buttons; three escorts orbit the planet.

Loading ships

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. The planet is the pack's generated art.

Yodacon Ship Pack · Tutorial Note 8 · October 2026

A Viewport per Ship: How the SVG Example Uses the Pack

Abstract—The SVG example shows each pack ship as a nested <svg> that holds the whole sheet-alpha.png and selects one frame with its viewBox, clipping the rest. This note follows the page's source: the scene in user units, the ship as a viewport, orbiting escorts that face their direction of travel, and a loop with no drawing step.

Index Terms—SVG, viewBox, sprite sheets, pixel art.

I. Introduction

SVG can show part of an image without any clipping code: a nested <svg> element establishes its own viewport, and its viewBox chooses which region of its content fills that viewport. The SVG example uses this to make each pack ship a small window onto its sprite sheet, then flies one ship and orbits three more around a planet. The page is examples/svg/index.html.

II. Assets Used

Table I. What the page takes from the pack
AssetRead atUsed for
ship.json ×4index.html:92Frame grid, stats, names
sheet-alpha.pngindex.html:78The pixels, as an SVG <image>
planets/gas/large/planet.pngindex.html:42The planet, a plain <image>
frameRect(), frameIndex()index.html:62Which cell to show

Only metadata is fetched by script; the browser loads the sheets itself when the <image> elements reference them, so no image is decoded in JavaScript.

III. The Scene

The scene is one <svg viewBox="0 0 640 400"> (index.html:34). All positions in the script are in those 640 × 400 user units, and CSS stretches the whole drawing to the page width, so the game logic never deals with screen pixels. One rule, image-rendering: pixelated (index.html:18), keeps the 1997 pixels square at whatever size the scene ends up. Stars are 1 × 1 <rect>s and the ships share a drop-shadow <filter> (index.html:37–39), an effect that costs one attribute here and a second drawing pass on a canvas.

IV. One Ship, One Viewport

makeShip() (Listing 1) builds a nested <svg> one frame in size (times a whole-number scale) and puts the entire sheet inside it at its natural size, 6w × 6h. Nothing in the sheet moves afterwards. To show frame f, place() sets

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

which is exactly frameRect()'s rectangle, so the viewport shows that cell and clips the other 35; nested SVG viewports clip by default. Position is the element's x/y, offset by half its size so the ship is centred on the point.

Listing 1. A ship as a nested viewport, index.html:75–87
75function makeShip(ship, scale) {76  const { frame_width: w, frame_height: h, columns, rows } = ship.sprite;77  const view = svgEl("svg", { width: w * scale, height: h * scale });78  view.append(svgEl("image", { href: ship.urls.sheet, width: w * columns, height: h * rows }));79  document.getElementById("ships").append(view);80  return {81    ship,82    place(x, y, heading) {83      const r = frameRect(ship, frameIndex(heading));84      view.setAttribute("viewBox", `${r.x} ${r.y} ${r.w} ${r.h}`);85      view.setAttribute("x", x - (r.w * scale) / 2);86      view.setAttribute("y", y - (r.h * scale) / 2);87    },

V. Heading and Motion

The player's update is the Canvas example's [2]: turn, thrust along the frame shown, cap the speed, wrap at the edges. Only the last step differs, a call to place() instead of a draw. The escorts (Listing 2) orbit clockwise: at angle φ from up a ship is at (cx + R sin φ, cy − R cos φ), and its velocity points along

θ = φ + 90°(2)

so each is drawn with frame frameIndex(φ + 90) and always faces the way it is going.

Listing 2. The escorts, index.html:132–136
132// Escorts orbit clockwise: at angle φ from up, they move along φ + 90°.133escorts.forEach((e, i) => {134  const R = 90 + 38 * i, phi = (t * (40 - 8 * i) + 120 * i) % 360, rad = (phi * Math.PI) / 180;135  e.place(W / 2 + R * Math.sin(rad), H / 2 - R * Math.cos(rad), phi + 90);136});

VI. Drawing

There is no draw function. The loop (index.html:142) only updates attributes, and the browser repaints the changed parts of the SVG. That is the trade: SVG costs a DOM element per ship, which suits tens of ships, not thousands, but gives hit-testing, accessibility (the scene's <title>, index.html:35), CSS styling and filters for nothing.

VII. Using It in Your Project

  1. Copy makeShip() and the image-rendering rule.
  2. Call place(x, y, heading) from your own update.
  3. Put the ships inside any SVG: a map, a diagram, an editor canvas.

The same window-onto-a-sheet idea, done with a CSS background instead of a viewport, is the DOM example [3].

VIII. Conclusion

A nested <svg> whose viewBox is Equation (1) is a complete sprite: the sheet is loaded once by the browser and every frame change is one attribute.

References

  1. Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
  2. Yodacon Ship Pack, “A game loop on Canvas 2D,” Tutorial Note 3, examples/canvas/.
  3. Yodacon Ship Pack, “A ship in plain HTML and CSS,” Tutorial Note 2, examples/dom/.
  4. Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.
  5. W3C, Scalable Vector Graphics (SVG) 2, §8.2 “The viewBox attribute,” Candidate Recommendation, 2018.