You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 02:07:35