React项目中SVG路径hover时替换振动动画为波浪动画需求
问题
在React项目中有一个包含多条水平路径的SVG,希望鼠标悬停在每条路径上时,触发从悬停点起始、延伸至路径末端的波浪动画,当前使用的振动CSS动画需要替换成这个效果。对SVG不熟悉,不知道如何实现,原React组件代码如下:
"use client"; import { useCallback } from "react"; import "./styles.css"; const SongPlayer = () => { const soundFontUrl = "/sounds/guitar/"; const playNote = useCallback((note: string) => { const audio = new Audio(soundFontUrl + note.trim() + ".mp3"); audio.play(); }, []); const handleMouseOver = (note: string, id: string) => { playNote(note); const el = document.getElementById(id); el?.classList.add("vibrating"); setTimeout(() => { el?.classList.remove("vibrating"); }, 1000); }; return ( <div className="w-full mt-[44px]"> <div className="w-full flex justify-center items-center"> <svg id="guitar" className="guitar" width="1700" height="324" viewBox="0 0 1441 324" fill="none" xmlns="http://www.w3.org/2000/svg" > <path id="g-one" onMouseOver={() => handleMouseOver("D3", "g-one")} d="M1075.88 242.167C1059.69 248.627 1044.41 255.881 1021.79 251.862C1020.83 251.69 1019.91 251.509 1019.03 251.319M1019.03 251.319C987.538 244.521 1010.27 226.472 1021.79 232.056C1028.69 235.4 1025.2 244.393 1019.03 251.319ZM1019.03 251.319C1014.6 256.283 1008.47 258.373 993.68 258.373C954.721 258.373 905.598 232.907 799 235.49L0 235.49" stroke="#A39D92" strokeWidth="1.5" /> <path id="g-two" onMouseOver={() => handleMouseOver("D5", "g-two")} d="M0 275H1089.5" stroke="#A39D92" strokeWidth="1.5" /> <path id="g-three" onMouseOver={() => handleMouseOver("C4", "g-three")} d="M0 164.5H1026" stroke="#A39D92" strokeWidth="1.5" /> <path id="g-four" onMouseOver={() => handleMouseOver("C2", "g-four")} d="M0 125H1057.5" stroke="#A39D92" strokeWidth="1.5" /> <path id="g-five" onMouseOver={() => handleMouseOver("F4", "g-five")} d="M0 54H1151" stroke="#A39D92" strokeWidth="1.5" /> <path id="g-six" onMouseOver={() => handleMouseOver("F2", "g-six")} d="M0 14.5H1164.5" stroke="#A39D92" strokeWidth="1.5" /> </svg> </div> </div> ); }; export default SongPlayer;
解决方案
要实现从悬停点出发的波浪动画,核心是利用SVG的stroke-dasharray和stroke-dashoffset属性结合CSS动画,同时获取鼠标在路径上的精确位置计算动画起始点。以下是具体实现:
1. 修改React组件代码
替换原组件,添加动画路径层,更新鼠标事件逻辑:
"use client"; import { useCallback, useState } from "react"; import "./styles.css"; const SongPlayer = () => { const soundFontUrl = "/sounds/guitar/"; // 存储各路径的动画状态:{ [id]: { startOffset: number, totalLength: number } } const [animations, setAnimations] = useState<Record<string, { startOffset: number; totalLength: number }>>({}); const playNote = useCallback((note: string) => { const audio = new Audio(soundFontUrl + note.trim() + ".mp3"); audio.play(); }, []); const handleMouseOver = useCallback((note: string, id: string, e: React.MouseEvent<SVGPathElement>) => { playNote(note); const pathEl = e.currentTarget; const totalLength = pathEl.getTotalLength(); // 转换鼠标坐标为SVG内部坐标,计算悬停点在路径上的长度 const point = pathEl.createSVGPoint(); point.x = e.clientX; point.y = e.clientY; const svgPoint = point.matrixTransform(pathEl.getScreenCTM()?.inverse() as SVGMatrix); const hoverLength = pathEl.getPointAtLength(svgPoint.x); // 计算动画起始偏移:从悬停点到路径末端的长度 const startOffset = totalLength - hoverLength; setAnimations(prev => ({ ...prev, [id]: { startOffset, totalLength } })); // 1秒后清除动画状态 setTimeout(() => { setAnimations(prev => { const newState = { ...prev }; delete newState[id]; return newState; }); }, 1000); }, [playNote]); // 获取动画路径的属性 const getAnimationPathProps = (id: string) => { const anim = animations[id]; if (!anim) return { display: "none" }; // 波浪样式:短虚线组合,可调整数值改变波浪密度 const dashArray = "4 2"; return { strokeDasharray: dashArray, strokeDashoffset: anim.startOffset, className: "wave-animation" }; }; return ( <div className="w-full mt-[44px]"> <div className="w-full flex justify-center items-center"> <svg id="guitar" className="guitar" width="1700" height="324" viewBox="0 0 1441 324" fill="none" xmlns="http://www.w3.org/2000/svg" > {/* 原始路径层 */} <path id="g-one" onMouseOver={(e) => handleMouseOver("D3", "g-one", e)} d="M1075.88 242.167C1059.69 248.627 1044.41 255.881 1021.79 251.862C1020.83 251.69 1019.91 251.509 1019.03 251.319M1019.03 251.319C987.538 244.521 1010.27 226.472 1021.79 232.056C1028.69 235.4 1025.2 244.393 1019.03 251.319ZM1019.03 251.319C1014.6 256.283 1008.47 258.373 993.68 258.373C954.721 258.373 905.598 232.907 799 235.49L0 235.49" stroke="#A39D92" strokeWidth="1.5" /> {/* 动画路径层:与原始路径形状一致 */} <path d="M1075.88 242.167C1059.69 248.627 1044.41 255.881 1021.79 251.862C1020.83 251.69 1019.91 251.509 1019.03 251.319M1019.03 251.319C987.538 244.521 1010.27 226.472 1021.79 232.056C1028.69 235.4 1025.2 244.393 1019.03 251.319ZM1019.03 251.319C1014.6 256.283 1008.47 258.373 993.68 258.373C954.721 258.373 905.598 232.907 799 235.49L0 235.49" stroke="#FFFFFF" strokeWidth="2" {...getAnimationPathProps("g-one")} /> <path id="g-two" onMouseOver={(e) => handleMouseOver("D5", "g-two", e)} d="M0 275H1089.5" stroke="#A39D92" strokeWidth="1.5" /> <path d="M0 275H1089.5" stroke="#FFFFFF" strokeWidth="2" {...getAnimationPathProps("g-two")} /> <path id="g-three" onMouseOver={(e) => handleMouseOver("C4", "g-three", e)} d="M0 164.5H1026" stroke="#A39D92" strokeWidth="1.5" /> <path d="M0 164.5H1026" stroke="#FFFFFF" strokeWidth="2" {...getAnimationPathProps("g-three")} /> <path id="g-four" onMouseOver={(e) => handleMouseOver("C2", "g-four", e)} d="M0 125H1057.5" stroke="#A39D92" strokeWidth="1.5" /> <path d="M0 125H1057.5" stroke="#FFFFFF" strokeWidth="2" {...getAnimationPathProps("g-four")} /> <path id="g-five" onMouseOver={(e) => handleMouseOver("F4", "g-five", e)} d="M0 54H1151" stroke="#A39D92" strokeWidth="1.5" /> <path d="M0 54H1151" stroke="#FFFFFF" strokeWidth="2" {...getAnimationPathProps("g-five")} /> <path id="g-six" onMouseOver={(e) => handleMouseOver("F2", "g-six", e)} d="M0 14.5H1164.5" stroke="#A39D92" strokeWidth="1.5" /> <path d="M0 14.5H1164.5" stroke="#FFFFFF" strokeWidth="2" {...getAnimationPathProps("g-six")} /> </svg> </div> </div> ); }; export default SongPlayer;
2. 添加CSS动画样式
在styles.css中替换原.vibrating类,添加波浪动画:
.wave-animation { animation: wave 1s linear forwards; } @keyframes wave { to { stroke-dashoffset: 0; } }
关键点说明
- 每个原始路径上方对应一个相同形状的动画路径,默认隐藏,触发时显示
- 通过坐标转换和
getPointAtLength方法,精确计算鼠标在路径上的位置,确保动画从悬停点开始 - 调整
strokeDasharray的数值可改变波浪密度(如"6 3"会让波浪更宽) - 动画颜色可通过
stroke属性修改,示例中用白色突出波浪效果
内容的提问来源于stack exchange,提问作者Hasan
相关产品推荐
相关产品推荐

