AudioWave.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import React, { useEffect, useRef, useState } from 'react';
  2. import './AudioWave.css';
  3. interface AudioWaveProps {
  4. isAnimating?: boolean;
  5. width?: number;
  6. height?: number;
  7. }
  8. export const AudioWave: React.FC<AudioWaveProps> = ({
  9. isAnimating = false,
  10. width = 375,
  11. height = 56,
  12. }) => {
  13. const containerRef = useRef<HTMLDivElement>(null);
  14. const [actualWidth, setActualWidth] = useState(width);
  15. // 监听容器宽度变化
  16. useEffect(() => {
  17. const updateWidth = () => {
  18. if (containerRef.current) {
  19. setActualWidth(containerRef.current.offsetWidth);
  20. }
  21. };
  22. updateWidth();
  23. window.addEventListener('resize', updateWidth);
  24. return () => window.removeEventListener('resize', updateWidth);
  25. }, []);
  26. // 固定竖条尺寸,根据宽度计算需要多少竖条来填满
  27. const barWidth = 4; // 固定竖条宽度
  28. const targetBarGap = 2; // 目标竖条间距
  29. const cornerRadius = 2; // 固定圆角
  30. const ANIMATION_INTERVAL = 180; // 动画更新间隔(毫秒),值越大动画越慢(从90增加到180,减慢跳动速率)
  31. // 根据实际宽度计算需要多少竖条(向上取整确保填满,并确保是奇数)
  32. const calculatedBarCount = Math.ceil(actualWidth / (barWidth + targetBarGap));
  33. const barCount = calculatedBarCount % 2 === 0 ? calculatedBarCount + 1 : calculatedBarCount;
  34. // 反向计算实际间距,确保完全填满宽度
  35. // 公式: totalWidth = barCount * barWidth + (barCount - 1) * actualGap
  36. const actualGap = Math.max(0, (actualWidth - barCount * barWidth) / (barCount - 1));
  37. const barSpacing = barWidth + actualGap;
  38. const STATIC_HEIGHT = Math.max(16, height * 0.35); // 静态时竖条高度(高度的35%,最小16px)
  39. // 竖条高度数组:存储每个竖条的当前高度
  40. const [barHeights, setBarHeights] = useState<number[]>([]);
  41. const animationRef = useRef<number | null>(null);
  42. const lastUpdateRef = useRef<number>(0);
  43. const wavePhaseRef = useRef<number>(0); // 水波纹相位,用于控制波浪传播
  44. // 当 barCount 变化时,调整 barHeights 数组长度
  45. useEffect(() => {
  46. setBarHeights((prev) => {
  47. if (prev.length === barCount) return prev;
  48. // 如果数组太短,补充静态高度
  49. if (prev.length < barCount) {
  50. return [...prev, ...Array(barCount - prev.length).fill(STATIC_HEIGHT)];
  51. }
  52. // 如果数组太长,截断
  53. return prev.slice(0, barCount);
  54. });
  55. }, [barCount, STATIC_HEIGHT]);
  56. // 生成水波纹效果的高度:使用正弦波函数,形成波浪传播效果
  57. const generateWaveHeight = (index: number, phase: number): number => {
  58. // 计算竖条在波浪中的位置(0 到 2π)
  59. const position = (index / barCount) * Math.PI * 2;
  60. // 使用正弦波函数,加上相位偏移形成传播效果
  61. const waveValue = Math.sin(position + phase);
  62. // 将正弦波值(-1 到 1)映射到高度范围
  63. const normalizedValue = (waveValue + 1) / 2; // 0 到 1
  64. // 增加随机性,让水波纹起伏更大(范围从 0.4 到 1.6,增加起伏幅度)
  65. const randomFactor = 0.4 + Math.random() * 1.2; // 0.4 到 1.6,随机性更大
  66. // 最低高度为原始值的1/2,最高为原始值的2倍
  67. const MIN_WAVE_HEIGHT = STATIC_HEIGHT * 0.5; // 最低高度为原始值的1/2
  68. const MAX_WAVE_HEIGHT = STATIC_HEIGHT * 2; // 最高高度为原始值的2倍
  69. // 计算高度范围
  70. const heightRange = MAX_WAVE_HEIGHT - MIN_WAVE_HEIGHT;
  71. // 计算最终高度:从 MIN_WAVE_HEIGHT 到 MAX_WAVE_HEIGHT,乘以随机因子增加起伏
  72. const calculatedHeight = MIN_WAVE_HEIGHT + normalizedValue * heightRange * randomFactor;
  73. // 确保高度在合理范围内
  74. return Math.max(MIN_WAVE_HEIGHT, Math.min(calculatedHeight, MAX_WAVE_HEIGHT));
  75. };
  76. // 更新波形:使用水波纹效果
  77. const updateWave = () => {
  78. // 更新相位,形成波浪传播效果(增加相位增量,让波浪传播更快)
  79. wavePhaseRef.current += 0.6; // 相位增量从 0.3 增加到 0.6,加快波浪传播速度
  80. if (wavePhaseRef.current > Math.PI * 2) {
  81. wavePhaseRef.current -= Math.PI * 2; // 保持相位在合理范围内
  82. }
  83. setBarHeights((prev) => {
  84. const targetLength = Math.max(1, barCount);
  85. if (prev.length === 0) {
  86. return Array.from({ length: targetLength }, () => STATIC_HEIGHT);
  87. }
  88. if (prev.length !== targetLength) {
  89. return Array.from({ length: targetLength }, (_, idx) => prev[idx] ?? STATIC_HEIGHT);
  90. }
  91. // 使用水波纹函数生成每个竖条的高度
  92. return prev.map((_, index) => generateWaveHeight(index, wavePhaseRef.current));
  93. });
  94. };
  95. const resetBarsToStatic = () => {
  96. // 重置相位
  97. wavePhaseRef.current = 0;
  98. setBarHeights((prev) => {
  99. const targetLength = Math.max(1, barCount);
  100. const needsResize = prev.length !== targetLength;
  101. const needsReset =
  102. prev.length === 0 ||
  103. prev.some((height) => Math.abs(height - STATIC_HEIGHT) > 0.5);
  104. if (!needsResize && !needsReset) {
  105. return prev;
  106. }
  107. return Array.from({ length: targetLength }, () => STATIC_HEIGHT);
  108. });
  109. };
  110. // 动画循环
  111. useEffect(() => {
  112. if (!isAnimating) {
  113. if (animationRef.current) {
  114. cancelAnimationFrame(animationRef.current);
  115. animationRef.current = null;
  116. }
  117. resetBarsToStatic();
  118. return;
  119. }
  120. const animate = (timestamp: number) => {
  121. // 每 ANIMATION_INTERVAL 更新一次(模拟音频采样率)
  122. if (timestamp - lastUpdateRef.current > ANIMATION_INTERVAL) {
  123. updateWave();
  124. lastUpdateRef.current = timestamp;
  125. }
  126. animationRef.current = requestAnimationFrame(animate);
  127. };
  128. animationRef.current = requestAnimationFrame(animate);
  129. return () => {
  130. if (animationRef.current) {
  131. cancelAnimationFrame(animationRef.current);
  132. animationRef.current = null;
  133. }
  134. };
  135. }, [isAnimating, barCount]);
  136. // 生成波形条位置(响应式间距)
  137. const bars = Array.from({ length: barCount }, (_, i) => ({
  138. x: i * barSpacing, // 使用计算的响应式间距
  139. }));
  140. return (
  141. <div
  142. ref={containerRef}
  143. className="audio-wave-container"
  144. style={{
  145. width: '100%',
  146. height: `${height}px`,
  147. }}
  148. >
  149. <svg
  150. width="100%"
  151. height={height}
  152. viewBox={`0 0 ${actualWidth} ${height}`}
  153. fill="none"
  154. xmlns="http://www.w3.org/2000/svg"
  155. preserveAspectRatio="none"
  156. >
  157. <defs>
  158. {/* 彩色渐变定义 */}
  159. <linearGradient id="waveGradient" x1="100%" y1="0%" x2="0%" y2="0%">
  160. <stop offset="0%" stopColor="#F67B7C" />
  161. <stop offset="29.64%" stopColor="#FFBEF4" />
  162. <stop offset="49.41%" stopColor="#A699FE" />
  163. <stop offset="74.11%" stopColor="#84CCF8" />
  164. <stop offset="98.81%" stopColor="#FFDD5E" />
  165. </linearGradient>
  166. {/* mask 定义:响应式竖条 */}
  167. <mask id="audioMask">
  168. {bars.map((bar, index) => {
  169. // 获取当前竖条的高度(从 barHeights 数组)
  170. // 如果 barHeights[index] 为 undefined 或 NaN,使用 STATIC_HEIGHT 作为默认值
  171. const currentHeight = barHeights[index] ?? STATIC_HEIGHT;
  172. const safeHeight = Number.isNaN(currentHeight) || currentHeight <= 0 ? STATIC_HEIGHT : currentHeight;
  173. // 底部固定在容器底部
  174. const bottomY = height;
  175. const topY = bottomY - safeHeight;
  176. return (
  177. <rect
  178. key={index}
  179. x={bar.x}
  180. y={topY}
  181. width={barWidth}
  182. height={safeHeight}
  183. rx={cornerRadius}
  184. fill="white"
  185. className="audio-wave-bar"
  186. />
  187. );
  188. })}
  189. </mask>
  190. </defs>
  191. {/* 应用 mask 到渐变背景(只保留竖条部分) */}
  192. <g mask="url(#audioMask)">
  193. {/* 彩色渐变背景 */}
  194. <rect
  195. x="0"
  196. y="0"
  197. width={actualWidth}
  198. height={height}
  199. fill="url(#waveGradient)"
  200. />
  201. {/* 20% 黑色叠加层 */}
  202. <rect
  203. x="0"
  204. y="0"
  205. width={actualWidth}
  206. height={height}
  207. fill="black"
  208. fillOpacity="0.2"
  209. />
  210. </g>
  211. </svg>
  212. </div>
  213. );
  214. };