mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +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,186 @@
|
||||
import type { SeekbarStyle } from '../store/authStoreTypes';
|
||||
|
||||
export function fmt(s: number): string {
|
||||
if (!s || isNaN(s)) return '0:00';
|
||||
return `${Math.floor(s / 60)}:${Math.floor(s % 60).toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export const BAR_COUNT = 500;
|
||||
/** Stored waveform bins per track (matches backend `bin_count` / PCM bins). */
|
||||
export const WAVE_BIN_COUNT = 500;
|
||||
/** `0.7 * mean + 0.3 * max` in normalized 0..1 space (v4 cache: first half = peak, second = mean-abs). */
|
||||
export const WAVE_MIX_MEAN = 0.7;
|
||||
export const WAVE_MIX_MAX = 0.3;
|
||||
export const SEG_COUNT = 60;
|
||||
export const FLAT_WAVE_NORM = 0.06;
|
||||
export const WAVE_MORPH_MS = 1000;
|
||||
export const STATIC_REDRAW_MIN_MS = 90;
|
||||
export const STATIC_REDRAW_FORCE_MS = 220;
|
||||
export const INTERPOLATION_PAINT_MIN_MS = 80;
|
||||
|
||||
export type Particle = {
|
||||
x: number; y: number;
|
||||
vx: number; vy: number;
|
||||
life: number; maxLife: number;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type AnimState = {
|
||||
particles: Particle[];
|
||||
time: number;
|
||||
lastProgress: number;
|
||||
angle: number;
|
||||
};
|
||||
|
||||
export function makeAnimState(): AnimState {
|
||||
return { particles: [], time: 0, lastProgress: 0, angle: 0 };
|
||||
}
|
||||
|
||||
export const ANIMATED_STYLES = new Set<SeekbarStyle>(['particletrail', 'pulsewave', 'liquidfill', 'retrotape']);
|
||||
|
||||
export type SeekbarColors = {
|
||||
played: string;
|
||||
buffered: string;
|
||||
unplayed: string;
|
||||
};
|
||||
|
||||
let cachedColors: SeekbarColors | null = null;
|
||||
let cachedColorsKey = '';
|
||||
|
||||
export function invalidateColorCache() {
|
||||
cachedColors = null;
|
||||
}
|
||||
|
||||
export function getColors(): SeekbarColors {
|
||||
const root = document.documentElement;
|
||||
const style = root.style;
|
||||
const key = [
|
||||
root.getAttribute('data-theme') ?? '',
|
||||
style.getPropertyValue('--accent'),
|
||||
style.getPropertyValue('--waveform-played'),
|
||||
style.getPropertyValue('--waveform-buffered'),
|
||||
style.getPropertyValue('--waveform-unplayed'),
|
||||
].join('|');
|
||||
if (cachedColors && cachedColorsKey === key) return cachedColors;
|
||||
const s = getComputedStyle(root);
|
||||
cachedColorsKey = key;
|
||||
cachedColors = {
|
||||
played: s.getPropertyValue('--waveform-played').trim() || s.getPropertyValue('--accent').trim() || '#cba6f7',
|
||||
buffered: s.getPropertyValue('--waveform-buffered').trim() || s.getPropertyValue('--ctp-overlay0').trim() || '#6c7086',
|
||||
unplayed: s.getPropertyValue('--waveform-unplayed').trim() || s.getPropertyValue('--ctp-surface1').trim() || '#313244',
|
||||
};
|
||||
return cachedColors;
|
||||
}
|
||||
|
||||
export function setupCanvas(
|
||||
canvas: HTMLCanvasElement,
|
||||
): { ctx: CanvasRenderingContext2D; w: number; h: number } | null {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return null;
|
||||
const w = canvas.clientWidth || canvas.getBoundingClientRect().width;
|
||||
const h = canvas.clientHeight || canvas.getBoundingClientRect().height;
|
||||
if (w === 0 || h === 0) return null;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const pw = Math.round(w * dpr);
|
||||
const ph = Math.round(h * dpr);
|
||||
if (canvas.width !== pw || canvas.height !== ph) {
|
||||
canvas.width = pw;
|
||||
canvas.height = ph;
|
||||
}
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
return { ctx, w, h };
|
||||
}
|
||||
|
||||
export function setShadowBlur(ctx: CanvasRenderingContext2D, blur: number) {
|
||||
ctx.shadowBlur = Math.max(0, blur);
|
||||
}
|
||||
|
||||
function hashStr(str: string): number {
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h = (h ^ str.charCodeAt(i)) >>> 0;
|
||||
h = Math.imul(h, 0x01000193) >>> 0;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
export function makeHeights(trackId: string): Float32Array {
|
||||
let s = hashStr(trackId);
|
||||
const h = new Float32Array(BAR_COUNT);
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
||||
h[i] = s / 0xffffffff;
|
||||
}
|
||||
for (let pass = 0; pass < 5; pass++) {
|
||||
for (let i = 1; i < BAR_COUNT - 1; i++) {
|
||||
h[i] = h[i - 1] * 0.25 + h[i] * 0.5 + h[i + 1] * 0.25;
|
||||
}
|
||||
}
|
||||
let max = 0;
|
||||
for (let i = 0; i < BAR_COUNT; i++) if (h[i] > max) max = h[i];
|
||||
if (max > 0) for (let i = 0; i < BAR_COUNT; i++) h[i] = 0.12 + (h[i] / max) * 0.88;
|
||||
return h;
|
||||
}
|
||||
|
||||
export function makeFlatWaveHeights(): Float32Array {
|
||||
const h = new Float32Array(BAR_COUNT);
|
||||
h.fill(FLAT_WAVE_NORM);
|
||||
return h;
|
||||
}
|
||||
|
||||
export function easeOutCubic(t: number): number {
|
||||
const x = Math.max(0, Math.min(1, t));
|
||||
return 1 - Math.pow(1 - x, 3);
|
||||
}
|
||||
|
||||
export function binsToHeights(src: number[]): Float32Array {
|
||||
const h = new Float32Array(BAR_COUNT);
|
||||
const n = src.length;
|
||||
if (n === WAVE_BIN_COUNT * 2) {
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
const idx = Math.min(WAVE_BIN_COUNT - 1, Math.floor((i / BAR_COUNT) * WAVE_BIN_COUNT));
|
||||
const maxNorm = Number(src[idx]) / 255;
|
||||
const meanNorm = Number(src[WAVE_BIN_COUNT + idx]) / 255;
|
||||
const v = WAVE_MIX_MEAN * meanNorm + WAVE_MIX_MAX * maxNorm;
|
||||
h[i] = Math.max(0.08, Math.min(1, v));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
if (n === WAVE_BIN_COUNT) {
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
const idx = Math.min(WAVE_BIN_COUNT - 1, Math.floor((i / BAR_COUNT) * WAVE_BIN_COUNT));
|
||||
const v = src[idx];
|
||||
h[i] = Math.max(0.08, Math.min(1, (Number(v) / 255)));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
for (let i = 0; i < BAR_COUNT; i++) {
|
||||
const idx = Math.min(n - 1, Math.floor((i / BAR_COUNT) * n));
|
||||
const v = src[idx];
|
||||
h[i] = Math.max(0.08, Math.min(1, (Number(v) / 255)));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
export function heightsNearlyEqual(a: Float32Array, b: Float32Array, eps: number): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (Math.abs(a[i] - b[i]) > eps) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function waveformBarThickness(logicalH: number, norm: number): number {
|
||||
const safeNorm = Math.max(FLAT_WAVE_NORM, norm);
|
||||
return Math.max(1, safeNorm * logicalH);
|
||||
}
|
||||
|
||||
export function quantizeProgressByBars(progress: number): number {
|
||||
const clamped = Math.max(0, Math.min(1, progress));
|
||||
return Math.max(0, Math.min(1, Math.floor(clamped * BAR_COUNT) / BAR_COUNT));
|
||||
}
|
||||
|
||||
export function isBarQuantizedSeekStyle(style: SeekbarStyle): boolean {
|
||||
return style === 'truewave' || style === 'pseudowave';
|
||||
}
|
||||
Reference in New Issue
Block a user