PURPOSE

Sprite utility module providing two pieces: a generic image downsampler reused by enemy-sprites, and a silhouette-based collision radius calculator that derives tight hitboxes from the alpha channel of v4 player ship diffuse textures. The legacy per-rarity player loader is gone; this file only computes geometry on top of v4 sprites now.

OWNS

  • downsampleSprite(src, targetSize) — canvas-based proportional resize of an HTMLImageElement to a target longest-axis dimension, returning a new image via dataURL round-trip.
  • getContentMaxFrac(hullClass) — returns max(contentW, contentH) / max(spriteW, spriteH) for a hull’s v4 diffuse, used to scale hull polygons to rendered content size.
  • getSilhouetteCollisionRadius(shipRadius, hullClass, rarity, shipScale) — returns world-unit collision radius derived from the tallest column of solid pixels in the v4 diffuse.
  • Per-hull caches: _silhouetteHeightFrac and _contentMaxFrac (module-local Record<string, number>).
  • SILHOUETTE_ALPHA_THRESHOLD = 155 constant defining what counts as a solid silhouette pixel.
  • Inline collision-math constants mirroring draw.ts: SHIP_DRAW_SCALE = 6.3, SHIP_SPRITE_PAD_MULT = 1.6, extra 1.2 WebGL pad factor, and the RARITY_SIZE_MULT table (common 0.92, uncommon 0.96, rare 1.00, epic 1.06, legendary 1.12).
  • Internal helper computeSilhouetteHeightFrac(img, key) that scans every column for top/bottom solid pixels and records the tallest run as a fraction of naturalHeight.

READS FROM

  • getShipV4(hullClass) from ships-v4-loader.ts — pulls the diffuse HTMLImageElement for the active hull.
  • ShipRarity type from ../../data/ships.
  • The DOM canvas 2D context (document.createElement('canvas'), getContext('2d'), getImageData) for pixel inspection and image scaling.

PUSHES TO

  • Module-local caches _silhouetteHeightFrac and _contentMaxFrac — populated lazily on first request per hull key.
  • Newly created HTMLImageElement instances returned from downsampleSprite (consumers own the result).
  • No external stores, no telemetry, no events.

DOES NOT

  • Does not load or fetch any image — relies on ships-v4-loader to have decoded the diffuse first.
  • Does not draw to the framebuffer or interact with WebGL; all canvases used here are throwaway 2D contexts for pixel reads.
  • Does not handle rarity-specific sprite variants — v4 ships only one diffuse per hull, so the cache keys are hull class alone.
  • Does not invalidate its caches; values persist for the session once computed.
  • Does not throw on unloaded sprites — getSilhouetteCollisionRadius and getContentMaxFrac return 0 so callers can fall back.
  • Does not import from draw.ts — collision constants are duplicated inline to avoid a circular dependency.

Signals

  • Return value 0 from getSilhouetteCollisionRadius or getContentMaxFrac signals “sprite not ready” — caller must fall back (collision uses the shield-based radius formula in that case).
  • downsampleSprite returning the original src signals either “already small enough” or “decode failure during dataURL round-trip” (the onerror handler resolves with the original image).
  • A cached 0.5 fraction in either cache signals a degenerate sprite (zero dimensions or no solid pixels found); subsequent calls reuse that value.

Entry points

  • downsampleSprite — called from enemy-sprites.ts during enemy sprite preparation.
  • getSilhouetteCollisionRadius — called from the player collision sizing path; consumes ship.radius, hull class, rarity, and optional shipScale.
  • getContentMaxFrac — called when scaling hull polygons to match rendered content within the padded sprite bounds.

Pattern notes

  • Lazy memoisation with a sentinel: caches check existing !== undefined so a legitimate 0 stays cached.
  • Pixel-perfect silhouette analysis runs at full natural resolution via getImageData on a one-shot 2D canvas; cost is paid once per hull then cached for the session.
  • Constants from draw.ts are intentionally copy-pasted with a comment marking them as a must-match-exactly mirror — preferred over a circular import.
  • Height-of-tallest-column metric (rather than width) is deliberate: when ships render sideways the height axis is the narrower silhouette dimension, producing a tighter collision radius that stays inside the visible hull.
  • shipSz formula shipRadius * SHIP_DRAW_SCALE * SHIP_SPRITE_PAD_MULT * 1.2 * rarityMult * shipScale and the aspect normaliser 1 / max(1, aspect) together reproduce the WebGL draw scale so the returned radius is in the same world units the renderer uses.
  • imageSmoothingQuality = 'high' is set explicitly on the downsample canvas; the dataURL round-trip is async because new Image() decode is async even from a data URL.