PURPOSE

Renders a ship sprite onto a <canvas> with a single uniform crop applied so the transparent padding baked into the 1024x1024 source PNGs is removed by the same amount on every ship. Uniform crop (rather than per-ship tight-bbox) is intentional: it preserves the relative size differences between hulls (small scouts stay small inside the card, capital hulls fill it) while still killing the dead margin around the silhouette.

OWNS

  • UNION_BBOX — the shared source-pixel sub-rect { x: 260, y: 175, w: 503, h: 701 } computed once across all 38 v4 sprites. It is the smallest rectangle that contains every ship’s tight alpha bounding box.
  • _imgCache — module-level Map<string, HTMLImageElement> keyed by sprite URL; stores fully-loaded Image instances so re-mounts paint synchronously.
  • _pending — module-level Map<string, Promise<HTMLImageElement>> for in-flight loads, so concurrent mounts of the same sprite share a single network request.
  • loadSprite(src) — internal loader returning a promise for a cached or freshly-decoded HTMLImageElement. Sets crossOrigin = 'anonymous'.
  • TrimmedShipImage(props) — exported React component. Holds a canvasRef and a useEffect that loads the sprite, paints the cropped sub-rect, and re-paints on parent resize.

READS FROM

  • The src prop (sprite URL produced by getShipV4SpritePath).
  • The hull prop (kept in deps for future per-hull overrides / debugging, but not read in the effect body).
  • The zoom prop (default 1) — multiplier applied after the contain-fit scale.
  • style and className props, passed through to the <canvas> element so CSS filters (locked grayscale, rarity glow) and sizing still apply.
  • window.devicePixelRatio for DPR-correct canvas backing-store sizing.
  • The parent element’s getBoundingClientRect() for layout dimensions, observed via ResizeObserver.

PUSHES TO

  • The internal <canvas> element: backing-store width / height set to boxW * dpr / boxH * dpr; CSS style.width / style.height set to boxW / boxH pixels; 2D context used to clear and draw a single sub-rect via ctx.drawImage(img, UNION_BBOX.x, UNION_BBOX.y, UNION_BBOX.w, UNION_BBOX.h, dx, dy, dw, dh) with imageSmoothingQuality = 'high'.
  • Module-level _imgCache / _pending maps (writes on load success / load completion).

DOES NOT

  • Does not measure per-ship alpha bounds at runtime — every ship uses the same UNION_BBOX.
  • Does not render via <img> — canvas is required for sub-rect drawing at DPR-correct resolution.
  • Does not strip or alter CSS filters; locked-grayscale and rarity-glow filters on the canvas element still apply.
  • Does not handle load errors visibly — a 404 leaves the canvas blank (the .catch is empty).
  • Does not re-measure or re-emit the UNION_BBOX when new ships are added. If a future hull’s silhouette pokes past these bounds it will clip; the union rect must be recomputed manually.
  • Does not own sprite-path resolution — callers pass src already resolved by getShipV4SpritePath.
  • Does not cancel or evict the image cache; cached HTMLImageElements live for the lifetime of the page.

Signals

  • cancelled flag in the effect closure — set on cleanup so a late-resolving loadSprite promise won’t paint after unmount.
  • ResizeObserver on the parent element triggers a re-paint with the last-loaded image whenever layout changes.
  • useEffect dep list [hull, src, zoom] — re-runs the load-and-paint flow whenever any of those change.

Entry points

  • import { TrimmedShipImage } from '...' — single named export consumed by metagame screens that display ship cards (collection, shop, etc.).
  • Props contract: { hull: string; src: string; style?: CSSProperties; className?: string; zoom?: number }.

Pattern notes

  • Module-level cache + in-flight promise map is the standard de-dup pattern for image loads shared across many simultaneous React mounts.
  • Backing-store size is DPR-multiplied; CSS size is in CSS pixels — keeps the canvas crisp on high-DPI phones without changing layout.
  • object-fit: contain math is reproduced manually: fitScale = min(boxW / UNION_BBOX.w, boxH / UNION_BBOX.h) * zoom * dpr, then center via (canvas.width - dw) / 2, (canvas.height - dh) / 2.
  • zoom > 1 over-zooms and intentionally lets ship edges bleed off-card.
  • Empty .catch(() => {}) on the load promise is deliberate — failed loads leave a blank canvas rather than throwing into React’s error boundary.
  • hull is included in useEffect deps even though it’s unused in the body, so a consumer that swaps hulls without changing src still retriggers paint.