/** * 频谱渲染器接口 * 所有频谱特效均需实现此接口 */ export interface SpectrumRenderer { /** * 渲染一帧频谱动画 * @param ctx Canvas 2D 渲染上下文 * @param bands 频段幅值数组 (0.0 ~ 1.0) * @param energy 总体能量值 (0.0 ~ 1.0) * @param width Canvas 宽度 * @param height Canvas 高度 * @param brandColor 主题品牌色 (hex string, e.g. '#FF0050') */ render(ctx: CanvasRenderingContext2D, bands: number[], energy: number, width: number, height: number, brandColor: string, options?: SpectrumRenderOptions): void /** * 重置渲染器状态 */ reset(): void } export interface SpectrumRenderOptions { complexityScale: number } export interface SpectrumPalette { primaryHue: number warmHue: number coolHue: number complementaryHue: number accentHue: number } /** * 规范化 hex 颜色字符串,异常时回退到默认值 */ export function normalizeHexColor(color: string, fallback: string = '#FF0050'): string { if (!color || color.length < 7 || color[0] !== '#') { return fallback } const hexBody: string = color.substring(1, 7) const valid: boolean = /^[0-9a-fA-F]{6}$/.test(hexBody) return valid ? '#' + hexBody : fallback } /** * 基于品牌色构建频谱调色板(HSL 色相偏移) */ export function buildSpectrumPalette(brandColor: string): SpectrumPalette { const baseColor: string = normalizeHexColor(brandColor) const baseHue: number = hexToHue(baseColor) return { primaryHue: baseHue, warmHue: (baseHue - 28 + 360) % 360, coolHue: (baseHue + 28) % 360, complementaryHue: (baseHue + 180) % 360, accentHue: (baseHue + 55) % 360 } } /** * 生成 rgba 字符串,便于 Canvas 透明度控制 */ export function rgbaFromHex(hex: string, alpha: number): string { const safeHex: string = normalizeHexColor(hex) const safeAlpha: number = Math.max(0, Math.min(1, alpha)) const r: number = parseInt(safeHex.substring(1, 3), 16) const g: number = parseInt(safeHex.substring(3, 5), 16) const b: number = parseInt(safeHex.substring(5, 7), 16) return `rgba(${r}, ${g}, ${b}, ${safeAlpha})` } /** * HSL 转 RGB hex 字符串 */ export function hslToHex(h: number, s: number, l: number): string { const hNorm: number = ((h % 360) + 360) % 360 const sNorm: number = Math.max(0, Math.min(1, s)) const lNorm: number = Math.max(0, Math.min(1, l)) const c: number = (1 - Math.abs(2 * lNorm - 1)) * sNorm const x: number = c * (1 - Math.abs((hNorm / 60) % 2 - 1)) const m: number = lNorm - c / 2 let r: number = 0 let g: number = 0 let b: number = 0 if (hNorm < 60) { r = c; g = x; b = 0 } else if (hNorm < 120) { r = x; g = c; b = 0 } else if (hNorm < 180) { r = 0; g = c; b = x } else if (hNorm < 240) { r = 0; g = x; b = c } else if (hNorm < 300) { r = x; g = 0; b = c } else { r = c; g = 0; b = x } const ri: number = Math.round((r + m) * 255) const gi: number = Math.round((g + m) * 255) const bi: number = Math.round((b + m) * 255) const toHex = (v: number): string => { const hex: string = v.toString(16) return hex.length < 2 ? '0' + hex : hex } return '#' + toHex(ri) + toHex(gi) + toHex(bi) } /** * 从 hex 字符串解析 HSL 色相 */ export function hexToHue(hex: string): number { let r: number = 0 let g: number = 0 let b: number = 0 if (hex.length >= 7) { r = parseInt(hex.substring(1, 3), 16) / 255 g = parseInt(hex.substring(3, 5), 16) / 255 b = parseInt(hex.substring(5, 7), 16) / 255 } const max: number = Math.max(r, g, b) const min: number = Math.min(r, g, b) const d: number = max - min let h: number = 0 if (d !== 0) { if (max === r) h = ((g - b) / d) % 6 else if (max === g) h = (b - r) / d + 2 else h = (r - g) / d + 4 h *= 60 if (h < 0) h += 360 } return h }