PURPOSE
Pure Canvas 2D renderer for warp puddles. Draws each puddle group from the outside as a deep-space nebula seen through a crisp circular window with a pulsing purple halo and crackling electric arcs, and from the inside as the same nebula expanded across the whole screen with the non-puddle world masked to black. Visual silhouette is pixel-identical to the gameplay collision circle: the rendered edge is a plain arc at the exact collision radius, with oscillation and arcs layered on top as additive decoration that never deforms the boundary.
OWNS
- A single lazily-built 1024 by 1024 nebula background canvas, cached in a module-level variable for the lifetime of the renderer. Generated procedurally on first use from a deterministic seed using big soft cloud gradients in a violet-blue / navy / wine palette, a handful of tighter brighter cores, and roughly 380 stars (mostly neutral, occasional warm amber).
- Constants for edge color, edge glow, outer halo color and reach multiplier, electric arc color, and the per-frame arc budget.
- The procedural arc-hash function used to seed short-lived rim sparks deterministically per frame.
- The orchestration choice of which groups are drawn as exterior vs interior given the warp transition value and the active group id.
READS FROM
camera(position and zoom) from../core/statefor world-to-screen projection.Clock.now()from../core/clockfor the animation time used by halo pulse, nebula scroll, and arc seeds.- The
WarpPuddleGroupshape from../world/warp-puddles: per-groupid, axis-alignedbboxfor visibility culling, and themembersarray, where each member supplies worldx,y, collisionradius, and acolorSeedused to keep arc placement varied between members. - The canvas dimensions on the supplied
CanvasRenderingContext2Dand theviewW/viewHpassed to the public entry point for visibility culling. - The
warpTtransition value supplied by the caller, which selects the exterior alpha (1 - warpT) and the interior intensity (warpT).
PUSHES TO
- The provided 2D context. All drawing is direct Canvas 2D:
arc,fill,stroke,drawImageof the cached nebula canvas, with composite operations toggled betweensource-overandlighter. Interior masking uses a blackrectwith counter-clockwise member arcs filled with theevenoddrule to carve the union-shaped holes. - A module-private DOM canvas created via
document.createElement('canvas')on first nebula build.
DOES NOT
- Does not mutate any puddle, group, or member state. Read-only with respect to game state.
- Does not run any per-pixel shader, WebGL pass, or filter. No
ctx.filter, no offscreen blur. The rippling effect is achieved by drawing the nebula twice at different scroll speeds and composite modes. - Does not displace, warp, or animate the silhouette. The visible edge is always
ctx.arc(sx, sy, sr, 0, TAU)at the exact collision radius. The pulsing glow varies stroke width only; arcs ride on the rim but use the same radius for their endpoints. - Does not compute groupings, collisions, transitions, or activation; those belong to the world module that produces
WarpPuddleGroup. - Does not register itself into any global render queue or store; the engine bridge calls it directly each frame.
- Does not allocate per-frame typed-array buffers, retain particles, or schedule deferred work.
Signals
warpTin[0, 1]drives the cross-fade: exterior groups use1 - warpTas their alpha, and the active interior useswarpTfor both the black mask alpha and the nebula intensity. The active group is skipped from the exterior pass wheneverwarpT > 0so the player does not see the same group drawn twice during the transition.activeGroupIdidentifies which group renders as interior. When null, only exteriors draw.- Early-out: the public entry returns immediately if there are no groups and
warpT <= 0. The exterior pass is skipped entirely when the exterior alpha falls below0.01. Each group is bbox-culled against the view rect in screen space. - Pulse signal for the outer edge glow is
0.85 + 0.15 * sin(t * 2.4)applied to stroke width, so the glow breathes without changing the silhouette. - The arc generator advances at twenty new spark seeds per second (
floor(t * 20)) and emits a fixed budget per member per frame.
Entry points
renderWarpPuddles(ctx, groups, activeGroupId, warpT, viewW, viewH)— public per-frame entry. Iterates groups, draws exteriors for visible non-active groups, then draws the interior pass for the active group whenwarpT > 0. Called by the engine bridge.disposeWarpPuddleRender()— drops the cached nebula canvas so it can be rebuilt on next use. Called on teardown.resizeWarpPuddleRuntimes(viewW, viewH)— no-op stub kept for symmetry with WebGL-backed renderers. Canvas 2D has nothing to resize. Called by the engine bridge on resize.- Internal:
getNebulaBg,addClipUnionPath,fillNebulaClipped,drawCrispEdge,arcHash,drawEdgeArcs,groupIsVisible,drawExterior,drawInterior.
Pattern notes
- Crisp-edge invariant: every visible boundary uses the member’s exact collision radius at the projected center. The collision test in the world module is a plain circle test against the same radius and center, so visual and gameplay boundaries match pixel-for-pixel. Wobble effects are explicitly forbidden inside the silhouette and live only as additive decoration outside or along the rim.
- One texture, two layers, zero shaders: rippling motion is faked by drawing the cached nebula four times for tiling, then again four times counter-scrolled at a different speed in
lightercomposite at lower alpha. Cross-interference of the two layers reads as motion without any per-pixel work. - Wrap-around tiling: scroll offsets are clamped into
[0, bg.width)and[0, bg.height)with a double-modulo to handle negative values, then the texture is drawn at four positions (origin, +width, +height, +width and +height) to cover the screen. - Interior masking uses the even-odd fill rule: a full-screen black rectangle is begun, then each member adds a counter-clockwise arc into the same path. Filling with
evenoddcarves the puddle-shaped holes in a single fill. - Nebula generation uses a linear congruential pseudo-random with a fixed seed so the background is identical across reloads.
- The nebula canvas is module-level (not per-instance), shared across all groups and across the exterior and interior passes, so the world looks visually consistent when transitioning into any puddle.
- Arc endpoints are computed from
cos(theta)/sin(theta)at the boundary radius, with a small jagged polyline drawn between them whose perpendicular jitter is bounded by three percent of the projected radius — small enough to read as crackle, large enough to be visible. - The renderer is defensive only at the DOM boundary:
getNebulaBgreturnsnullwhendocumentis undefined (SSR) or when the 2D context is unavailable, and the fill function bails out silently in that case.