PURPOSE

Full-scene post-processing pipeline applied to the gameplay layer after world/particle/effect drawing but before HUD. Provides cinematic mastering through bloom, atmospheric fog, world- and screen-space dust motes, bokeh lens particles, vignette, CRT scanlines, god rays, and CSS-filter contrast/saturation. Per-planet presets (dark, sunlit) toggle vignette, god rays, and bloom intensity. Mobile gets a lighter pipeline (1/8-resolution bloom, vignette only).

OWNS

  • Bloom scratch canvases (_bloomCvs at 1/4 res; _bloomLiteCvs at 1/8 res for mobile) with viewport-resize rebuild guards.
  • Mastering preset table PP_PRESETS keyed by dark / sunlit, with _activePreset mutable at runtime via setMode / patchPreset.
  • Behind-ship dust pool (_dustBehind, 40 motes in world space with parallax 0.3) and above-ship dust pool (_dustAbove, 20 motes in screen space).
  • Pre-baked mote stamp (_moteStamp, 128px warm radial gradient) and bokeh stamp (_bokehStamp, 128px warm-core / cool-ring soft circle).
  • Atmospheric fog noise tile cache _fogTiles keyed by ${type}_${tintR}_${tintG}_${tintB}, plus legacy _fogTile for the default noise variant.
  • Pre-baked vignette canvas (_vignetteCvs, oversized by VIGNETTE_MARGIN on each side, elliptical squeeze 0.85) and scanline canvas (_scanlineCvs, 1px lines at 3px gap, alpha 0.025).
  • Internal game time accumulator _gameTime, advanced inside update.
  • Seeded RNG _sr, hash noise _hashNoise, tileable _valueNoise, and _fbm octave summation for fog texture synthesis.

READS FROM

  • W, H, camera from ../core for viewport dimensions and world-to-screen transforms.
  • camera.x, camera.y, camera.zoom for dust parallax projection and fog drift offsets.
  • PostProcessingMode type from ../../data/planet-config for setMode argument shape.
  • _activePreset fields (bloomAlphaMult, vignetteEnabled, contrastBoost, saturationShift, godRaysEnabled) gating most pipeline branches.

PUSHES TO

  • The caller-supplied CanvasRenderingContext2D — composites bloom (lighter), dust motes (source-over with globalAlpha), fog (screen), contrast/saturation (CSS filter self-blit), vignette (source-over drawImage), god rays (screen with rotated linear gradients), and scanlines (source-over drawImage).
  • No data stores, no events, no telemetry — purely pixel output and internal canvas caches.

DOES NOT

  • Does not own or modify world entities, simulation state, or input.
  • Does not draw HUD, UI, or text — strictly screen-space visual mastering of the gameplay layer.
  • Does not allocate per frame in the hot path; dust pools, stamps, fog tiles, vignette, and scanlines are all pre-baked or pool-reused.
  • Does not handle shield-break inverse-color overlay (section header exists but body is empty).
  • Does not own Camera instance management; the Camera import is unused outside of the type reference at top.
  • Does not provide a high-quality fog tile rebuild on resize (fog tiles are size-fixed at FOG_TILE_SIZE = 512).
  • Does not validate preset names beyond falling back to dark for unknown modes.

Signals

  • setMode(mode) is the planet-level swap signal — called at run start and level_advance based on PlanetDef.postProcessing.
  • patchPreset(patch) is the VFX-dashboard live-tweak signal — mutates the active preset in place via Object.assign.
  • quality parameter on drawScreenEffects ('high' | 'mobile' | 'low') gates which pipeline stages run; 'low' is the thermal-emergency mode that drops everything except vignette.
  • Viewport resize is implicit — every per-frame draw helper checks current W/H against cached dimensions and rebuilds its scratch canvas inline.

Entry points

  • PostProcessing.update(dt) — advances _gameTime, lazy-initializes both dust pools on first call, advances dust positions and wraps them in world/screen space.
  • PostProcessing.drawFog(ctx, alphaOverride?, texType?, tintR?, tintG?, tintB?, parallaxMult?, speedMult?) — draws a tileable fog layer at the requested texture variant (noise | wispy | clouds | dense | patchy); call after shadows, before sticker blit.
  • PostProcessing.drawDustBehind(ctx) — projects world-space behind-dust motes to screen space using camera parallax and zoom; call inside drawWorld immediately before the sticker blit pass.
  • PostProcessing.drawBokeh(ctx, count = 12) — draws screen-space soft bokeh circles at deterministic hash-seeded positions with slow sine drift.
  • PostProcessing.drawScreenEffects(ctx, quality) — composites bloom (full or lite or skipped) → above-dust (high only) → contrast/saturation filter (preset-gated) → vignette (preset-gated) → god rays (preset-gated, not in low) → scanlines (high only).
  • PostProcessing.setMode(mode) — swap active preset; unknown modes fall back to dark.
  • PostProcessing.patchPreset(patch) — partial in-place override of the active preset.

Pattern notes

  • Pre-bake everything that is constant per resolution: vignette, scanlines, fog tiles, mote stamp, bokeh stamp. The hot path is a sequence of drawImage blits with globalAlpha and globalCompositeOperation swaps; no per-frame createRadialGradient, no createImageData, no per-mote gradient construction.
  • Downsampled bloom uses CSS filter: blur(Npx) on the offscreen scratch canvas, then a single 'lighter' additive blit back at full size. Sunlit-class presets with bloomAlphaMult >= 3.0 accumulate multiple passes (3) for layered glow.
  • Bloom blur radius scales with preset: BLOOM_BLUR_PX * bloomAlphaMult * 0.5 clamped to 60px. Same scaling for mobile lite bloom.
  • Dust mote pool is fixed-size with seeded RNG (_sr based on Math.sin of large primes), giving deterministic but visually-random initial placement. Wobble is per-mote sine/cosine of _gameTime against a per-mote phase, speed, and amplitude.
  • World-space dust wraps inside a DUST_BEHIND_FIELD-sized box centered on camera.{x,y} * DUST_BEHIND_PARALLAX. Screen-space dust wraps at viewport edges with a radius * 2 margin so blobs do not pop in or out.
  • Fog texture builder uses tileable FBM (_fbm summing octaves of _valueNoise with periodic wrap-around). Variants encode pattern shape — noise is classic FBM, wispy is ridged absolute-value with sharpening, clouds uses domain warping, dense is low-frequency, patchy thresholds FBM for hard clear/thick zones. Each tile is then softened with a 4-pass low-alpha self-blit before being cached.
  • Fog parallax and drift are independently scalable per call via parallaxMult and speedMult, so multiple layers can share the same texture cache at different perceived depths.
  • Vignette is drawn into an oversized canvas (screen + VIGNETTE_MARGIN * 2 per axis) by filling pure black and then carving a radial gradient hole with 'destination-out'. Elliptical squeeze (VIGNETTE_H_SQUEEZE = 0.85) achieves a portrait-friendly tighter-sides look. The runtime blit offsets by -VIGNETTE_MARGIN so edges are guaranteed pure black even after camera zoom shenanigans.
  • Contrast/saturation pass self-blits the canvas with a CSS filter string; this is only invoked when the preset actually requests non-zero values, so default presets pay no cost.
  • God rays are tall rotated linear gradients composited with 'screen' blend at very low alpha; angle drifts at GOD_RAY_ROTATION_SPEED rad/sec; each successive beam is wider and slightly more transparent. Suppressed in 'low' quality.
  • Public surface is a single PostProcessing object literal, not a class — module-level singletons hold all state, matching the codebase’s other rendering subsystems (e.g. background.ts).