The spring panorama is a 3904×1088 strip of cherry-blossom petal light-trails over neon Tokyo. Behind it, at the same dimensions, sits winter: snow, icicles, kids throwing snowballs, the same skyline bleached white and cold. Both images share one canvas. The front one has a hole in it.
When bass energy in the track rises, the radial hole in the spring layer expands, exposing the winter scene behind it. When it drops, spring closes back over itself. The result reads as a window into a different season — a temporal portal, in the terminology of the devlog. No GPU shader. Two drawImage calls with a clip path between them.
The layer model
Winter goes down first as a full-width background. Spring sits on top. Before the spring layer is drawn, the canvas clips to everything outside a circle centred on the frame. The circle’s radius is a function of bass magnitude. Everything inside the circle — the hole — never gets painted. Winter shows through by absence.
The clip path uses winding order. ctx.beginPath(), then ctx.rect the full frame in one direction, then ctx.arc the hole in the opposite direction. ctx.clip(). Opposite-winding paths subtract: the renderer removes the circle from the rectangle and clips to the remainder. Standard 2D canvas compositing. No offscreen buffer, no blend mode, no temporary canvas. The hole exists only as the absence of drawing.
The bass value maps to holeR directly. At rest, the hole is small — a suggestion of something behind the frame. At peak, it dilates to several hundred pixels and winter dominates the centre. The mapping isn’t smoothed; bass energy arrives from the audio analyser as a rolling average. A second smoothing pass would dull the responsiveness without improving stability.
The halfway swap
At swapFrame = round(durationInFrames * 0.5), the two layers exchange roles.
The transition runs as a bell curve. swapOpen goes 0→1→0 over ±50 frames centred on swapFrame. During the bloom, holeR receives an additive swapOpen * 1500. The hole inflates past the edges of the frame. At peak open, both scenes are simultaneously visible across the full canvas — cherry-blossom light-trails bleeding over snowfields, the neon skyline present in both layers at once. Then the swap completes: winter moves to front, spring goes behind, and the hole closes back to bass-driven size.
What was the window becomes the wall. What was the wall becomes the window.
The first half of the track has spring in front with winter visible through bass pulses. The second half inverts the relationship. Same mechanics, swapped layer order. The set-piece doesn’t require any state beyond a frame counter and the bell curve.
The centre
The first build had a screen-blend spotlight at the centre — a coloured glow drawn over the winter image to make the core feel luminous. It looked applied. The image underneath was doing the work and the overlay was suppressing it.
The fix was removing the overlay. The winter scene — neon skyline at sunset, snow catching the light, warm orange against blue-white — is already luminous. The image is the glow. Anything drawn over it pushes it back toward flat.
This is the same correction as removing a drop shadow from an element that already has depth. The instinct to add glow was solving a problem the overlay itself was creating. Without it, the core reads as bright by content.
The panoramas
The spring scene (bg-city.jpg, 3.4 MB) is swirling cherry-blossom petal light-trails over neon Tokyo, 3904×1088. The winter scene (bg-winter.jpg, 3.3 MB) is a snowball fight in front of snow-laden blossoms and a neon skyline at sunset, same dimensions.
The dimension match matters more than the content choice. All the parallax and grade maths reference BG_W and BG_H constants. A replacement panorama at matching dimensions requires no code change; the constants hold and the compositing logic is unaffected. The winter scene was replaced mid-sprint for a richer version — same filename, same dimensions, zero downstream changes.
The neon skyline appears in both scenes. That’s intentional. It’s the thread that keeps the portal reading as coherent rather than disorienting — two moments in the same place, different seasons, the neon constant between them.
What this doesn’t do
The radial hole has a hard edge. Soft edges would need a gradient mask or actual shader work. The hard edge reads cleanly enough at motion that feathering isn’t missed, but it would be the first addition.
The portal is always centred. A tracked portal — one that follows a focal point in the panorama — would need pre-computed anchor data or runtime feature detection. Out of scope.
The halfway swap is time-based, not cue-based. round(durationInFrames * 0.5) is an approximation. A musical cue would require a baked-in cue sheet or onset detection at render time. When the cue is known, replacing * 0.5 with a swapCue parameter is a one-line change.
Where this leaves the build
The visualiser now has two coherent scene states separated by a mid-track inversion, with a bass-responsive window between them throughout. The compositing adds one drawImage call per frame. Performance cost is negligible.
The early instinct was to reach for a shader. Shader-based effects handle soft edges, colour grading, and spatial distortion. The temporal portal seemed like shader territory. What the clip-path approach revealed is that the visual interest here is geometric, not mathematical. The hole is a circle. Its radius changes. Shaders solve a different class of problem.
Feathering is the gap between this and shader quality. Getting there is a canvas gradient mask — a third drawImage call with an alpha channel — not a rewrite of the compositing architecture. The portal works now. That addition is the next iteration.
Two layers. One clip path. Winding-order arithmetic.



