| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- 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<HTMLCanvasElement>(null);
- // 存储历史频率数据,用于平滑处理
- const historyRef = useRef<number[][]>([]);
- // 控制保留的历史数据帧数,影响平滑度
- const historyLengthRef = useRef(10);
- // 存储动画帧ID,用于清理
- const animationFrameRef = useRef<number>();
- /**
- * 更新频率历史数据
- * 使用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 (
- <div className={styles["voice-print"]}>
- <canvas ref={canvasRef} />
- </div>
- );
- }
|