The winter scene sat behind the spring one for about twenty minutes before I understood what it was.
Spring in front: cherry-blossom petal trails in neon light over Tokyo, 3904 pixels wide. Winter behind: snow, icicles, kids with snowballs, same dimensions. A radial mask on the front layer, driven by bass, cut a hole wherever the low frequencies were loud enough to punch through. Through the hole: winter. The hole breathed. The scene behind it was static. What I had, without quite intending it, was a temporal portal.
The swap came later. It only worked because of something the hole geometry already had.
What was already running
The visualiser is panoramic. Both seasonal layers are 3904×1088. That’s wider than the output canvas, giving the parallax scroll room to move. They share those dimensions, so the mask geometry needs no coordinate translation between them. The spring layer sits in front; the winter layer sits behind it, invisible outside the hole.
The bass mask is a radial gradient centred on a fixed point in the frame. Quiet passage: small hole, a glimpse of winter. Loud hit: the hole opens. The effect is directional. The eye follows the centre because radial geometry has centres, and centres draw attention. Every frame, the hole is doing two things simultaneously. Responding to audio. Directing visual attention to a specific point.
The swap exploits the second thing.
The centre glow
Before the swap, the portal centre had a screen-blend overlay: a coloured spotlight drawn on top of whatever was visible through the hole. The intent was visual weight at the centre. The problem was that the overlay read as post-processing. It coloured the scene rather than revealed it.
The fix was an observation. The image should be the glow. Not the glow over the image. Removing the screen-blend overlay let the winter scene come through the portal in its own colours. Blossoms under snow, and the skyline behind them, read as content rather than tinted composition.
The replacement winter panorama reinforced this. The original had snow, icicles, and a snowball fight in the foreground: one depth plane. The replacement added snow-laden blossoms at mid-ground and a neon skyline at sunset behind them. Three depth planes. Through a portal, depth matters. The parallax scroll that runs on both panoramas reads more clearly when there is something to parallax between.
Same BG_W and BG_H constants, same mask geometry, same parallax math. A file swap. No code change.
The mid-track swap
At swapFrame = round(durationInFrames * 0.5), the arithmetic midpoint of the render, the panoramas swap roles. Spring goes behind. Winter comes to the front, with the mask now cutting holes in winter to reveal spring beneath.
The mechanics are a bell curve. swapOpen runs from 0 to 1 to 0 across a ±50-frame window centred on swapFrame. At peak, holeR += swapOpen * 1500. The baseline radius from bass is still there; the swap adds 1500 pixels on top of it. That addition is enough to bloom the hole until it covers the frame entirely.
The sequence:
- Bass drives
holeRnormally through the approach frames swapOpenbegins climbing; the hole grows past what bass alone would produce- At peak, the hole covers the frame: winter fully visible, spring eclipsed behind it
swapOpendescends; the hole contracts back toward the bass contribution- The layers have swapped; the hole now cuts winter to reveal spring behind it
No cut. No crossfade. No separate transition layer. The swap happens inside geometry that was already running.
Why the hole geometry owns the reveal
A cut is instantaneous and editorial. A crossfade has its own timing, parallel to the audio. Both work. Both are also disconnected from the bass: they are choices applied to the video layer while the audio does something to the audio layer. The connections are loose.
The portal swap uses the same variable, holeR, that bass has been modifying since frame one. The swap doesn’t introduce a new mechanism. It scales an existing one to a size it hasn’t reached before. That scale difference is what makes the swap legible as a large event rather than a random transition.
The audience doesn’t know holeR by name, but they have internalised the correspondence across the first half of the track: hole size maps to audio intensity. When swapOpen drives holeR to frame-covering scale, the magnitude of the expansion reads as a major audio event even if the track is doing something quiet at that precise moment. The geometry has been building visual expectation since frame one. The swap cashes it in over fifty frames.
A cut or crossfade has to earn that authority in the moment. The portal already has it.
The constraint that doesn’t scale
swapOpen * 1500 is a canvas-size-dependent constant. It works at the current output dimensions. At a different canvas size, the bloom might not cover the frame, or might overshoot.
The structural pattern is portable: bell curve over holeR, centred on a fixed frame, ±50-frame window. The constant is not. If this pattern gets reused at a different canvas size, derive the scale factor from canvas width. Don’t leave it fixed.
Out of scope from this sprint: dynamic derivation. The visualiser has fixed output dimensions; the shortcut holds.
What the swap commits to
The track has two halves. In the first, winter is background, glimpsed through bass-opened holes in spring. In the second, spring is background, visible through holes in winter. The reversal implies a turn. Whether that turn reads as climax, release, or arrival depends entirely on what the music does at swapFrame.
The swap doesn’t impose a reading. It creates a structural event at the midpoint and lets audio colour it. If the midpoint is a quiet bridge, the bloom will be visually large and musically underwhelming. The approach is sound; the track choice matters.
Out of scope: detecting the correct swap frame programmatically from the audio. round(durationInFrames * 0.5) is a deliberate simplification. It works when the track’s structure roughly aligns with its arithmetic centre.
What the pattern is
Two panoramas behind a shared bass-driven mask. The mask opens both. swapOpen is one bell curve wired to holeR at a fixed frame — the only additional code for the scene change.
The reveal mechanism was already running. The question was only how far to drive it.



