player bar: a complete waveform arrives as one motion instead of popping

Chosen on waveform-arrive.html (M2, direct morph, 1000ms): when a track
has its whole waveform at once — a local file, a fully-cached stream, or
the final decode replacing a stream's progressive peaks — every bar eases
from whatever the lane currently shows straight into the new shape, all
together. From the resting dots that reads as a uniform grow; from a
mid-sweep swell or the previous track's bars it is literally one waveform
transforming into another. The streaming rise keeps its left-to-right
story untouched; the morph fires only on a peaks swap at coverage 1, and
never on a resize, which re-lays the same shape and stays instant.

The canvas keeps a record of what it actually painted last frame — dots,
swell, rise, morph or settled — so the morph always starts from the truth
on screen, and the fade colours (accent, boundary blend, hover wash) stay
live through it. The rail mirrors the motion in CSS: an `arriving` class
carries the same 1000ms house-ease height transition, placed after the
rise rule so a stream's final decode arrives on this ease too.

Measured live on a cached start: bar heights walked 5.2 → 8.2 → 10.6 →
12.3 → 13.6 → 14.6 → 15.0 and settled at 15.1 at the 1000ms mark — the
ease-out curve, frame by frame.
This commit is contained in:
KeelrLeek committed 2026-08-03 16:46:37 -05:00
1 parent 4c9a6e312a
commit 1765de65cf
3 files changed
+119 -11

No files matched your search

+33
View File
@@ -37,6 +37,7 @@
BAR_W,
waveLevels,
RISE_CADENCE_MS,
ARRIVE_MS,
PENDING_ALPHA,
} from "../lib/waveshape.js";
@@ -398,6 +399,28 @@
return wavePending || v == null || (railRising && i >= railRevealed);
}
// The arrival morph — the playbar's M2, in CSS: when a COMPLETE waveform
// lands (local track, cached stream, a stream's final decode), the
// `arriving` class puts an ARRIVE_MS height transition on every bar, so
// the whole lane eases from whatever it showed into the new shape at
// once. Svelte reuses the <i> nodes across the swap, which is what makes
// the transition run from the old inline heights.
let railArriving = $state(false);
let railArrivePeaks = null;
let railArriveTimer = null;
$effect(() => {
const w = player.waveform;
if (w.length > 0 && railCoverage >= 1 && w !== railArrivePeaks) {
railArrivePeaks = w;
railArriving = true;
clearTimeout(railArriveTimer);
railArriveTimer = setTimeout(() => (railArriving = false), ARRIVE_MS + 80);
} else if (w.length === 0) {
railArrivePeaks = null;
}
});
$effect(() => () => clearTimeout(railArriveTimer));
/* `bars` is already shaped 0..1 by waveshape.js (which also explains, with
measurements, why the old gain-and-clamp rule here rendered half the
lane at one height and had to be propped up with per-bar noise). This
@@ -1082,6 +1105,7 @@
class:pending={wavePending}
class:loading={wavePending && player.wavePending}
class:rising={railRising}
class:arriving={railArriving}
style="--bar-gap: {BAR_PITCH - BAR_W}px; --bars: {waveBars.length}"
role="slider"
tabindex="0"
@@ -1978,6 +2002,15 @@
height 2000ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 400ms linear;
}
/* Arrival morph — the playbar's M2: a complete waveform eases in as one
motion, every bar at once. 1000ms is ARRIVE_MS in waveshape.js — change
them together. After `.rising` so a stream's final decode arrives on
this ease even while the rise class lingers. */
.wave.arriving i {
transition:
height 1000ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 400ms linear;
}
.times {
display: flex;
justify-content: space-between;