refactor(waveform-seek): H3 — extract renderers + 2 hooks + SeekbarPreview component (#665)

* refactor(waveform-seek): H3.1 — extract helpers + constants + types

* refactor(waveform-seek): H3.2 — extract drawSeekbar + style renderers to utils/waveformSeekRenderers.ts

* refactor(waveform-seek): H3.3 — split renderers into static + animated

* refactor(waveform-seek): H3.4 — extract SeekbarPreview to WaveformSeekPreview.tsx

* refactor(waveform-seek): H3.5 — extract useWaveformHeights hook + hoist constants

* refactor(waveform-seek): H3.6 — extract useWaveformInterpolation hook
This commit is contained in:
Frank Stellmacher
2026-05-13 22:04:45 +02:00
committed by GitHub
parent b8a9fe860e
commit dc5c64a109
9 changed files with 1183 additions and 1084 deletions
+290
View File
@@ -0,0 +1,290 @@
import {
BAR_COUNT, SEG_COUNT,
getColors, setShadowBlur, setupCanvas, waveformBarThickness,
} from './waveformSeekHelpers';
export function drawWaveform(
canvas: HTMLCanvasElement,
heights: Float32Array | null,
progress: number,
buffered: number,
) {
const r = setupCanvas(canvas);
if (!r) return;
const { ctx, w, h } = r;
const { played, buffered: buffCol, unplayed } = getColors();
const pNorm = Math.max(0, Math.min(1, progress));
const bNorm = Math.max(pNorm, Math.min(1, buffered));
if (!heights) {
// No waveform data yet: flat rail like `drawLineDot`, but do not return early
// before played/buffered — otherwise there is no visible playhead.
const cy = h / 2;
const lh = 2;
const dotR = 5;
ctx.globalAlpha = 0.35;
ctx.fillStyle = unplayed;
ctx.fillRect(0, cy - lh / 2, w, lh);
if (buffered > 0) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = buffCol;
ctx.fillRect(0, cy - lh / 2, Math.min(1, buffered) * w, lh);
}
if (progress > 0) {
ctx.globalAlpha = 1;
ctx.fillStyle = played;
ctx.shadowColor = played;
setShadowBlur(ctx, 5);
ctx.fillRect(0, cy - lh / 2, pNorm * w, lh);
setShadowBlur(ctx, 0);
}
ctx.globalAlpha = 1;
if (w > 0) {
const dx = Math.max(dotR, Math.min(w - dotR, pNorm * w));
ctx.shadowColor = played;
setShadowBlur(ctx, 7);
ctx.beginPath();
ctx.arc(dx, cy, dotR, 0, Math.PI * 2);
ctx.fillStyle = played;
ctx.fill();
setShadowBlur(ctx, 0);
}
ctx.globalAlpha = 1;
return;
}
const x1Of = (i: number) => (i / BAR_COUNT) * w;
const x2Of = (i: number) => ((i + 1) / BAR_COUNT) * w;
ctx.globalAlpha = 0.28;
ctx.fillStyle = unplayed;
for (let i = 0; i < BAR_COUNT; i++) {
if (i / BAR_COUNT < bNorm) continue;
const bh = waveformBarThickness(h, heights[i]);
const x = x1Of(i);
ctx.fillRect(x, (h - bh) / 2, x2Of(i) - x, bh);
}
ctx.globalAlpha = 0.45;
ctx.fillStyle = buffCol;
for (let i = 0; i < BAR_COUNT; i++) {
const frac = i / BAR_COUNT;
if (frac < pNorm || frac >= bNorm) continue;
const bh = waveformBarThickness(h, heights[i]);
const x = x1Of(i);
ctx.fillRect(x, (h - bh) / 2, x2Of(i) - x, bh);
}
if (pNorm > 0) {
ctx.globalAlpha = 1;
ctx.fillStyle = played;
for (let i = 0; i < BAR_COUNT; i++) {
if (i / BAR_COUNT >= pNorm) break;
const bh = waveformBarThickness(h, heights[i]);
const x = x1Of(i);
ctx.fillRect(x, (h - bh) / 2, x2Of(i) - x, bh);
}
}
ctx.globalAlpha = 1;
}
export function drawLineDot(canvas: HTMLCanvasElement, progress: number, buffered: number) {
const r = setupCanvas(canvas);
if (!r) return;
const { ctx, w, h } = r;
const { played, buffered: buffCol, unplayed } = getColors();
const cy = h / 2;
const lh = 2;
const dotR = 5;
ctx.globalAlpha = 0.35;
ctx.fillStyle = unplayed;
ctx.fillRect(0, cy - lh / 2, w, lh);
if (buffered > 0) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = buffCol;
ctx.fillRect(0, cy - lh / 2, buffered * w, lh);
}
ctx.globalAlpha = 1;
ctx.fillStyle = played;
ctx.fillRect(0, cy - lh / 2, progress * w, lh);
const dx = Math.max(dotR, Math.min(w - dotR, progress * w));
ctx.shadowColor = played;
setShadowBlur(ctx, 7);
ctx.beginPath();
ctx.arc(dx, cy, dotR, 0, Math.PI * 2);
ctx.fillStyle = played;
ctx.fill();
setShadowBlur(ctx, 0);
ctx.globalAlpha = 1;
}
export function drawBar(canvas: HTMLCanvasElement, progress: number, buffered: number) {
const r = setupCanvas(canvas);
if (!r) return;
const { ctx, w, h } = r;
const { played, buffered: buffCol, unplayed } = getColors();
const bh = 4;
const rad = bh / 2;
const y = (h - bh) / 2;
ctx.globalAlpha = 0.3;
ctx.fillStyle = unplayed;
ctx.beginPath();
ctx.roundRect(0, y, w, bh, rad);
ctx.fill();
if (buffered > 0) {
ctx.globalAlpha = 0.5;
ctx.fillStyle = buffCol;
ctx.beginPath();
ctx.roundRect(0, y, buffered * w, bh, rad);
ctx.fill();
}
if (progress > 0) {
ctx.globalAlpha = 1;
ctx.fillStyle = played;
ctx.shadowColor = played;
setShadowBlur(ctx, 5);
ctx.beginPath();
ctx.roundRect(0, y, progress * w, bh, rad);
ctx.fill();
setShadowBlur(ctx, 0);
}
ctx.globalAlpha = 1;
}
export function drawThick(canvas: HTMLCanvasElement, progress: number, buffered: number) {
const r = setupCanvas(canvas);
if (!r) return;
const { ctx, w, h } = r;
const { played, buffered: buffCol, unplayed } = getColors();
const bh = Math.min(14, h);
const rad = bh / 2;
const y = (h - bh) / 2;
ctx.globalAlpha = 0.25;
ctx.fillStyle = unplayed;
ctx.beginPath();
ctx.roundRect(0, y, w, bh, rad);
ctx.fill();
if (buffered > 0) {
ctx.globalAlpha = 0.45;
ctx.fillStyle = buffCol;
ctx.beginPath();
ctx.roundRect(0, y, buffered * w, bh, rad);
ctx.fill();
}
if (progress > 0) {
ctx.globalAlpha = 1;
ctx.fillStyle = played;
ctx.shadowColor = played;
setShadowBlur(ctx, 10);
ctx.beginPath();
ctx.roundRect(0, y, progress * w, bh, rad);
ctx.fill();
setShadowBlur(ctx, 0);
}
ctx.globalAlpha = 1;
}
export function drawSegmented(canvas: HTMLCanvasElement, progress: number, buffered: number) {
const r = setupCanvas(canvas);
if (!r) return;
const { ctx, w, h } = r;
const { played, buffered: buffCol, unplayed } = getColors();
const gap = 2;
const segW = (w - gap * (SEG_COUNT - 1)) / SEG_COUNT;
const segH = h * 0.65;
const y = (h - segH) / 2;
const playedIdx = Math.floor(progress * SEG_COUNT);
for (let i = 0; i < SEG_COUNT; i++) {
const frac = i / SEG_COUNT;
const x = i * (segW + gap);
setShadowBlur(ctx, 0);
if (frac < progress) {
ctx.globalAlpha = 1;
ctx.fillStyle = played;
if (i === playedIdx - 1) {
ctx.shadowColor = played;
setShadowBlur(ctx, 5);
}
} else if (frac < buffered) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = buffCol;
} else {
ctx.globalAlpha = 0.28;
ctx.fillStyle = unplayed;
}
ctx.beginPath();
ctx.roundRect(x, y, Math.max(1, segW), segH, 1);
ctx.fill();
}
setShadowBlur(ctx, 0);
ctx.globalAlpha = 1;
}
// ── new styles ────────────────────────────────────────────────────────────────
export function drawNeon(canvas: HTMLCanvasElement, progress: number, buffered: number) {
const r = setupCanvas(canvas);
if (!r) return;
const { ctx, w, h } = r;
const { played, unplayed } = getColors();
const cy = h / 2;
// Ghost track — barely visible
ctx.globalAlpha = 0.07;
ctx.fillStyle = unplayed;
ctx.fillRect(0, cy - 1, w, 2);
if (buffered > 0) {
ctx.globalAlpha = 0.12;
ctx.fillStyle = unplayed;
ctx.fillRect(0, cy - 1, buffered * w, 2);
}
if (progress <= 0) return;
const px = progress * w;
// Wide outer glow
ctx.globalAlpha = 0.18;
ctx.fillStyle = played;
ctx.shadowColor = played;
setShadowBlur(ctx, 22);
ctx.fillRect(0, cy - 5, px, 10);
// Mid glow
ctx.globalAlpha = 0.45;
setShadowBlur(ctx, 12);
ctx.fillRect(0, cy - 2.5, px, 5);
// Inner glow
ctx.globalAlpha = 0.85;
setShadowBlur(ctx, 5);
ctx.fillRect(0, cy - 1.5, px, 3);
// Bright white core
ctx.globalAlpha = 1;
ctx.fillStyle = '#ffffff';
ctx.shadowColor = played;
setShadowBlur(ctx, 4);
ctx.fillRect(0, cy - 0.75, px, 1.5);
// End-cap flare
setShadowBlur(ctx, 16);
ctx.beginPath();
ctx.arc(px, cy, 2.5, 0, Math.PI * 2);
ctx.fillStyle = '#ffffff';
ctx.fill();
setShadowBlur(ctx, 0);
ctx.globalAlpha = 1;
}