Sprite Baking Pipeline

At level boot, atlas-builder.ts pre-bakes enemy, boss, and debris sprites into a single runtime texture atlas. The atlas canvas is 4096×4096 (bumped from 2048 in v5.147.1 to fit 17 boss sprites at 256×256), built once via Canvas 2D and uploaded to the WebGL sprite batch as a GPU texture. Every quad rendered afterward samples from that one atlas — no per-frame Canvas 2D paths, no per-frame strokes, no per-frame gradients.

Regions are packed by a simple cursor allocator: each _allocRegion(w, h) call walks left-to-right with 2px bleed padding, wrapping to a new row when the cursor overflows. Each named region stores its UV coords (u0, v0, u1, v1) in the _regions map and is looked up by sprite identifier via getAtlasRegion(name). Aspect ratios are tracked separately because PNG patches can override the default 1.0 square.

The atlas contains utility stamps (white circle, soft glow, particle square, particle star, ring, missile triangle, bullet glow), one neon bullet sprite per weapon color, six pre-baked death-shard polygons drawn with a deterministic LCG so each variant is reproducible across reloads, the layered XP orb (outer green glow → black outline → green disk → white-hot core), the crate sticker (📦 emoji with thick black + green rim), the ship sprite placeholder (512×512 blue triangle, later patched with the real PNG), and reserved 128×128 placeholder regions for every enemy archetype × rarity (9 archetypes × 5 rarities = 45 enemy slots). The enemy placeholders are left empty at build time and patched at runtime by patchAtlasRegion(name, img) once the red-tinted outlined PNG sprite loads. After patching, the caller re-uploads the atlas to the GPU.

bridge-sprite-baking.ts exports the two helpers that produce the patched images.

bakeOutlinedSprite(img, sz, strokeColor, glowColor, strokePx = 2) returns a freshly allocated canvas with the source sprite wrapped in a crisp stroke and an optional soft glow halo. Internally it uses an angular dilation trick: 32 copies of the source image are drawn on a circle of radius strokePx, then re-tinted to the stroke color via source-in composition. Sampling at 32 points eliminates the octagonal artifact you get from cardinal-only stamping. The composite order back-to-front is: two glow layers at strokePx+5 (alpha 0.22) and strokePx+3 (alpha 0.38), one dense stroke layer at strokePx (alpha 1.0), then the original sprite on top. A 14px padding is reserved around the sprite so the stroke and glow have room to spread without clipping. Passing glowColor = null skips the halo (common enemies), and using the same color for stroke and glow gives a single unified rarity tint.

getFogStamp() returns a 256×256 cached canvas of soft white-noise cloud blobs on a black background. It bakes 12 overlapping radial gradients with Math.random()-jittered positions and radii (40–100px) on the first call, then memoizes the canvas in _fogStampCache so every later call returns the same texture without re-baking. The fog stamp is drawn as a parallax overlay at 1–3 drawImage calls per frame — cheap atmospheric fog without per-frame noise generation.

A third helper, bakeStickerEmoji(emoji, sz, fontPx, outerRimPx, innerRimPx, outerRimColor, innerRimColor), lives in atlas-builder.ts and powers the crate sprite. ctx.strokeText() does not outline COLR/SBIX color-emoji glyphs in Chromium — lineWidth is silently ignored on color glyphs. Instead, the helper bakes the emoji once, derives a binary silhouette mask from its alpha channel, then stamps the mask at every integer offset inside a disk of outerRimPx (filled black) and innerRimPx (filled white or accent color) before drawing the colored emoji on top. Composite order back-to-front: black rim, white/accent rim, emoji fill — the chunky sticker look used for the loot crate.

The net effect is that all expensive Canvas 2D operations (strokes, gradients, emoji silhouettes, noise generation) happen exactly once at level boot. Everything else is a quad draw against the atlas UVs through the WebGL sprite batch.

Files

  • src/starship-survivors/engine/rendering/atlas-builder.tsbuildAtlas(), packing helpers, all bake-at-boot sprites, reserveAtlasRegion() / patchAtlasRegion(), bakeStickerEmoji().
  • src/starship-survivors/engine/bridge-sprite-baking.tsbakeOutlinedSprite() (32-sample angular dilation), getFogStamp() (cached 256×256 noise).