import { useEffect, useRef, useCallback } from "react"; import styles from "./voice-print.module.scss"; interface VoicePrintProps { frequencies?: Uint8Array; isActive?: boolean; } export function VoicePrint({ frequencies, isActive }: VoicePrintProps) { // Canvas引用,用于获取绘图上下文 const canvasRef = useRef(null); // 存储历史频率数据,用于平滑处理 const historyRef = useRef([]); // 控制保留的历史数据帧数,影响平滑度 const historyLengthRef = useRef(10); // 存储动画帧ID,用于清理 const animationFrameRef = useRef(); /** * 更新频率历史数据 * 使用FIFO队列维护固定长度的历史记录 */ const updateHistory = useCallback((freqArray: number[]) => { historyRef.current.push(freqArray); if (historyRef.current.length > historyLengthRef.current) { historyRef.current.shift(); } }, []); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; /** * 处理高DPI屏幕显示 * 根据设备像素比例调整canvas实际渲染分辨率 */ const dpr = window.devicePixelRatio || 1; canvas.width = canvas.offsetWidth * dpr; canvas.height = canvas.offsetHeight * dpr; ctx.scale(dpr, dpr); /** * 主要绘制函数 * 使用requestAnimationFrame实现平滑动画 * 包含以下步骤: * 1. 清空画布 * 2. 更新历史数据 * 3. 计算波形点 * 4. 绘制上下对称的声纹 */ const draw = () => { // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); if (!frequencies || !isActive) { historyRef.current = []; return; } const freqArray = Array.from(frequencies); updateHistory(freqArray); // 绘制声纹 const points: [number, number][] = []; const centerY = canvas.height / 2; const width = canvas.width; const sliceWidth = width / (frequencies.length - 1); // 绘制主波形 ctx.beginPath(); ctx.moveTo(0, centerY); /** * 声纹绘制算法: * 1. 使用历史数据平均值实现平滑过渡 * 2. 通过正弦函数添加自然波动 * 3. 使用贝塞尔曲线连接点,使曲线更平滑 * 4. 绘制对称部分形成完整声纹 */ for (let i = 0; i < frequencies.length; i++) { const x = i * sliceWidth; let avgFrequency = frequencies[i]; /** * 波形平滑处理: * 1. 收集历史数据中对应位置的频率值 * 2. 计算当前值与历史值的加权平均 * 3. 根据平均值计算实际显示高度 */ if (historyRef.current.length > 0) { const historicalValues = historyRef.current.map((h) => h[i] || 0); avgFrequency = (avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) / (historyRef.current.length + 1); } /** * 波形变换: * 1. 归一化频率值到0-1范围 * 2. 添加时间相关的正弦变换 * 3. 使用贝塞尔曲线平滑连接点 */ const normalized = avgFrequency / 255.0; const height = normalized * (canvas.height / 2); const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002); points.push([x, y]); if (i === 0) { ctx.moveTo(x, y); } else { // 使用贝塞尔曲线使波形更平滑 const prevPoint = points[i - 1]; const midX = (prevPoint[0] + x) / 2; ctx.quadraticCurveTo( prevPoint[0], prevPoint[1], midX, (prevPoint[1] + y) / 2, ); } } // 绘制对称的下半部分 for (let i = points.length - 1; i >= 0; i--) { const [x, y] = points[i]; const symmetricY = centerY - (y - centerY); if (i === points.length - 1) { ctx.lineTo(x, symmetricY); } else { const nextPoint = points[i + 1]; const midX = (nextPoint[0] + x) / 2; ctx.quadraticCurveTo( nextPoint[0], centerY - (nextPoint[1] - centerY), midX, centerY - ((nextPoint[1] + y) / 2 - centerY), ); } } ctx.closePath(); /** * 渐变效果: * 从左到右应用三色渐变,带透明度 * 使用蓝色系配色提升视觉效果 */ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)"); gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)"); gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)"); ctx.fillStyle = gradient; ctx.fill(); animationFrameRef.current = requestAnimationFrame(draw); }; // 启动动画循环 draw(); // 清理函数:在组件卸载时取消动画 return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [frequencies, isActive, updateHistory]); return (
); }