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-levelMap<string, HTMLImageElement>keyed by sprite URL; stores fully-loadedImageinstances so re-mounts paint synchronously._pending— module-levelMap<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-decodedHTMLImageElement. SetscrossOrigin = 'anonymous'.TrimmedShipImage(props)— exported React component. Holds acanvasRefand auseEffectthat loads the sprite, paints the cropped sub-rect, and re-paints on parent resize.
READS FROM
- The
srcprop (sprite URL produced bygetShipV4SpritePath). - The
hullprop (kept in deps for future per-hull overrides / debugging, but not read in the effect body). - The
zoomprop (default1) — multiplier applied after the contain-fit scale. styleandclassNameprops, passed through to the<canvas>element so CSS filters (locked grayscale, rarity glow) and sizing still apply.window.devicePixelRatiofor DPR-correct canvas backing-store sizing.- The parent element’s
getBoundingClientRect()for layout dimensions, observed viaResizeObserver.
PUSHES TO
- The internal
<canvas>element: backing-storewidth/heightset toboxW * dpr/boxH * dpr; CSSstyle.width/style.heightset toboxW/boxHpixels; 2D context used to clear and draw a single sub-rect viactx.drawImage(img, UNION_BBOX.x, UNION_BBOX.y, UNION_BBOX.w, UNION_BBOX.h, dx, dy, dw, dh)withimageSmoothingQuality = 'high'. - Module-level
_imgCache/_pendingmaps (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
.catchis empty). - Does not re-measure or re-emit the
UNION_BBOXwhen 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
srcalready resolved bygetShipV4SpritePath. - Does not cancel or evict the image cache; cached
HTMLImageElements live for the lifetime of the page.
Signals
cancelledflag in the effect closure — set on cleanup so a late-resolvingloadSpritepromise won’t paint after unmount.ResizeObserveron the parent element triggers a re-paint with the last-loaded image whenever layout changes.useEffectdep 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: containmath 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 > 1over-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. hullis included inuseEffectdeps even though it’s unused in the body, so a consumer that swaps hulls without changingsrcstill retriggers paint.