PURPOSE

Pre-renders all sprite assets onto a single 4096×4096 texture atlas at game startup. Programmatically bakes enemy shapes, bullet archetypes, particle stamps, the XP orb, crate emoji, ring/star/triangle utility sprites, glow gradients, death-shard polygons, and the ship-sprite placeholder onto one Canvas 2D surface, then hands the canvas to the WebGL sprite batch for GPU upload. No external image files — every sprite is drawn from code or baked from emoji glyphs.

OWNS

  • ATLAS_SIZE constant — 4096 px (bumped from 2048 in v5.147.1 for 256×256 boss sprites; 4096 is the WebGL2 / GLES2 guaranteed minimum).
  • Packing cursor state — _cursorX, _cursorY, _rowHeight (module-level shelf packer with 2 px padding).
  • _regions map — name → AtlasRegion UV coords, the canonical lookup table for every named sprite in the atlas.
  • _aspectRatios map — per-region width/height ratio, default 1.0 (square).
  • _atlasCanvas — module-level reference to the built canvas, kept for runtime patching after initial bake.
  • The BULLET_COLORS table (13 weapon colors: rifle, shotgun, missile, revolver, cannon, mortar, disc, flame, lightning, railgun, sweep, shield, line).
  • The ENEMY_ARCHETYPES × ENEMY_RARITIES reserved-region matrix (9 × 5 = 45 empty 128 px slots for runtime PNG patching).
  • Six baked death-shard polygon variants with deterministic per-variant LCG seeding (no Math.random() at module init).
  • Internal sprite renderers _drawCircleSprite and _drawNeonBullet, and the temp-canvas helper _makeTmpCanvas.

READS FROM

  • AtlasRegion type imported from ./sprite-batch.
  • DOM document.createElement('canvas') and CanvasRenderingContext2D (Canvas 2D + system emoji font via sans-serif).

PUSHES TO

  • Returns the built HTMLCanvasElement from buildAtlas() — the renderer hands this canvas to the WebGL sprite batch as a texture.
  • _regions map is read by every sprite-drawing call across the engine via getAtlasRegion / tryGetAtlasRegion.
  • _atlasCanvas is exposed via getAtlasCanvas() so the bridge can patch PNG sprites into reserved regions after first load.

DOES NOT

  • Does not upload the texture to the GPU — that’s the sprite batch’s job after buildAtlas() returns the canvas.
  • Does not load external image files or PNGs — every initial sprite is drawn programmatically. PNG patching happens later via patchAtlasRegion, driven by the bridge.
  • Does not draw polygon enemy fallbacks — enemy archetype regions are reserved empty and stay empty until the runtime patches them with red-tinted outlined PNGs on first encounter.
  • Does not handle dynamic atlas growth — overflow logs a console.warn and the offending region gets clipped. Sizing is fixed at 4096.
  • Does not animate anything — atlas is baked once and only mutated via explicit patchAtlasRegion calls.
  • Does not use Math.random() for death-shard baking — uses a deterministic per-variant LCG so the bake is reproducible across reloads.
  • Does not use ctx.strokeText to outline color-glyph emoji — Chromium silently ignores lineWidth on COLR/SBIX glyphs, so it builds a silhouette mask and stamps it at every offset inside a disc instead.

Signals

None. This module has no events, observers, or pub/sub. Coordination with the renderer and bridge is via direct function calls and the shared _regions / _atlasCanvas module state.

Entry points

  • buildAtlas(): HTMLCanvasElement — bakes the full atlas; called once by the renderer at startup.
  • getAtlasRegion(name): AtlasRegion — throws if missing.
  • tryGetAtlasRegion(name): AtlasRegion | null — returns null if missing.
  • getAtlasRegionNames(): string[] — debug-only listing of registered names.
  • getAtlasAspect(name): number / setAtlasAspect(name, ratio): void — per-region aspect ratio.
  • reserveAtlasRegion(name, w, h): { x, y, region } — allocates and registers an empty region for later patching.
  • patchAtlasRegion(name, img): void — draws a loaded image into a previously reserved region; caller is responsible for re-uploading the atlas to the GPU.
  • getAtlasCanvas(): HTMLCanvasElement | null — returns the live atlas canvas for patching.
  • bakeStickerEmoji(emoji, sz, fontPx, outerRimPx, innerRimPx, outerRimColor?, innerRimColor?): HTMLCanvasElement — standalone exported helper for chunky sticker-style emoji rendering; used by the crate region internally and by other modules (e.g. crates code) for ad-hoc emoji sprites.

Pattern notes

  • Shelf packing. Sprites are placed left-to-right into rows. When the cursor would overflow the right edge, _allocRegion advances to a new row sized by the tallest sprite in the previous row, plus 2 px padding to prevent texture bleeding under bilinear filtering.
  • UV coordinates only. _registerRegion stores only normalized u0/v0/u1/v1 floats in the range 0–1; pixel coordinates are not retained. Patching reconstructs pixel bounds by multiplying back by ATLAS_SIZE.
  • Bake order matches code order. Regions are allocated in declaration order: white circle, glow stamp, bullet archetypes, reserved enemy slots, particle square, death shards, star, ring, missile triangle, bullet glow, XP orb, crate, ship-sprite placeholder. Adding new sprites at the end is safe; reordering changes UV layout and forces a re-bake on next boot.
  • Empty placeholders for runtime patching. Enemy archetype × rarity regions are reserved but not drawn, then patched by the bridge once PNG sprites load. There is no polygon fallback — pre-patch enemies render as transparent quads.
  • Sticker emoji technique. bakeStickerEmoji derives a binary silhouette mask from the colored emoji’s alpha via source-in, stamps the mask at every offset within an outer-rim radius (filled black) and inner-rim radius (filled custom color, e.g. crate uses bright green #3ce06b), then composites the colored emoji on top — outer rim, inner rim, emoji fill. This is the only reliable way to outline color-glyph emoji in Chromium.
  • Death-shard determinism. Per-variant LCG seed (v + 1) * 9301 with the standard seed = (seed * 9301 + 49297) % 233280 recurrence; shards are baked white and tinted red at draw time via sprite-batch vertex color, so the bake doesn’t fork per color shade.
  • Crash-on-bad-data. getAtlasRegion throws on missing name; only tryGetAtlasRegion and patchAtlasRegion silently no-op for unknown names. Atlas overflow logs console.warn but does not crash.
  • Module-level mutable state. Both packer cursor and _atlasCanvas are file-scoped lets. buildAtlas resets the cursor on each call, but calling it twice would double-register every region; treat it as one-shot per session.