mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
dc5c64a109
* 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
325 lines
8.2 KiB
TypeScript
325 lines
8.2 KiB
TypeScript
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 ────────────────────────────────────────────────────────────────
|