GSAP 3D飞行动画优化:如何实现完美的直线冲刺效果
在开发 CosmicInterestsSection 组件时,我们遇到一个棘手的视觉问题:当博客卡片从远处飞向观察者时,即使我们设置了线性的 X 和 Z 轴变化,视觉上看起来却是走"曲线"的。特别是在经过大标题文字时,这种"拐弯"的感觉尤为明显。
本文记录了从问题分析到最终实现"笔直冲向屏幕"效果的完整优化过程。
1. 问题现象
我们需要实现的效果是:两张博客预览图从屏幕深处(Z轴极远处)交替飞出,笔直地冲向屏幕前的观察者。
初始实现的缺陷:
- 视觉曲线:图片在飞行过程中似乎会先向外扩,然后再加速冲向边缘,导致一种"抛物线"或"拐弯"的视觉错觉。
- 起始位置偏移:图片不是从屏幕正中心出发,而是从标题两侧开始。
- 重叠问题:两张图片在起点处完全重叠,缺乏层次感。
2. 原因分析
经过仔细排查,我们发现问题的核心在于 CSS Transform 的坐标系分离 和 透视投影(Perspective)的非线性特性。
2.1 父子元素 Transform 分离
最初的代码中,我们将 X/Y 轴的平移放在了父容器上,而将 Z 轴的纵深变化放在了子元素上。
// 错误的做法
.to(parent, { x: '36vw' }) // 父元素控制水平移动
.to(child, { z: 800 }) // 子元素控制纵深当父元素移动时,子元素的局部坐标系随之移动。但在 3D 透视投影下,父元素的 X 轴移动和子元素的 Z 轴移动结合后,并不会在屏幕 2D 平面上产生线性的投影轨迹,导致了"曲线"错觉。
2.2 缺乏线性斜率约束
要让物体在 3D 空间中看起来是在做"直线运动",它的 X 轴位移(dX)和 Z 轴位移(dZ)必须保持恒定的比例(Slope)。 即:$\frac{\Delta X}{\Delta Z} = Constant$
如果 X 轴只移动了一点点,而 Z 轴移动了很多,或者反过来,都会导致视觉轨迹偏离直线。
3. 解决方案
最终的解决方案包含三个关键点:统一 Transform、中心归零 和 线性轨迹计算。
3.1 统一 Transform 到子元素
我们将所有的变换(X, Y, Z, Scale)全部统一应用到子元素 .post-preview 上,并强制父容器的偏移为 0。这样可以确保所有变换都在同一个坐标系下进行计算。
3.2 线性轨迹计算 (The Linear Trajectory)
为了保证笔直的飞行效果,我们计算了精确的起止坐标。
桌面端 (水平飞行):
我们希望图片从稍微偏离中心的位置(4vw)飞到屏幕边缘外(22vw),同时 Z 轴从 -200 飞到 800。
为了保证全程线性,我们使用了两段式动画,但保持斜率一致:
- 入场阶段: Z: -200 → X: ±15vw (接近观察者)
- 冲刺阶段: Z: 800 → X: ±22vw (飞出屏幕)
通过计算 $\frac{dX}{dZ}$ 比例,确保 X 的变化相对于 Z 的变化是恒定的。
移动端 (垂直飞行): 原理相同,只是将 X 轴的运动改为了 Y 轴(上下飞行)。
3.3 代码实现
以下是优化后的核心代码:
// 桌面端动画 (Desktop)
if (window.innerWidth > 768) {
// 确保父元素居中,不干扰子元素轨迹
gsap.set(post, { x: 0, y: 0 });
// 1. 初始化状态:设置起始位置和初始间距
// 使用 x 偏移 (4vw) 让两张图在起点就分开,避免重叠
gsap.set(`${postClass} .post-preview`, {
z: -2000,
x: postIndex === 0 ? '-4vw' : '4vw',
scale: 0.6,
opacity: 0
});
// 2. 飞行时间线
tl.to(`${postClass} .post-preview`, {
opacity: 1,
scale: 1.0,
z: -200,
// 关键点:中间帧的 X 值必须在直线上
x: postIndex === 0 ? '-15vw' : '15vw',
filter: 'blur(0px)',
duration: 2.5,
ease: 'none' // 必须使用线性缓动,否则轨迹会弯曲
}, postStartTime)
.to(`${postClass} .post-preview`, {
scale: 1.1,
z: 800,
// 终点 X 值,保持斜率一致
x: postIndex === 0 ? '-22vw' : '22vw',
opacity: 0,
filter: 'blur(2px)',
duration: 3.0,
ease: 'none'
}, postStartTime + 2.5);
}
// 移动端动画 (Mobile)
else {
gsap.set(post, { x: 0, y: 0 });
// 垂直方向的初始间距 (6vh)
gsap.set(`${postClass} .post-preview`, {
z: -2000,
y: postIndex === 0 ? '-6vh' : '6vh',
x: 0,
scale: 0.6,
opacity: 0
});
tl.to(`${postClass} .post-preview`, {
opacity: 1,
scale: 1.0,
z: -200,
y: postIndex === 0 ? '-20vh' : '20vh',
filter: 'blur(0px)',
duration: 2.0,
ease: 'none'
}, postStartTime)
.to(`${postClass} .post-preview`, {
scale: 1.1,
z: 800,
y: postIndex === 0 ? '-28vh' : '28vh',
opacity: 0,
filter: 'blur(2px)',
duration: 2.2,
ease: 'none'
}, postStartTime + 2.2);
}4. 关键经验总结
- 统一坐标系:做 3D 轨迹动画时,尽量将所有变换应用在同一个元素上,避免父子元素坐标系叠加带来的不可控透视畸变。
- 线性缓动 (Linear Ease):如果你想要物理上"直"的轨迹,务必使用
ease: 'none'。任何加速或减速曲线在 3D 投影下都可能表现为路径的弯曲。 - 斜率一致性:计算好起点、中间点和终点的坐标比率。如果 Z 轴走了 50%,X 轴也应该走 50%。
- 初始间距:不要让物体从同一个点(0,0,0)生成,给它们一个微小的初始偏移(如
4vw),会让视觉效果更加自然且有层次感。
通过这次优化,我们成功实现了博客卡片笔直冲向用户的视觉冲击力,同时完美适配了移动端的竖向交互体验。