PURPOSE

Render pipeline orchestrator for the game’s main canvas. Owns the 2D canvas context, manages a named layer system with z-index ordering, sets up viewport clipping and DPR transforms each frame, initializes the WebGL2 sprite batch + atlas (with Canvas 2D fallback), and exposes camera-aware world/screen coordinate conversion and visibility culling. Ported from the legacy 08a-render-core.js.

OWNS

  • Renderer class — holds the canvas element, its CanvasRenderingContext2D, a Map<string, RenderLayer> of named layers, and a layerOrder array sorted ascending by zIndex.
  • webglReady boolean flag — true once the sprite batch atlas has been uploaded.
  • Default render layers created in initializeLayers: background (z=0), world (z=10), particles (z=20), effects (z=30), hud (z=100).
  • RenderLayer interface — { name, zIndex, entities: any[] }.
  • Per-frame canvas state: DPR transform via ctx.setTransform(dpr, 0, 0, dpr, 0, 0), opaque black clear, and a viewport clip rect of (0, 0, W, H).
  • safeRadialGradient helper — clamps radii against zero / NaN / negative / inverted ordering before calling ctx.createRadialGradient.
  • PAL constant — the game-wide named color palette (player, enemy, shield, xp, station, heat, boss, beacon, text, neutral families).

READS FROM

  • ../coreW, H (CSS-pixel viewport size), camera (with x, y, zoom), dpr (device pixel ratio), game.
  • ../core/configPERF_FLAGS.noBackground (skips the background draw pass when set, e.g. via ?noBackground).
  • ../core/render-diagdiagBeginPass / diagEndPass for per-pass diagnostic instrumentation (wraps the background pass).
  • ./backgroundBackground.draw(ctx) for the procedural backdrop pass.
  • ./post-processingPostProcessing import (post-FX module).
  • ./sprite-batchinitSpriteBatch, getSpriteBatch, getGlowBatch for the WebGL2 batched sprite path.
  • ./atlas-builderbuildAtlas to produce the texture atlas canvas uploaded to the GPU.

PUSHES TO

  • The HTML <canvas> element supplied at construction — both via the 2D context for layered drawing and via the WebGL sprite/glow batches (which call uploadAtlas and resize(W, H, dpr)).
  • Console — a single console.warn when WebGL2 is unavailable and the renderer falls back to Canvas 2D.
  • Caller-supplied draw callbacks — render() invokes the passed drawBackground, drawWorld, drawParticles, drawEffects, optional drawLighting, and drawHUD functions in fixed order.

DOES NOT

  • Does not draw entities itself — endFrame iterates layerOrder but performs no per-entity drawing; layers are populated and drawn by external systems via the render() callbacks.
  • Does not flush the sprite batch — uploads the atlas and resizes the batches, but flushing is owned elsewhere.
  • Does not own camera state or update camera position — only reads camera.x, camera.y, camera.zoom from ../core.
  • Does not apply post-processing — PostProcessing is imported but not invoked in this file.
  • Does not resize the WebGL batch viewports on resize() — only the backing-store dimensions on the canvas are updated, guarded against no-op reassignment.
  • Does not handle input, animation, physics, or game state.

Signals

  • webglReady (public field) — observable flag indicating the WebGL2 sprite batch is ready and the atlas is uploaded; false when WebGL2 is unavailable.
  • console.warn('WebGL2 sprite batch unavailable — using Canvas 2D fallback') — emitted once during _initSpriteBatch if initSpriteBatch() returns falsy.
  • diagBeginPass('background') / diagEndPass('background') — render-diagnostic markers around the background draw inside beginFrame.

Entry points

  • new Renderer(canvasElement) — constructs the renderer, grabs the 2D context (throws 'Failed to get 2D context' on failure), initializes default layers, and initializes the sprite batch.
  • render(drawBackground, drawWorld, drawParticles, drawEffects, drawHUD, drawLighting?) — main per-frame entry. Calls beginFrame, then invokes the four world-draw callbacks in order, then endFrame, then the optional drawLighting, then drawHUD last (HUD is drawn on top of any darkness overlay and is un-post-processed).
  • beginFrame() — applies the DPR transform, clears to #000000, sets viewport clip, and runs the background pass (gated by PERF_FLAGS.noBackground).
  • endFrame() — restores the viewport clip and iterates layerOrder (composite pass; per-entity drawing happens elsewhere).
  • createLayer(name, zIndex) / getLayer(name) / addToLayer(layerName, entity) / clearLayers() — layer management API.
  • worldToScreen(wx, wy) / screenToWorld(sx, sy) — camera-aware coordinate conversion using camera.x, camera.y, camera.zoom, and W/H.
  • isVisible(wx, wy, radius=0, margin=100) — viewport visibility test in screen space with default 100-pixel margin.
  • getVisibleBounds() — returns axis-aligned world-space rect of the current viewport with a fixed 500-world-unit margin scaled by 1/camera.zoom, suitable for spatial-query culling.
  • setViewportClip() / restoreViewport() — paired ctx.save + clip and ctx.restore for clipping draws to the canvas viewport.
  • getCanvas() / getContext() / getDimensions() — accessors.
  • resize(width, height) — reassigns canvas.width / canvas.height only when dimensions change (no-op guard).
  • safeRadialGradient(ctx, x0, y0, r0, x1, y1, r1) — exported standalone helper that clamps radii and coordinates before delegating to ctx.createRadialGradient.
  • PAL — exported palette constant.

Pattern notes

  • Game code draws in CSS-pixel coordinates; the DPR transform applied at the start of each beginFrame scales those calls to the canvas backing store’s native device resolution. setTransform (not scale) is used so prior transforms are reset rather than compounded.
  • Layer composition is currently nominal — layerOrder is sorted on insertion, but endFrame does not draw layer entities. Systems pre-draw into the canvas during the render() callbacks, and the layer map is a registry surface (addToLayer, getLayer) for systems that want a shared entity bucket.
  • render() defines a fixed pass order: background → world → particles → effects → (viewport restore) → optional lighting → HUD. Lighting/darkness sits between the world and the HUD so the HUD is never dimmed. The HUD is intentionally drawn outside any post-processing chain.
  • WebGL2 sprite batch is opt-out by failure: initSpriteBatch() returning falsy triggers a warn and a silent Canvas 2D fallback — there is no hard error and webglReady stays false. The atlas canvas is shared between the alpha sprite batch and the additive glow batch, and both are sized to (W, H, dpr) at construction.
  • resize() deliberately guards the backing-store reassignment because writing canvas.width / canvas.height resets the entire drawing buffer (transforms, styles, clip, content), causing performance collapse and visual glitches if done every frame.
  • safeRadialGradient exists because createRadialGradient throws on zero / NaN / negative / inverted radii; it clamps r1 to 0.001, forces r0 < r1, and zeroes non-finite coordinates to keep the renderer crash-free in the presence of bad input.
  • PAL is a flat string-keyed object — colors are looked up by name (e.g. PAL.player, PAL.enemyEngine) so palette tweaks propagate everywhere without touching draw sites.