如何为SVG飞机关键帧动画绘制跟随轨迹线条?
实现飞机移动轨迹留存效果
核心思路
利用SVG线条的stroke-dasharray+stroke-dashoffset动画特性,结合你已有的CSS变量--crash-point,让轨迹线随飞机移动逐步绘制并留存完整路径。轨迹的终点坐标直接关联飞机的最终位置,适配随机生成的移动距离。
具体实现步骤
1. 替换轨迹元素为SVG线条
把原有的<div className="line"></div>换成SVG元素,精准控制轨迹的起点、终点和动画逻辑:
<svg className="trace-svg" viewBox="0 0 500 500"> <line className="trace-line" x1="0" y1="0" x2="var(--crash-point)" y2="calc(var(--crash-point) * -1.5)" /> </svg>
2. 为SVG轨迹添加样式与动画
在CSS中设置轨迹的绘制动画,让它和飞机的移动同步:
.trace-svg { position: absolute; left: 0; bottom: 0; width: 100%; height: 100%; z-index: 998; /* 放在飞机下方,不遮挡飞机 */ } .trace-line { stroke: #000; stroke-width: 2; /* 用勾股定理计算斜向轨迹的总长度,确保dasharray匹配线条长度 */ stroke-dasharray: calc(var(--crash-point) * 1.802); /* sqrt(1²+1.5²)≈1.802 */ stroke-dashoffset: calc(var(--crash-point) * 1.802); animation: drawTrace 5s ease-in-out forwards; } @keyframes drawTrace { to { stroke-dashoffset: 0; } }
3. 适配坠毁阶段的轨迹(可选)
如果需要在飞机坠毁阶段继续绘制轨迹,添加衔接动画:
.img-container.crash .trace-line { animation: drawTrace 5s ease-in-out forwards, drawCrashTrace 5s ease-in-out forwards 5s; } @keyframes drawCrashTrace { from { x2: var(--crash-point); y2: calc(var(--crash-point) * -1.5); stroke-dasharray: calc(var(--crash-point) * 1.802); stroke-dashoffset: 0; } to { x2: var(--crash-point); y2: calc(var(--crash-point) / 2); stroke-dasharray: calc(var(--crash-point) * 2); /* 坠毁段y方向总长度为2倍--crash-point */ stroke-dashoffset: 0; } }
4. 调整容器动画保持最终状态
给所有容器动画添加forwards关键字,确保动画结束后停在最终位置:
.img-container { animation: moveDiagonal 5s ease-in-out forwards; } .animation-container { animation: shiftContainerX 5s ease-in-out forwards; } .crash-container { animation: crashContainer 5s ease-in-out forwards; } .crash { animation: crash 5s ease-in-out forwards; } img { animation: moveX 10s forwards; }
修改后的完整代码
App.jsx
import { useEffect, useRef } from "react"; import "./App.css"; import plane from "./assets/plane.svg"; function App() { const planeRef = useRef(); const containerRef = useRef(); const animationContainerRef = useRef(); useEffect(() => { const crashPoint = `${Math.floor(Math.random() * (500 - 200 + 1)) + 200}px`; document.documentElement.style.setProperty("--crash-point", crashPoint); console.log(document.documentElement.style.getPropertyValue("--crash-point")); }, []); return ( <> <div className="big-container"> <div className="animation-container shift-container-x" ref={animationContainerRef} > <div ref={containerRef} className="img-container" onAnimationEnd={() => { containerRef.current.classList.add("crash"); animationContainerRef.current.classList.add( "crash-container", ); }} > <img ref={planeRef} src={plane} alt="飞机" /> {/* SVG轨迹线条 */} <svg className="trace-svg" viewBox="0 0 500 500"> <line className="trace-line" x1="0" y1="0" x2="var(--crash-point)" y2="calc(var(--crash-point) * -1.5)" /> </svg> </div> </div> </div> </> ); } export default App;
App.css
html { --crash-point: 200px; } .big-container { position: relative; height: 500px; width: 500px; border: 2px solid red; margin: 5rem; margin-top: 700px; background: transparent; } .animation-container { border: 1px solid purple; position: absolute; height: 2500px; width: 2000px; bottom: -25rem; background: url(./assets/white-dots.jpg) repeat center; background-size: 15%; z-index: -100; animation: shiftContainerX 5s ease-in-out forwards; } .crash-container { animation: crashContainer 5s ease-in-out forwards; } .img-container { position: absolute; left: 0; bottom: 25rem; width: 500px; height: 50px; animation: moveDiagonal 5s ease-in-out forwards; z-index: 999; } .crash { animation: crash 5s ease-in-out forwards; } img { position: absolute; left: 0; bottom: 0; width: 50px; object-fit: contain; height: 50px; animation: moveX 10s forwards; z-index: 999; } .trace-svg { position: absolute; left: 0; bottom: 0; width: 100%; height: 100%; z-index: 998; } .trace-line { stroke: #000; stroke-width: 2; stroke-dasharray: calc(var(--crash-point) * 1.802); stroke-dashoffset: calc(var(--crash-point) * 1.802); animation: drawTrace 5s ease-in-out forwards; } .img-container.crash .trace-line { animation: drawTrace 5s ease-in-out forwards, drawCrashTrace 5s ease-in-out forwards 5s; } @keyframes moveDiagonal { 100% { transform: translate( var(--crash-point), calc(var(--crash-point) * -1.5) ); } } @keyframes crash { 0% { transform: translate( var(--crash-point), calc(var(--crash-point) * -1.5) ); } 100% { transform: translate(var(--crash-point), calc(var(--crash-point) / 2)); } } @keyframes moveX { 100% { transform: translate(calc(var(--crash-point) / 1.5)); } } @keyframes shiftContainerX { 100% { transform: translate( calc(var(--crash-point) / -1.25), calc(var(--crash-point) / 1.5) ); } } @keyframes crashContainer { 0% { transform: translate( calc(var(--crash-point) / -1.25), calc(var(--crash-point) / 1.5) ); } 100% { transform: translate( calc(var(--crash-point) / -1.25), calc(var(--crash-point) / -1.5) ); } } @keyframes drawTrace { to { stroke-dashoffset: 0; } } @keyframes drawCrashTrace { from { x2: var(--crash-point); y2: calc(var(--crash-point) * -1.5); stroke-dasharray: calc(var(--crash-point) * 1.802); stroke-dashoffset: 0; } to { x2: var(--crash-point); y2: calc(var(--crash-point) / 2); stroke-dasharray: calc(var(--crash-point) * 2); stroke-dashoffset: 0; } }
内容的提问来源于stack exchange,提问作者Tices
相关产品推荐
相关产品推荐

