PURPOSE
Owns the active camera: ship-follow positioning, smoothed zoom, transient screen-shake, transient zoom-pulse, and world↔screen coordinate conversion. Exports a single Camera object used by gameplay, rendering, weapons, and combat code.
OWNS
- Module-scoped screen-shake state:
_shakeAmp,_shakeT,_shakeDur,_shakeLastX,_shakeLastY. - Module-scoped zoom-pulse state:
_zoomPulseFactor,_zoomPulseT,_zoomPulseDur,_zoomPulseEasing,_zoomPulseLastDelta. - The per-frame mutation of the shared
camerastate object (position, zoom, target position, target zoom). - Linear-decay shake math and four easing curves for zoom-pulse decay (
linear,easeOutCubic,easeOutQuart,easeInOutCubic). - Coordinate-conversion helpers (
toS,toSx,toSy,toW).
READS FROM
game.phasefrom../core— only follows ship when phase isplayingorbirth.ship.x,ship.y,ship.heat,ship.warpTfrom../core— follow target, heat-zoom input, warp-puddle zoom multiplier.W,Hfrom../core— canvas width/height for centering intoS/toW.CFG.HEAT_ZOOM_S,CFG.HEAT_ZOOM_A,CFG.BASE_ZOOM,CFG.CAMERA_LERPfrom../core— heat-zoom curve start, heat-zoom amplitude, base zoom level, position-lerp rate.lerpfrom../core/utils.
PUSHES TO
- The shared
camerastate object: writescamera.x,camera.y,camera.zoom,camera.targetX,camera.targetY,camera.targetZoomeach call toupdate.
DOES NOT
- Does not draw anything — purely state and math.
- Does not own the
camerastate object itself (created bymakeCamerainengine/core/state.ts). - Does not handle boss-state camera overrides. Comment explicitly forbids re-introducing boss-state camera coupling; bosses are disabled and the camera stays pure player-follow with heat-zoom for the full level.
- Does not clamp camera to room/arena bounds (room clamping lives in
engine/boss/boss-room.ts, applied afterCamera.update). - Does not allocate in
toSx/toSy(the single-axis variants are zero-alloc for tight loops);toSandtoWreturn a fresh{x, y}object.
Signals
Camera.shake(amp, dur)— Triggers screen-shake. Amplitude in pixels (world-space), duration in seconds. Stronger-shake-wins replacement: incoming shake is ignored ifamp × duris not greater than the current_shakeAmp × _shakeT. Resets_shakeTtodur.Camera.zoomPulse(factor, dur, easing?)— Triggers a one-shot multiplicative zoom pulse.factor < 1pulls back,factor > 1punches in. Decays back to1.0overdurseconds using the chosen easing curve (defaulteaseOutCubic). Stronger-pulse-wins replacement by|1 - factor| × dur. Ticks on wall-dt — independent of gametimeDilation.Camera.update(dt)— Per-frame: removes last frame’s shake offset, setstargetX/Y/Zoomfrom ship state duringplaying/birth, lerps camera toward target, applies new shake offset, applies new zoom-pulse delta. Must be called once per frame before anytoScalls.Camera.toS(wx, wy)— World → screen, returns{x, y}.Camera.toSx(wx)/Camera.toSy(wy)— World → screen, single axis, no allocation.Camera.toW(sx, sy)— Screen → world, returns{x, y}.
Entry points
engine/core/loop.tscallsCamera.update(simDt)once per simulation step.engine/physics/movement.tsalso callsCamera.update(dt)and firesCamera.shakefor over-heat thrust and one-shot stop-shake.engine/combat/damage.tsfiresCamera.shakeon player hits, hull damage, and shield breaks.engine/combat/collision-resolver.tsfiresCamera.shakevia per-enemydef.hitShake.engine/weapons/weapons.tsfiresCamera.shakevia per-weapondef.fireShake.engine/bridge.tsfiresCamera.shakefor event-driven shakes.engine/vfx/boss-layers.tsfiresCamera.zoomPulsefor boss-layer transitions.data/bosses/doomsayer.tsfiresCamera.shake(24, 0.8).- Rendering, VFX, world, and HUD modules consume
Camera.toS/toSx/toSy/toWfor coordinate conversion.
Pattern notes
- Anchor cleanup. Both shake and zoom-pulse store the last-applied delta and subtract it at the top of the next
updatebefore re-lerping. Keeps the camera anchor andcamera.zoomundisturbed by transient effects. - Follow offset. During
playing/birth,targetY = ship.y + 60 / camera.zoom— camera sits 60 px south of ship center (in screen pixels, zoom-corrected), so ship appears higher on screen. - Heat zoom. Piecewise: below
HEAT_ZOOM_S(0.75) heat fraction, zoom stays atBASE_ZOOM. Above, linearly ramps up byHEAT_ZOOM_A(0.12) across the remaining heat range. - Warp zoom. When
ship.warpT > 0, target zoom is multiplied by1 + 0.2 * ship.warpT. Multiplicative so it stacks cleanly with heat zoom. - Lerp rates. Position uses
CFG.CAMERA_LERP(6). Zoom uses a hardcoded rate of4. - Shake decay. Linear from
_shakeAmpatt = durdown to0att = 0. Random per-frame offset in±magon both axes. - Stronger-wins replacement. Both
shakeandzoomPulsecompare remaining strength (amp × t) of any in-flight effect against the new request’s full strength (amp × dur). A weaker incoming effect cannot interrupt a stronger active one. - dt sources. Camera position/zoom lerp and shake decay tick on
simDt(called fromloop.ts). Zoom-pulse decay ticks on the samedtpassed in but documented as “wall-dt only” — see callers for whichdtthey pass.