一、动画基础与数学原理

动画的本质:让静止的画面「活」起来

动画到底是什么?说白了,就是让画面动起来。

我刚开始做动画时,总觉得这是个玄学。后来才明白,动画的本质其实很简单——快速切换一系列静态图像,利用人眼的视觉暂留效应,产生连续运动的错觉

人眼有个特点:当一个图像消失后,它会在视网膜上停留约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像素。

小结

这一章我们聊了动画的底层原理:

  • 动画的本质是视觉暂留效应
  • 时间与空间的关系构成了动画的骨架
  • 线性插值是动画计算的基础工具
  • 缓动函数让动画从「机械」变「自然」
  • 贝塞尔曲线提供了更灵活的曲线控制

下一章,我们会把这些数学原理应用到实际的路径规划中。到时候你会发现——有了这些基础,复杂动画也不过是「搭积木」而已。