The portal blooms once at halfway. After that, the bass punches holes in winter.

At swapFrame = round(durationInFrames * 0.5), a bell-shaped swapOpen value peaks at 1. The radial portal that’s been opening and closing with the bass since the track started now expands by an additional 1,500 pixels of radius. Spring is pushed to a thin ring at the edges. Winter fills centre frame. Then the layers commit: winter goes front, spring drops behind. swapOpen collapses back toward 0 over the next 50 frames. The second half of the track runs the same bass-mask geometry (same radial hole, same amplitude-to-radius curve, same rendering pass), but now the hole punches through winter and shows you spring.

The bloom at halfway is deliberate. It isn’t a bass hit. It isn’t beat-aligned. It’s a fixed structural event tied to track position, not audio analysis.

Two panoramas, same dimensions

Both backgrounds are 3904×1088. They represent the same location in different seasons.

bg-city.jpg is the spring layer: swirling cherry-blossom petal light-trails over neon Tokyo, 3.4 MB. bg-winter.jpg is the winter layer: snowball fight, snow-laden blossoms, neon skyline at sunset, 3.3 MB.

The winter image was replaced this sprint. The original was a placeholder. The replacement has more snowfall texture and a warmer neon-reflection palette. Dimensions stayed at 3904×1088, so nothing in the code changed. BG_W, BG_H, parallax maths, canvas position calculations: all untouched. A file swap, and only that simple because the dimensions were right first time.

Both panoramas scroll laterally with the audio to produce parallax. The dimensional match is load-bearing: the back layer isn’t scaled or fitted into the canvas, it occupies the same coordinate space as the front. When the hole opens, the alignment is pixel-accurate.

The bass mask

The front panorama carries a radial alpha mask. Its radius scales with the current bass amplitude. Quiet section: radius small, front layer fills the frame, back layer invisible. Bass hit: radius expands, front retreats, back bleeds through the hole.

This isn’t a dissolve. It isn’t a crossfade. It’s an alpha-channel punch: the front layer is transparent inside a circle and opaque everywhere outside it. The bass signal controls the geometry of the punch: how wide the circle opens, how quickly it contracts when the frequency drops.

The effect reads as a window. The foreground is solid except where the bass says it shouldn’t be. Behind the window is a different season.

The halfway swap

Improving on the static two-layer setup described in “The spring panorama has winter underneath it,” the swap mechanic adds a second axis of state to the background: track position.

The swapOpen bell runs over ±50 frames centred on swapFrame. At any frame within that window, holeR += swapOpen * 1500 adds up to 1,500 pixels to the portal radius, far beyond the maximum radius any bass hit achieves during normal playback. The expansion is unambiguously structural. A viewer seeing the track for the first time will read the bloom as an intentional event, not as a loud beat.

At peak swapOpen, the layers swap. The rendering code switches which panorama is drawn front and which is drawn behind. swapOpen then decays back to 0, and the hole contracts to whatever the current bass amplitude dictates. From that frame forward, the front layer is winter. Bass hits punch through winter and reveal spring.

There’s a subtlety in reading the second half. The swap doesn’t announce itself beyond the bloom. Once swapOpen has decayed to zero, the frame reads as “winter with a bass-responsive hole showing spring.” Whether a viewer registers that as “the effect flipped” or simply “the scene changed” depends on how attentive they are and how clearly the bloom read a moment earlier. The bell envelope over ±50 frames exists partly to answer that question: wide enough to register as an event rather than a cut.

The engineering decision was to tie the swap to durationInFrames * 0.5 rather than any musical landmark. No beat detection required, no onset analysis, no frame-accurate audio alignment. The trade-off: if the audio has a dead section at the 50% mark, the bloom lands flat. For this track it doesn’t. Whether that holds for a different audio file is a known unknown. Out of scope: beat-aligned swap triggering.

Making the centre the glow

The portal’s centre posed a visual problem. The radial hole reveals the back layer, but the dead centre of the hole (where the portal radius is largest and the front layer is most transparent) was reading as flat. The surrounding spring layer has saturation and movement from the petal light-trails. The winter image at centre looked dull by comparison.

The first approach was a screen-blend spotlight: a generated glow laid over the image at the portal centre. That added luminosity, but the character was wrong. It was a glow about the image rather than of it.

The fix was to remove the overlay entirely. The winter image sits directly in the portal centre with no augmentation. The richer replacement panorama (warmer palette, more texture in the snow, neon reflections in the ice) provides the visual weight the original placeholder didn’t. No generated glow required.

The connection to the panorama upgrade is direct. The overlay existed partly to compensate for a background that wasn’t visually rich enough. Once the image was rich enough, the overlay was the problem.

What the background is now

Before this sprint, the background had one degree of freedom: portal radius, driven by bass amplitude.

After, it has two:

  • Bass amplitude controls portal radius, frame by frame, throughout the track
  • Track position controls which panorama is front and which is behind, with a single transition at the 50% mark

These axes are independent. The bass can be loud or quiet during either half; the swap happens regardless. Together they produce four observable states:

  • Spring front, quiet: spring fills the frame
  • Spring front, loud: bass punches through spring, winter shows
  • Winter front, quiet: winter fills the frame
  • Winter front, loud: bass punches through winter, spring shows

The visualiser doesn’t label these states. There’s nothing in the code that says “state 3.” But the background has to hold up visually across all four, because any given frame will be in one of them. The panorama upgrade mattered here: a richer winter needs to be as legible at centre frame as the spring layer, both as the thing being revealed and as the thing being punched through.

The first and second halves of the track have different visual identities even when the music uses the same patterns. What looks like one continuous background is two temporal states operating on the same geometry.

Out of scope: a third seasonal layer, dynamic swap timing derived from audio analysis, multiple swap events.

All writing →