PURPOSE
All canvas drawing for the hub-and-spoke world layer: hub circles, spoke corridors, lanterns at hub centers, ambient particles inside hubs, flow particles along spokes, lantern activation burst VFX, and a multiply-blend danger tint over dark hubs. Every color derives from the 4-slot palette on LevelConfig (base, line, accent, illuminated) plus the separate illuminationColor. No hardcoded colors except the danger tint hex and a white-hot beacon core. Illuminated hubs/spokes swap their line color references to illuminated at reduced opacity.
OWNS
- All Canvas2D drawing for hubs, spokes, lanterns, lantern bursts, flow particles, ambient particles, and danger tint.
- Three style switches mapped to draw branches:
SpokeStyleId(highway|energy|ghost),HubStyleId(station|nexus|crater),HubParticleType(none|dust|embers|data|spores). - Three
LanternPresetIdburst animations:solar_flare,dawn,shatter, with hard-coded durations inBURST_DURATIONS(1.0s, 1.5s, 0.5s). - Local helpers:
hexToRgb,rgbaStr,toScreen,hashId,lerp. - Per-hub deterministic seeding for ambient particles via
hashId(hub.id)combined with prime multipliers. - Crater-style deterministic rim wobble keyed off hub world position.
READS FROM
LevelData.config(LevelConfig):palette,spokeStyle,spokeWidth,spokeWMin,spokeWMax,spokeFlow.enabled,spokeFlow.speed,hubStyle,hubParticles,illuminationColor,lanternPreset,dangerTint.LevelData.generation.hubsandLevelData.generation.spokesfor geometry.LevelData.illuminationMap: Map<string, boolean>for per-hub lit state (spoke is lit if either endpoint hub is lit).visibleHubIndices: Set<number>from the caller to cull both hubs and spokes (spoke kept if either endpoint is visible).lanterns: Map<string, LanternState>fromengine/world/illuminationforactivatedflag andactivatedAttimestamp.time: number(seconds) for rim rotation, lantern pulse, particle motion, burst progress, and flow scroll.CameraXform { x, y, zoom, halfW, halfH }matchingCamera.toS()semantics.
PUSHES TO
- Mutates the passed
CanvasRenderingContext2Donly. Every public draw function brackets its work inctx.save()/ctx.restore(). drawLanternsuses a nestedsave/restorearoundglobalCompositeOperation = 'lighter'for additive beacon glow.drawDangerTintOverlayswitchesglobalCompositeOperationto'multiply'then restores to'source-over'beforerestore().- No state writes outside the canvas context. No allocations into
LevelDataorlanterns.
DOES NOT
- Does not compute illumination state, lantern activation, or chunk visibility — consumes them.
- Does not own the render order; the call sequence is enforced by
bridge.ts(see header comment). - Does not generate hubs, spokes, or lantern entries.
- Does not draw the atmospheric fog layers (above/below); the wilds fog mask is a no-op stub kept only to preserve call sites.
- Does not branch on hub kind/biome — only on
hubStyle/spokeStyle/hubParticles/lanternPresetfromLevelConfig. - Crater hub style has no inner structure (
drawHubsInnerStructureearly-returns). - Center line and flow particles are restricted to a single style each (
highwaymedian;energyflow), and flow also requiresspokeFlow.enabled.
Signals
litboolean per hub fromilluminationMap.get(hub.id) === true; per spoke isaLit || bLit.LanternState.activatedplusactivatedAt→ burst is drawn whiletime - activatedAtis in[0, BURST_DURATIONS[lanternPreset]).levelData.config.spokeFlow.enabledgates flow particles (also requiresspokeStyle === 'energy').hubParticles === 'none'short-circuits ambient particle pass.dangerTint <= 0short-circuits the multiply overlay; otherwisetintAlpha = dangerTint * 0.3on each dark (unlit) hub.- Unlit lantern pulse uses
0.1 + 0.1 * sin(time * 3.14)(~0.5 Hz). - Nexus rim rotates at
2π/30rad/s (2 RPM, 5 arc segments, 15% gap fraction). - Embers wrap upward over
sr * 2with phase(time * 20 + pSeed) % (sr * 2)and discard points that land outside the hub circle.
Entry points
drawSpokesFill(ctx, levelData, cam, visibleHubIndices)drawSpokesEdges(ctx, levelData, cam, visibleHubIndices)drawSpokesCenterLine(ctx, levelData, cam, visibleHubIndices)drawSpokeFlowParticles(ctx, levelData, cam, visibleHubIndices, time)drawHubsFill(ctx, levelData, cam, visibleHubIndices)drawHubsRim(ctx, levelData, cam, visibleHubIndices, time)drawHubsInnerStructure(ctx, levelData, cam, visibleHubIndices)drawLanterns(ctx, levelData, cam, visibleHubIndices, time)drawLanternBurst(ctx, levelData, cam, visibleHubIndices, time, lanterns)drawHubAmbientParticles(ctx, levelData, cam, visibleHubIndices, time)drawDangerTintOverlay(ctx, levelData, cam, visibleHubIndices)drawWildsFogMask(_ctx, _levelData, _cam, _viewW, _viewH)— intentional no-op stub.
Render order (from header, enforced by bridge.ts): spokes fill → spokes edges → spokes center line → spoke flow particles → hubs fill → hubs rim → hubs inner structure → lanterns → lantern burst → hub ambient particles → danger tint overlay.
Pattern notes
- Pure draw module: receives
ctx, geometry, illumination, time; returns void. No game state, no allocations beyond per-frame temporaries. - Style branching is
switchon the config style id, with style-specific opacity tables inlined (e.g., spoke fill alpha is0.2forghost,0.4forenergy,0.7otherwise). - Spoke width derives from
lerp(spokeWMin, spokeWMax, spokeWidth)and is doubled then scaled by zoom for stroke width; edge offsets use a perpendicular unit normal computed fromdx, dy, len = hypot(dx, dy) || 1. - Illumination changes color (line → illuminated) and reduces alpha rather than adding extra strokes (except
stationrim andnexusrim, which add a bloom pass at0.06alpha and4 * cam.zoomwidth when lit). - Crater wobble is
1 + 0.06 * sin(a * 7 + hub.x * 0.1) + 0.04 * cos(a * 11 + hub.y * 0.1)over 32 segments — deterministic per hub from world position. - Ambient particle determinism:
seed = hashId(hub.id), per-particlepSeed = seed + i * 7919, then two prime-multiplier hashes (2654435761,1597334677) mod 2^32 normalized to[0,1)for base angle and base radius. Count ismin(30, floor(hub.r / 3)). - Spoke flow uses
DOT_COUNT = 7evenly spaced dots scrolling along the spoke with offset(time * speed * 200 * cam.zoom) % len; color ispalette.accent(orilluminatedwhen lit) at alpha 0.3. - Lantern lit state uses
globalCompositeOperation = 'lighter'for three additive layers (wide bloom, core glow, white-hot center) plus a vertical pillar gradient stroke above the hub. - Danger tint uses
globalCompositeOperation = 'multiply'withDANGER_COLOR = '#2a0a0a'; only unlit hubs receive it (spokes are not tinted by this function despite the function name’s plural). drawWildsFogMaskis preserved as an empty stub: the previous implementation produced a checkerboard grid artifact and was deleted; the export remains so callers don’t break.