一、动画基础与数学原理
动画的本质:让静止的画面「活」起来
动画到底是什么?说白了,就是让画面动起来。
我刚开始做动画时,总觉得这是个玄学。后来才明白,动画的本质其实很简单——快速切换一系列静态图像,利用人眼的视觉暂留效应,产生连续运动的错觉。
人眼有个特点:当一个图像消失后,它会在视网膜上停留约0.1秒。如果你在0.1秒内放上下一张图,大脑就会自动「脑补」出中间的运动过程。这就是动画能骗过眼睛的根本原因。
核心要点:
- 动画 = 静态画面 × 时间维度
- 帧率(FPS)决定了流畅度:24fps是电影标准,60fps是UI流畅标准
- 每一帧的变化量决定了运动速度
我在项目中遇到过一个问题:一个简单的按钮动画,在60Hz屏幕上很流畅,换到120Hz屏幕上却出现了卡顿。后来才发现,是帧率适配没做好。嗯,这里要注意——动画不能写死帧率,要用时间驱动。
时间与空间的关系:动画的「坐标轴」
动画其实就是在时间轴上,描述物体在空间中的位置变化。
你想想看,一个球从A点滚到B点,我们关心什么?
- 时间:从开始到结束,花了多久?
- 空间:每一时刻,球在什么位置?
- 速度:位置随时间变化的快慢
用数学语言来说:
位置 = f(时间)
速度 = 位置对时间的导数
加速度 = 速度对时间的导数
我习惯把动画看作一个「时间→位置」的映射函数。输入时间,输出位置。就这么简单。
我的经验:
做动画时,永远用「当前时间 - 开始时间」来计算进度,而不是用计数器累加。因为计数器会受帧率影响,而时间不会。我曾经因为这个踩过坑——60fps下动画跑2秒,30fps下跑了4秒,画面直接慢放。
线性插值:最简单的动画数学
线性插值(Lerp)是动画里最基础、最常用的数学工具。没有之一。
公式长这样:
value = start + (end - start) * t
其中 t 是0到1之间的进度值。
举个例子:从100px移动到200px,t=0.5时,位置就是150px。很简单对吧?
但实际项目中,我们很少直接用线性插值。为什么?因为线性运动太「死板」了——物体突然启动、突然停止,看起来像机器人。
避坑指南:
我曾经在一个UI动效里用了纯线性插值,结果被设计师吐槽「像PPT切换」。后来我加上了缓动函数,效果立刻好了很多。记住:自然界没有匀速运动,所有真实运动都有加速和减速过程。
缓动函数:让动画有「灵魂」
缓动函数(Easing Function)就是对线性插值中的 t 进行「加工」,让运动更自然。
常见的缓动类型:
| 类型 | 效果 | 适用场景 |
|---|---|---|
| ease-in | 慢→快 | 物体下落、弹出 |
| ease-out | 快→慢 | 物体停止、淡出 |
| ease-in-out | 慢→快→慢 | UI过渡、页面切换 |
| 弹性缓动 | 超出目标再弹回 | 弹窗、通知提示 |
代码实现一个简单的 ease-out:
def ease_out_quad(t):
"""二次方缓出:t从0到1,输出从慢到快"""
return 1 - (1 - t) * (1 - t)
def ease_in_out_cubic(t):
"""三次方缓入缓出:两头慢中间快"""
if t < 0.5:
return 4 * t * t * t
else:
return 1 - (-2 * t + 2) ** 3 / 2
我建议你在项目里准备一套常用的缓动函数库。我自己就维护了一个小工具包,里面大概有20多种缓动函数,随用随取。
贝塞尔曲线原理:工业级动画的基石
贝塞尔曲线是缓动函数的「进阶版」。它用控制点来定义一条曲线,比数学公式更直观。
三次贝塞尔曲线的公式:
B(t) = (1-t)³·P₀ + 3(1-t)²·t·P₁ + 3(1-t)·t²·P₂ + t³·P₃
其中 P₀ 是起点,P₃ 是终点,P₁ 和 P₂ 是控制点。
在CSS里,我们经常用 cubic-bezier() 来定义动画曲线:
/* 标准缓入缓出 */
animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
/* 弹性效果 */
animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
关键理解:
贝塞尔曲线的横轴是时间 t,纵轴是进度。曲线越陡,运动越快;曲线越平,运动越慢。控制点可以「拉」出各种效果——甚至可以让进度先倒退再前进(比如弹性效果)。
我记得有一次做复杂的路径动画,需要让一个元素沿着S形轨迹运动。用线性插值根本搞不定,最后用三段贝塞尔曲线拼接起来,效果完美。说白了,贝塞尔曲线就是动画界的「瑞士军刀」——一个工具解决大部分问题。
实战:把理论串起来
来看一个完整的例子:让一个方块从左侧滑入,带缓动效果。
import time
def animate_slide_in(element, duration=1.0):
"""让元素从左侧滑入,带缓出效果"""
start_time = time.time()
start_x = -100 # 起始位置:屏幕外
end_x = 100 # 结束位置:屏幕内
while True:
current_time = time.time()
elapsed = current_time - start_time
t = min(elapsed / duration, 1.0) # 限制在0~1
# 应用缓出函数
eased_t = 1 - (1 - t) * (1 - t)
# 计算当前位置
current_x = start_x + (end_x - start_x) * eased_t
# 更新元素位置
element.style.left = f"{current_x}px"
if t >= 1.0:
break
time.sleep(1/60) # 60fps
这个例子包含了:
- 时间驱动(用 elapsed 而不是计数器)
- 线性插值(计算 current_x)
- 缓动函数(eased_t)
- 帧率控制(sleep 1/60)
我的建议:
初学者最容易犯的错误是「一步到位」——直接把元素从A移到B,没有中间过程。记住:动画的灵魂在中间帧。每一帧的变化量越小,动画越流畅。我一般会把动画拆成60帧以上,每帧变化不超过1像素。
小结
这一章我们聊了动画的底层原理:
- 动画的本质是视觉暂留效应
- 时间与空间的关系构成了动画的骨架
- 线性插值是动画计算的基础工具
- 缓动函数让动画从「机械」变「自然」
- 贝塞尔曲线提供了更灵活的曲线控制
下一章,我们会把这些数学原理应用到实际的路径规划中。到时候你会发现——有了这些基础,复杂动画也不过是「搭积木」而已。