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(['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'; }