forked from Keelrleek/SoulStream
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:
1 parent
4c9a6e312a
commit
1765de65cf
3 files changed
+119
-11
No files matched your search
@@ -26,6 +26,7 @@
|
||||
SWELL_WIDTH_BARS,
|
||||
RISE_MS,
|
||||
RISE_CADENCE_MS,
|
||||
ARRIVE_MS,
|
||||
PENDING_ALPHA,
|
||||
} from "../lib/waveshape.js";
|
||||
import Icon from "./Icon.svelte";
|
||||
@@ -185,6 +186,14 @@
|
||||
let riseLevels = [];
|
||||
let riseLevelsPeaks = null;
|
||||
let riseLevelsCount = 0;
|
||||
// The arrival morph (M2 on waveform-arrive.html): when a COMPLETE
|
||||
// waveform lands, every bar eases from whatever the lane painted last —
|
||||
// resting dots, a mid-sweep swell, the previous shape — straight into the
|
||||
// new one over ARRIVE_MS. `lastDrawnHeights` is kept current by every
|
||||
// painter so the morph always starts from the truth on screen.
|
||||
let lastDrawnHeights = null;
|
||||
let morphFrom = null;
|
||||
let morphStart = 0;
|
||||
let animRaf = null;
|
||||
|
||||
function paintBar(ctx, x, mid, h, color) {
|
||||
@@ -217,6 +226,7 @@
|
||||
const period = SWELL_SWEEP_MS + SWELL_REST_MS;
|
||||
const u = (now % period) / SWELL_SWEEP_MS; // > 1 during the rest beat
|
||||
const centre = u * (count + SWELL_WIDTH_BARS * 2) - SWELL_WIDTH_BARS;
|
||||
const heights = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
let env = 0;
|
||||
if (pending && u <= 1) {
|
||||
@@ -224,11 +234,25 @@
|
||||
env = Math.exp(-d * d);
|
||||
}
|
||||
const alpha = PENDING_ALPHA + (0.85 - PENDING_ALPHA) * env;
|
||||
paintBar(ctx, i * BAR_PITCH, mid, 2 + env * SWELL_AMP_PX * 2, `rgba(122,122,122,${alpha})`);
|
||||
heights[i] = 2 + env * SWELL_AMP_PX * 2;
|
||||
paintBar(ctx, i * BAR_PITCH, mid, heights[i], `rgba(122,122,122,${alpha})`);
|
||||
}
|
||||
lastDrawnHeights = heights;
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** The fade, one bar at a time — shared by the rise and the arrival
|
||||
* morph, whose heights differ every frame and cannot use the grouped
|
||||
* clip-fills of the settled painter. */
|
||||
function barColor(i, full, playedBars, washing, washEndBar) {
|
||||
if (i < full) return theme.accent;
|
||||
if (i === full && playedBars - full > 0) {
|
||||
return blendAccent(theme.accent, playedBars - full, washing);
|
||||
}
|
||||
if (washing && i > full && i <= washEndBar) return `rgb(${WASHED_RGB.join(",")})`;
|
||||
return UNPLAYED;
|
||||
}
|
||||
|
||||
/** The streaming rise. Released bars ease from the floor to their height;
|
||||
* queued and beyond-frontier bars are pending dots. The colour pass is
|
||||
* the full painter's fade — accent, boundary blend, wash, muted — done
|
||||
@@ -247,9 +271,11 @@
|
||||
const washing = hoverX > playX;
|
||||
const washEndBar = washing ? Math.floor(hoverX / BAR_PITCH) : -1;
|
||||
const full = Math.floor(playedBars);
|
||||
const heights = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const x = i * BAR_PITCH;
|
||||
if (i >= riseReleased) {
|
||||
heights[i] = 2;
|
||||
paintBar(ctx, x, mid, 2, `rgba(122,122,122,${PENDING_ALPHA})`);
|
||||
continue;
|
||||
}
|
||||
@@ -257,17 +283,10 @@
|
||||
const eased = 1 - Math.pow(1 - grow, 3);
|
||||
const target = Math.max(2, (riseLevels[i] ?? 0.12) * H);
|
||||
const h = 2 + (target - 2) * eased;
|
||||
let color;
|
||||
if (i < full) color = theme.accent;
|
||||
else if (i === full && playedBars - full > 0) {
|
||||
color = blendAccent(theme.accent, playedBars - full, washing);
|
||||
} else if (washing && i > full && i <= washEndBar) {
|
||||
color = `rgb(${WASHED_RGB.join(",")})`;
|
||||
} else {
|
||||
color = UNPLAYED;
|
||||
}
|
||||
paintBar(ctx, x, mid, h, color);
|
||||
heights[i] = h;
|
||||
paintBar(ctx, x, mid, h, barColor(i, full, playedBars, washing, washEndBar));
|
||||
}
|
||||
lastDrawnHeights = heights;
|
||||
if (hoverX >= 0) {
|
||||
const LINE_H = 18;
|
||||
ctx.fillStyle = theme.accentHover;
|
||||
@@ -458,6 +477,7 @@
|
||||
// while a waveform request is actually working.
|
||||
if (!peaks || peaks.length === 0) {
|
||||
riseKey = null;
|
||||
morphFrom = null;
|
||||
const moving = drawUnloaded(ctx, W, H, mid, count, now);
|
||||
lastPlayedBars = Number.NEGATIVE_INFINITY;
|
||||
scheduleAnim(moving);
|
||||
@@ -512,6 +532,7 @@
|
||||
const newest = riseReleased > 0 ? (riseBorn[riseReleased - 1] ?? 0) : 0;
|
||||
const growing = riseReleased > 0 && now - newest < RISE_MS;
|
||||
if (waveCov < 1 || riseReleased < count || growing) {
|
||||
morphFrom = null; // the rise owns the lane's motion
|
||||
drawRise(ctx, W, H, mid, count, now, frontier, playFracNow, playedBarsNow);
|
||||
lastPlayedBars = playedBarsNow;
|
||||
lastHoverDrawn = hoverX;
|
||||
@@ -534,6 +555,7 @@
|
||||
wavePathWidth !== W ||
|
||||
wavePathHeight !== H;
|
||||
if (geometryChanged) {
|
||||
const peaksSwapped = wavePathPeaks !== peaks;
|
||||
// Same pitch AND same shaping as the Now Playing rail — both come
|
||||
// from waveshape.js — so the two lanes in the window read as one
|
||||
// instrument and neither can drift from the other. Mirrored about
|
||||
@@ -551,6 +573,15 @@
|
||||
wavePathPeaks = peaks;
|
||||
wavePathWidth = W;
|
||||
wavePathHeight = H;
|
||||
// A NEW complete waveform arrives on the morph — from the dots,
|
||||
// from the swell mid-sweep, from the previous track's shape, or
|
||||
// from the progressive peaks the final stream decode replaces.
|
||||
// Only on a peaks swap at a matching bar count: a pure resize
|
||||
// re-lays the same shape and stays instant.
|
||||
if (peaksSwapped && lastDrawnHeights?.length === count) {
|
||||
morphFrom = [...lastDrawnHeights];
|
||||
morphStart = now;
|
||||
}
|
||||
}
|
||||
|
||||
const playFrac = player.duration
|
||||
@@ -558,6 +589,42 @@
|
||||
: 0;
|
||||
const playX = playFrac * W;
|
||||
const playedBars = playFrac * waveCount;
|
||||
|
||||
// Mid-arrival: every bar eases from where it was to where the new
|
||||
// waveform puts it, all together, with the full fade colours live.
|
||||
if (morphFrom) {
|
||||
const t = (now - morphStart) / ARRIVE_MS;
|
||||
if (t >= 1) {
|
||||
morphFrom = null;
|
||||
} else {
|
||||
const k = 1 - Math.pow(1 - t, 3);
|
||||
const full = Math.floor(playedBars);
|
||||
const washing = hoverX > playX;
|
||||
const washEndBar = washing ? Math.floor(hoverX / BAR_PITCH) : -1;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
const heights = new Array(waveCount);
|
||||
for (let i = 0; i < waveCount; i++) {
|
||||
const from = morphFrom[i] ?? 2;
|
||||
const h = from + (waveHeights[i] - from) * k;
|
||||
heights[i] = h;
|
||||
paintBar(ctx, i * BAR_PITCH, mid, h, barColor(i, full, playedBars, washing, washEndBar));
|
||||
}
|
||||
lastDrawnHeights = heights;
|
||||
if (hoverX >= 0) {
|
||||
const LINE_H = 18;
|
||||
ctx.fillStyle = theme.accentHover;
|
||||
ctx.fillRect(hoverX - 0.5, (H - LINE_H) / 2, 1, LINE_H);
|
||||
}
|
||||
lastPlayedBars = playedBars;
|
||||
lastHoverDrawn = hoverX;
|
||||
lastAccent = theme.accent;
|
||||
lastAccentHover = theme.accentHover;
|
||||
lastDuration = player.duration;
|
||||
scheduleAnim(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const visualsChanged =
|
||||
geometryChanged ||
|
||||
hoverX !== lastHoverDrawn ||
|
||||
@@ -623,6 +690,7 @@
|
||||
ctx.fillRect(hoverX - 0.5, (H - LINE_H) / 2, 1, LINE_H);
|
||||
}
|
||||
|
||||
lastDrawnHeights = waveHeights;
|
||||
lastPlayedBars = playedBars;
|
||||
lastHoverDrawn = hoverX;
|
||||
lastAccent = theme.accent;
|
||||
|
||||
Reference in new issue
Block a user