| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import React, { useEffect, useRef, useState } from 'react';
- import './AudioWave.css';
- interface AudioWaveProps {
- isAnimating?: boolean;
- width?: number;
- height?: number;
- }
- export const AudioWave: React.FC<AudioWaveProps> = ({
- isAnimating = false,
- width = 375,
- height = 56,
- }) => {
- const containerRef = useRef<HTMLDivElement>(null);
- const [actualWidth, setActualWidth] = useState(width);
-
- // 监听容器宽度变化
- useEffect(() => {
- const updateWidth = () => {
- if (containerRef.current) {
- setActualWidth(containerRef.current.offsetWidth);
- }
- };
-
- updateWidth();
- window.addEventListener('resize', updateWidth);
- return () => window.removeEventListener('resize', updateWidth);
- }, []);
-
- // 固定竖条尺寸,根据宽度计算需要多少竖条来填满
- const barWidth = 4; // 固定竖条宽度
- const targetBarGap = 2; // 目标竖条间距
- const cornerRadius = 2; // 固定圆角
- const ANIMATION_INTERVAL = 180; // 动画更新间隔(毫秒),值越大动画越慢(从90增加到180,减慢跳动速率)
-
- // 根据实际宽度计算需要多少竖条(向上取整确保填满,并确保是奇数)
- const calculatedBarCount = Math.ceil(actualWidth / (barWidth + targetBarGap));
- const barCount = calculatedBarCount % 2 === 0 ? calculatedBarCount + 1 : calculatedBarCount;
-
- // 反向计算实际间距,确保完全填满宽度
- // 公式: totalWidth = barCount * barWidth + (barCount - 1) * actualGap
- const actualGap = Math.max(0, (actualWidth - barCount * barWidth) / (barCount - 1));
- const barSpacing = barWidth + actualGap;
-
- const STATIC_HEIGHT = Math.max(16, height * 0.35); // 静态时竖条高度(高度的35%,最小16px)
-
- // 竖条高度数组:存储每个竖条的当前高度
- const [barHeights, setBarHeights] = useState<number[]>([]);
-
- const animationRef = useRef<number | null>(null);
- const lastUpdateRef = useRef<number>(0);
- const wavePhaseRef = useRef<number>(0); // 水波纹相位,用于控制波浪传播
-
- // 当 barCount 变化时,调整 barHeights 数组长度
- useEffect(() => {
- setBarHeights((prev) => {
- if (prev.length === barCount) return prev;
-
- // 如果数组太短,补充静态高度
- if (prev.length < barCount) {
- return [...prev, ...Array(barCount - prev.length).fill(STATIC_HEIGHT)];
- }
-
- // 如果数组太长,截断
- return prev.slice(0, barCount);
- });
- }, [barCount, STATIC_HEIGHT]);
- // 生成水波纹效果的高度:使用正弦波函数,形成波浪传播效果
- const generateWaveHeight = (index: number, phase: number): number => {
- // 计算竖条在波浪中的位置(0 到 2π)
- const position = (index / barCount) * Math.PI * 2;
- // 使用正弦波函数,加上相位偏移形成传播效果
- const waveValue = Math.sin(position + phase);
- // 将正弦波值(-1 到 1)映射到高度范围
- const normalizedValue = (waveValue + 1) / 2; // 0 到 1
- // 增加随机性,让水波纹起伏更大(范围从 0.4 到 1.6,增加起伏幅度)
- const randomFactor = 0.4 + Math.random() * 1.2; // 0.4 到 1.6,随机性更大
- // 最低高度为原始值的1/2,最高为原始值的2倍
- const MIN_WAVE_HEIGHT = STATIC_HEIGHT * 0.5; // 最低高度为原始值的1/2
- const MAX_WAVE_HEIGHT = STATIC_HEIGHT * 2; // 最高高度为原始值的2倍
- // 计算高度范围
- const heightRange = MAX_WAVE_HEIGHT - MIN_WAVE_HEIGHT;
- // 计算最终高度:从 MIN_WAVE_HEIGHT 到 MAX_WAVE_HEIGHT,乘以随机因子增加起伏
- const calculatedHeight = MIN_WAVE_HEIGHT + normalizedValue * heightRange * randomFactor;
- // 确保高度在合理范围内
- return Math.max(MIN_WAVE_HEIGHT, Math.min(calculatedHeight, MAX_WAVE_HEIGHT));
- };
- // 更新波形:使用水波纹效果
- const updateWave = () => {
- // 更新相位,形成波浪传播效果(增加相位增量,让波浪传播更快)
- wavePhaseRef.current += 0.6; // 相位增量从 0.3 增加到 0.6,加快波浪传播速度
- if (wavePhaseRef.current > Math.PI * 2) {
- wavePhaseRef.current -= Math.PI * 2; // 保持相位在合理范围内
- }
-
- setBarHeights((prev) => {
- const targetLength = Math.max(1, barCount);
- if (prev.length === 0) {
- return Array.from({ length: targetLength }, () => STATIC_HEIGHT);
- }
- if (prev.length !== targetLength) {
- return Array.from({ length: targetLength }, (_, idx) => prev[idx] ?? STATIC_HEIGHT);
- }
- // 使用水波纹函数生成每个竖条的高度
- return prev.map((_, index) => generateWaveHeight(index, wavePhaseRef.current));
- });
- };
- const resetBarsToStatic = () => {
- // 重置相位
- wavePhaseRef.current = 0;
- setBarHeights((prev) => {
- const targetLength = Math.max(1, barCount);
- const needsResize = prev.length !== targetLength;
- const needsReset =
- prev.length === 0 ||
- prev.some((height) => Math.abs(height - STATIC_HEIGHT) > 0.5);
- if (!needsResize && !needsReset) {
- return prev;
- }
- return Array.from({ length: targetLength }, () => STATIC_HEIGHT);
- });
- };
- // 动画循环
- useEffect(() => {
- if (!isAnimating) {
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current);
- animationRef.current = null;
- }
- resetBarsToStatic();
- return;
- }
- const animate = (timestamp: number) => {
- // 每 ANIMATION_INTERVAL 更新一次(模拟音频采样率)
- if (timestamp - lastUpdateRef.current > ANIMATION_INTERVAL) {
- updateWave();
- lastUpdateRef.current = timestamp;
- }
- animationRef.current = requestAnimationFrame(animate);
- };
- animationRef.current = requestAnimationFrame(animate);
- return () => {
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current);
- animationRef.current = null;
- }
- };
- }, [isAnimating, barCount]);
- // 生成波形条位置(响应式间距)
- const bars = Array.from({ length: barCount }, (_, i) => ({
- x: i * barSpacing, // 使用计算的响应式间距
- }));
- return (
- <div
- ref={containerRef}
- className="audio-wave-container"
- style={{
- width: '100%',
- height: `${height}px`,
- }}
- >
- <svg
- width="100%"
- height={height}
- viewBox={`0 0 ${actualWidth} ${height}`}
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- preserveAspectRatio="none"
- >
- <defs>
- {/* 彩色渐变定义 */}
- <linearGradient id="waveGradient" x1="100%" y1="0%" x2="0%" y2="0%">
- <stop offset="0%" stopColor="#F67B7C" />
- <stop offset="29.64%" stopColor="#FFBEF4" />
- <stop offset="49.41%" stopColor="#A699FE" />
- <stop offset="74.11%" stopColor="#84CCF8" />
- <stop offset="98.81%" stopColor="#FFDD5E" />
- </linearGradient>
-
- {/* mask 定义:响应式竖条 */}
- <mask id="audioMask">
- {bars.map((bar, index) => {
- // 获取当前竖条的高度(从 barHeights 数组)
- // 如果 barHeights[index] 为 undefined 或 NaN,使用 STATIC_HEIGHT 作为默认值
- const currentHeight = barHeights[index] ?? STATIC_HEIGHT;
- const safeHeight = Number.isNaN(currentHeight) || currentHeight <= 0 ? STATIC_HEIGHT : currentHeight;
- // 底部固定在容器底部
- const bottomY = height;
- const topY = bottomY - safeHeight;
-
- return (
- <rect
- key={index}
- x={bar.x}
- y={topY}
- width={barWidth}
- height={safeHeight}
- rx={cornerRadius}
- fill="white"
- className="audio-wave-bar"
- />
- );
- })}
- </mask>
- </defs>
-
- {/* 应用 mask 到渐变背景(只保留竖条部分) */}
- <g mask="url(#audioMask)">
- {/* 彩色渐变背景 */}
- <rect
- x="0"
- y="0"
- width={actualWidth}
- height={height}
- fill="url(#waveGradient)"
- />
- {/* 20% 黑色叠加层 */}
- <rect
- x="0"
- y="0"
- width={actualWidth}
- height={height}
- fill="black"
- fillOpacity="0.2"
- />
- </g>
- </svg>
- </div>
- );
- };
|