mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
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:
committed by
GitHub
parent
b8a9fe860e
commit
dc5c64a109
@@ -0,0 +1,324 @@
|
||||
import { AnimState, BAR_COUNT, getColors, setShadowBlur, setupCanvas } from './waveformSeekHelpers';
|
||||
|
||||
export function drawPulseWave(
|
||||
canvas: HTMLCanvasElement,
|
||||
progress: number,
|
||||
buffered: number,
|
||||
animState: AnimState,
|
||||
) {
|
||||
const r = setupCanvas(canvas);
|
||||
if (!r) return;
|
||||
const { ctx, w, h } = r;
|
||||
const { played, buffered: buffCol, unplayed } = getColors();
|
||||
const cy = h / 2;
|
||||
const px = progress * w;
|
||||
const t = animState.time;
|
||||
|
||||
// Base line
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fillStyle = unplayed;
|
||||
ctx.fillRect(0, cy - 1, w, 2);
|
||||
|
||||
if (buffered > 0) {
|
||||
ctx.globalAlpha = 0.45;
|
||||
ctx.fillStyle = buffCol;
|
||||
ctx.fillRect(0, cy - 1, buffered * w, 2);
|
||||
}
|
||||
|
||||
// Animated pulse centered at playhead
|
||||
const pulseR = Math.min(38, w * 0.13);
|
||||
const amp = Math.min(h * 0.42, 5.5);
|
||||
const sigma = pulseR * 0.42;
|
||||
const startX = Math.max(0, px - pulseR);
|
||||
const endX = Math.min(w, px + pulseR);
|
||||
|
||||
// Flat played line up to where the wave envelope starts
|
||||
if (progress > 0) {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = played;
|
||||
ctx.shadowColor = played;
|
||||
setShadowBlur(ctx, 3);
|
||||
ctx.fillRect(0, cy - 1, startX, 2);
|
||||
setShadowBlur(ctx, 0);
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.strokeStyle = played;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.shadowColor = played;
|
||||
setShadowBlur(ctx, 7);
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, cy);
|
||||
for (let x = startX; x <= endX; x += 0.75) {
|
||||
const dx = x - px;
|
||||
const env = Math.exp(-(dx * dx) / (2 * sigma * sigma));
|
||||
const wave = env * amp * Math.sin(dx * 0.28 - t * 18);
|
||||
ctx.lineTo(x, cy - wave);
|
||||
}
|
||||
ctx.stroke();
|
||||
setShadowBlur(ctx, 0);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
export function drawParticleTrail(
|
||||
canvas: HTMLCanvasElement,
|
||||
progress: number,
|
||||
buffered: number,
|
||||
animState: AnimState,
|
||||
) {
|
||||
const r = setupCanvas(canvas);
|
||||
if (!r) return;
|
||||
const { ctx, w, h } = r;
|
||||
const { played, buffered: buffCol, unplayed } = getColors();
|
||||
const cy = h / 2;
|
||||
const px = progress * w;
|
||||
|
||||
// Spawn particles at playhead based on movement
|
||||
const prevPx = animState.lastProgress * w;
|
||||
const moved = Math.abs(px - prevPx);
|
||||
const spawnN = Math.min(5, 1 + Math.floor(moved * 1.5));
|
||||
for (let i = 0; i < spawnN; i++) {
|
||||
animState.particles.push({
|
||||
x: px + (Math.random() - 0.5) * 3,
|
||||
y: cy + (Math.random() - 0.5) * (h * 0.55),
|
||||
vx: -(Math.random() * 1.0 + 0.3),
|
||||
vy: (Math.random() - 0.5) * 0.6,
|
||||
life: 1,
|
||||
maxLife: 25 + Math.random() * 35,
|
||||
size: Math.random() * 1.8 + 0.8,
|
||||
});
|
||||
}
|
||||
animState.lastProgress = progress;
|
||||
|
||||
// Update + cull
|
||||
for (const p of animState.particles) {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.vy *= 0.97;
|
||||
p.life -= 1 / p.maxLife;
|
||||
}
|
||||
animState.particles = animState.particles.filter(p => p.life > 0);
|
||||
if (animState.particles.length > 180) {
|
||||
animState.particles = animState.particles.slice(-180);
|
||||
}
|
||||
|
||||
// Background line
|
||||
ctx.globalAlpha = 0.28;
|
||||
ctx.fillStyle = unplayed;
|
||||
ctx.fillRect(0, cy - 1, w, 2);
|
||||
|
||||
if (buffered > 0) {
|
||||
ctx.globalAlpha = 0.45;
|
||||
ctx.fillStyle = buffCol;
|
||||
ctx.fillRect(0, cy - 1, buffered * w, 2);
|
||||
}
|
||||
|
||||
// Played line
|
||||
if (progress > 0) {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = played;
|
||||
ctx.shadowColor = played;
|
||||
setShadowBlur(ctx, 4);
|
||||
ctx.fillRect(0, cy - 1, px, 2);
|
||||
setShadowBlur(ctx, 0);
|
||||
}
|
||||
|
||||
// Particles
|
||||
ctx.shadowColor = played;
|
||||
for (const p of animState.particles) {
|
||||
ctx.globalAlpha = p.life * 0.85;
|
||||
setShadowBlur(ctx, 5);
|
||||
ctx.fillStyle = played;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
setShadowBlur(ctx, 0);
|
||||
|
||||
// Playhead dot
|
||||
if (progress > 0) {
|
||||
const dx = Math.max(5, Math.min(w - 5, px));
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = played;
|
||||
ctx.shadowColor = played;
|
||||
setShadowBlur(ctx, 10);
|
||||
ctx.beginPath();
|
||||
ctx.arc(dx, cy, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
setShadowBlur(ctx, 0);
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
export function drawLiquidFill(
|
||||
canvas: HTMLCanvasElement,
|
||||
progress: number,
|
||||
buffered: number,
|
||||
animState: AnimState,
|
||||
) {
|
||||
const r = setupCanvas(canvas);
|
||||
if (!r) return;
|
||||
const { ctx, w, h } = r;
|
||||
const { played, buffered: buffCol, unplayed } = getColors();
|
||||
const t = animState.time;
|
||||
|
||||
const tubeH = Math.min(13, Math.max(6, h * 0.62));
|
||||
const tubeR = tubeH / 2;
|
||||
const y0 = (h - tubeH) / 2;
|
||||
const y1 = y0 + tubeH;
|
||||
|
||||
// Glass tube background
|
||||
ctx.globalAlpha = 0.18;
|
||||
ctx.fillStyle = unplayed;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, y0, w, tubeH, tubeR);
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.strokeStyle = unplayed;
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.stroke();
|
||||
|
||||
if (buffered > 0) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, y0, w, tubeH, tubeR);
|
||||
ctx.clip();
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fillStyle = buffCol;
|
||||
ctx.fillRect(0, y0, buffered * w, tubeH);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (progress > 0) {
|
||||
const px = progress * w;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, y0, w, tubeH, tubeR);
|
||||
ctx.clip();
|
||||
|
||||
// Liquid body with animated wave on top surface
|
||||
const surfaceY = y0 + tubeH * 0.22; // liquid surface ~78% full
|
||||
const waveAmp = Math.min(2.0, tubeH * 0.14);
|
||||
const waveFreq = 0.09;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-1, y1 + 1);
|
||||
ctx.lineTo(-1, surfaceY);
|
||||
|
||||
for (let x = 0; x <= px + 1; x += 1) {
|
||||
const wave = waveAmp * Math.sin(x * waveFreq + t * 2.2);
|
||||
ctx.lineTo(x, surfaceY + wave);
|
||||
}
|
||||
ctx.lineTo(px + 1, y1 + 1);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = played;
|
||||
ctx.shadowColor = played;
|
||||
setShadowBlur(ctx, 9);
|
||||
ctx.fill();
|
||||
setShadowBlur(ctx, 0);
|
||||
|
||||
// Glass highlight on top
|
||||
const hl = ctx.createLinearGradient(0, y0, 0, y0 + tubeH * 0.45);
|
||||
hl.addColorStop(0, 'rgba(255,255,255,0.28)');
|
||||
hl.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
ctx.globalAlpha = 0.6;
|
||||
ctx.fillStyle = hl;
|
||||
ctx.fillRect(0, y0, px, tubeH * 0.45);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Tube outline (on top)
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.strokeStyle = unplayed;
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(0, y0, w, tubeH, tubeR);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
export function drawRetroTape(
|
||||
canvas: HTMLCanvasElement,
|
||||
progress: number,
|
||||
buffered: number,
|
||||
animState: AnimState,
|
||||
) {
|
||||
const r = setupCanvas(canvas);
|
||||
if (!r) return;
|
||||
const { ctx, w, h } = r;
|
||||
const { played, buffered: buffCol, unplayed } = getColors();
|
||||
const cy = h / 2;
|
||||
|
||||
animState.angle += 0.055;
|
||||
|
||||
const reelR = Math.min(h / 2 - 0.5, 9);
|
||||
// Map progress to a center x that keeps the reel fully within the canvas
|
||||
const px = reelR + (w - 2 * reelR) * progress;
|
||||
|
||||
// Background track
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fillStyle = unplayed;
|
||||
ctx.fillRect(0, cy - 1, w, 2);
|
||||
|
||||
if (buffered > 0) {
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.fillStyle = buffCol;
|
||||
ctx.fillRect(0, cy - 1, buffered * w, 2);
|
||||
}
|
||||
|
||||
// Played portion — up to the left edge of the reel
|
||||
if (progress > 0) {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = played;
|
||||
ctx.shadowColor = played;
|
||||
setShadowBlur(ctx, 4);
|
||||
ctx.fillRect(0, cy - 1, px - reelR, 2);
|
||||
setShadowBlur(ctx, 0);
|
||||
}
|
||||
|
||||
// Spinning reel at playhead
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.strokeStyle = played;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.shadowColor = played;
|
||||
setShadowBlur(ctx, 7);
|
||||
|
||||
// Outer ring
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, cy, reelR, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
setShadowBlur(ctx, 0);
|
||||
|
||||
// Hub
|
||||
const hubR = Math.max(1.5, reelR * 0.28);
|
||||
ctx.fillStyle = played;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, cy, hubR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Spokes
|
||||
if (reelR > hubR + 2) {
|
||||
ctx.lineWidth = 0.9;
|
||||
ctx.strokeStyle = played;
|
||||
for (let s = 0; s < 3; s++) {
|
||||
const a = animState.angle + (s * Math.PI * 2) / 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + Math.cos(a) * (hubR + 0.5), cy + Math.sin(a) * (hubR + 0.5));
|
||||
ctx.lineTo(px + Math.cos(a) * (reelR - 0.5), cy + Math.sin(a) * (reelR - 0.5));
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
setShadowBlur(ctx, 0);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// ── dispatcher ────────────────────────────────────────────────────────────────
|
||||
Reference in New Issue
Block a user