atlas-loader

PURPOSE

Fetches a per-category VFX atlas manifest + its per-component PNG layers from /src/starship-survivors/data/vfx/atlas/, then uploads the decoded pixel data into a GPU texture-array via a batch interface. Bridge between baked atlas assets on disk and the WebGL renderer.

OWNS

  • AtlasManifestFile interface — JSON shape: category, format, atlasDir, and a layers[] array. Each layer: componentId, layerIndex, frameCount, fps, tileSize, gridCols, gridRows, loopMode ('oneshot' | 'loop' | 'parameterized'), pivot {x, y}, bakedAt.
  • LoadedAtlas interface — { manifest, layerImages: Map<string, HTMLImageElement> } keyed by componentId.
  • loadAtlas(category) — async fetch + decode pipeline.
  • uploadAtlasToTexture(batch, atlas) — CPU-side decode-to-pixels + per-layer GPU upload.

READS FROM

  • HTTP fetch: /src/starship-survivors/data/vfx/atlas/${category}.manifest.json (manifest JSON).
  • HTTP image load: /src/starship-survivors/data/vfx/atlas/${category}/${componentId}.png (one PNG per layer, loaded in parallel via Promise.all).
  • AtlasCategory type from ./atlas-format.

PUSHES TO

  • batch.createEmptyAtlas(tileSize, gridCols, gridRows, layerCount) — allocates a 2D-array texture sized off layer 0.
  • batch.uploadLayer(layerIndex, tileSize, gridCols, gridRows, pixels: Uint8Array) — uploads one layer’s RGBA bytes per layer.
  • Returns LoadedAtlas | null to caller; null on fetch failure or non-OK response.

DOES NOT

  • Does NOT define AtlasCategory (imported from atlas-format).
  • Does NOT create the WebGL context, the renderer, or the batch object — caller supplies it.
  • Does NOT cache atlases across calls — each loadAtlas re-fetches.
  • Does NOT validate manifest shape at runtime — trusts the JSON.
  • Does NOT handle partial-failure of layer PNG loads — one rejected image rejects the entire Promise.all.
  • Does NOT crop, repack, or resize images — assumes the PNG already matches tileSize * gridCols x tileSize * gridRows.
  • Does NOT free the temporary <canvas> it allocates per layer (relies on GC).
  • Does NOT animate, sample, or interpret frameCount / fps / loopMode / pivot — those are passthrough metadata for downstream sampler code.

Signals

  • loadAtlas returns null on fetch error or HTTP non-OK; throws (via Promise.all rejection) if a layer PNG fails to decode.
  • uploadAtlasToTexture early-returns when manifest.layers.length === 0.
  • uploadAtlasToTexture skips any layer whose componentId is missing from layerImages (continue).

Entry points

  • loadAtlas(category: AtlasCategory): Promise<LoadedAtlas | null> — public, async, called at workbench / scene init.
  • uploadAtlasToTexture(batch, atlas): void — public, sync, called after loadAtlas resolves and a WebGL batch exists.

Pattern notes

  • Two-phase init: fetch JSON + decode images (loadAtlas) → CPU canvas readback + GPU upload (uploadAtlasToTexture). Splits async I/O from sync GL work so callers can sequence GL state changes.
  • Texture-array sizing trusts layer 0: createEmptyAtlas uses layers[0].tileSize / gridCols / gridRows for the whole array. Mixed-size layers within a category are not supported.
  • CPU readback via 2D canvas: each PNG is drawImage’d into a fresh <canvas>, then getImageData(...).data is wrapped as Uint8Array(data.buffer) for upload. This is the standard browser path to get raw RGBA bytes from an HTMLImageElement.
  • Soft-fail on missing manifest: returning null (rather than throwing) lets callers gate on atlas availability without try/catch — consistent with the workbench being a dev tool that may not have baked assets.
  • Duck-typed batch: the parameter is a structural interface (createEmptyAtlas, uploadLayer), not a concrete class import, so this module stays renderer-agnostic.

EXTRACT-CANDIDATE

  • AtlasManifestFile layer schema (componentId, layerIndex, frameCount, fps, tileSize, gridCols, gridRows, loopMode, pivot, bakedAt) is the same shape the bake pipeline writes and the sampler reads. Worth promoting to a shared type in atlas-format.ts if it isn’t already, so loader / baker / sampler can’t drift.
  • The “image canvas getImageData Uint8Array” RGBA-extraction helper is generic; if any other module needs raw bytes from an HTMLImageElement, extract imageToRgbaBytes(img, w, h) into a shared util.