canvas:我是怎么把微积分用在动画上的

1,385 阅读5分钟

我在青少年时期产生了探寻存在之意义的渴求,大学期间就只学习了文学与哲学等人文学科,所以我并不知道别人口中艰涩、无趣、毫无用处的微积分竟然是上帝的语言”。——《微积分的力量》

这句话我是感同身受的,我高中大学都是文科,意识到它们的作用时已经是相见恨晚。

说个故事

我读大一的时候(2015年),有门课叫西方经济学,现在很多东西我都忘了,唯一记得的就是边际效应。

在微观经济学中,边际效用(英语:Marginal utility),又译为边际效应,是指每新增(或减少)一个单位的商品或服务,它对商品或服务的收益增加(或减少)的效用,也即是“效用──商品或服务量”图的斜率。——维基百科

当时学边际效应,老师要求我们计算收益曲线的斜率,也就是对曲线求导。

图片

我只觉得很神奇,但不知道为什么求导就能得出边际收益曲线,而且这玩意跟微积分很像。

微积分是什么

我不打算涉及任何数学符号表示的公式,我会引用一段话来阐述微积分:

为了探究任意一个连续的形状、物体、运动、过程或现象(不管它看起来有多么狂野和复杂),把它重新想象成由无穷多个简单部分组成的事物,分析这些部分,然后把结果加在一起,就能理解最初的那个整体。——《微积分的力量》

简单来说,微积分可分为微分和积分,分解的过程叫微分,叠加的过程叫积分。

以我们中学最熟悉的位移、速度、加速度举例:

  1. 微分过程:位移->速度->加速度

  2. 积分过程:加速度->速度->位移

图片

微积分与动画的关系

根据上面的物理例子,我们同样能对动画得出相似的结论:

  1. 微分过程:动画->帧->帧变化

  2. 积分过程:帧变化->帧->动画

为什么可以呢?

因为动画也是连续的过程,人眼识别连续图像的速度是24帧/秒,只要在1秒内塞下24张静态图片,那么我们就认为他是动画;如果1秒只有那么几张,我们可能认为它是PPT。

下图是MIT的经典力学公开课,教授做了一个自由落体运动的实验,关掉所有的灯,留下一台按一定频率闪烁的灯,用相机长曝光记录轨迹。图片

有了这张轨迹图,即使它不会动,我们也能感受到球在做自由落体运动,这就是积分学在摄影长曝光上展示的魅力,包括瀑布、星轨等照片亦是如此,换句话说,摄影就是进光量的积分过程(说这话的时候感觉自己升华到了哲学层次

那如果频闪一次拍一张照片,这些照片连续播放起来就是动画,像小时候看的连环画一样。

图片

(图不会动,不是你网络不行,而是我没找到动图)

关于RAF

什么是RAF

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。——MDN

RAF就是为了动画而生的函数,会根据屏幕的刷新率执行。如果屏幕的刷新率为60Hz,就是1秒内最多能连续播出60张静态画面。

递归调用RAF做动画

在回调函数再次调用RAF,就能做到连续绘制图像。

  const animate = () => {
    // 每一帧的绘制
    update();
    requestAnimationFrame(animate);
  };
  animate();

是否清除前一帧的残留

上面的代码并没有清除上一帧的残留,动画会像长曝光一样出现轨迹:

图片

清除残留很简单,在绘制下一帧前调用上一篇文章提到的橡皮擦ctx.clearRect即可:

  const animate = () => {
    // 先清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    update();
    requestAnimationFrame(animate);
  };
  animate();

图片

如果想来点残影,像流星一样带一条尾巴,只要清除残留的时候不完全清除即可,这里就不能用橡皮擦了,要用ctx.fillRect重新覆盖一个带透明度的背景色:

  const animate = () => {
    // 覆盖带透明度的背景色
    ctx.fillStyle = 'rgba(255,255,255,0.1)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    update();
    requestAnimationFrame(animate);
  };
  animate();

图片

实现一个简单的二维粒子运动

说了这么多,现在就着手实践一下

基本的代码框架

  1. 粒子类,包含位置(x,y),绘制方法(画圆),更新数据(改变位移)

  2. 动画,递归调用RAF来更新粒子的位移,这里就是运用积分学的地方

class Particle {
  x,
  y,
  ...
  constructor(props) {...}
  draw() {
    ...
  }
  update() {
        ...
    this.draw();
  }
}

const p = new Particle({
  ...
});

const animate = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  p.update();
  requestAnimationFrame(animate);
};
animate();

实践积分方程

我们先要找到粒子运动微分的部分,回想一下我们上面提到的:位移->速度->加速度。

我们抛开加速度不说,速度(vx,vy)就是位移(x,y)微分后的结果,反过来只要我们求出粒子的速度(vx,vy),在每一帧更新的时候使用如下公式更新位移:

下一帧的位置=当前帧的位置+速度

那么,第n帧的位置就是n个速度积分后的结果:

// 第一帧
S1=v0
// 第二帧
S2=v0+v1
// 第三帧
S3=v0+v1+v2
...
// 第n帧
Sn=v0+v1+...vn-1

我们设速度为(1,1),写进update方法里:

class Particle {
  ...
  update() {
    // 其他操作
    ...
    // 积分过程
    this.x+=1;
    this.y+=1;
    this.draw();
  }
}

得出的效果就会类似下图所示:

图片

つづく

本文引入微积分简单介绍了动画的绘制,后面的篇章就会涉及到动画交互、向量、力等更有兴趣的东西。

又到了引流时间,欢迎大家关注dkplus公众号,聊一些图像相关的技术。