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
| Asset | Read at | Used for |
|---|---|---|
ship.json ×4 | index.html:92 | Frame grid, stats, names |
sheet-alpha.png | index.html:78 | The pixels, as an SVG <image> |
planets/gas/large/planet.png | index.html:42 | The planet, a plain <image> |
frameRect(), frameIndex() | index.html:62 | Which 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.
index.html:75–8775function 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.
index.html:132–136132// 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
- Copy
makeShip()and theimage-renderingrule. - Call
place(x, y, heading)from your own update. - 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
- Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
- Yodacon Ship Pack, “A game loop on Canvas 2D,” Tutorial Note 3, examples/canvas/.
- Yodacon Ship Pack, “A ship in plain HTML and CSS,” Tutorial Note 2, examples/dom/.
- Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.
- W3C, Scalable Vector Graphics (SVG) 2, §8.2 “The viewBox attribute,” Candidate Recommendation, 2018.