最终结果:https://codepen.io/yuqianma/pen/WYVNEe

前面我们发现了,画布可以被清除;不清除画布的话,图形会一直保留在上面。
你一定猜到了,动画就是通过不断地绘制-清除产生的。

时间

当然,如果我们一次性把事情都干完,也就没有动画的效果了。
在这里,我们引入一个可以让事情延迟的API:setTimeout

Have a try!

  1. console.log('ping', new Date());
  2. function pong() {
  3. console.log('pong', new Date());
  4. }
  5. setTimeout(pong, 2000);

new Date() 可以获取当前时间

把上面的修改一下

  1. function green() {
  2. rect(70, 10, 50, 50, 'green');
  3. }
  4. function yellow() {
  5. rect(70, 10, 50, 50, 'yellow');
  6. }
  7. setTimeout(green, 500);
  8. setTimeout(yellow, 1000);

500ms后,画个绿色的矩形
1000ms后,画个黄色的矩形

你的第一个、没有X用的动画完成了

我们发现,动画需要让它不断地运行下去。
那么我们需要不断地调用 setTimeout

  1. setTimeout(fn1, 500);
  2. setTimeout(fn2, 1000);
  3. setTimeout(fn3, 1500);
  4. // ...

用循环?
看上去也可以,不过,要多少个调用?

如果延时结束时,再调用一次延时,会怎么样呢?

  1. function green() {
  2. rect(70, 10, 50, 50, 'green');
  3. setTimeout(yellow, 1000);
  4. }
  5. function Yellow() {
  6. rect(70, 10, 50, 50, 'yellow');
  7. }
  8. setTimeout(green, 500);

发生了什么?
500ms后,画绿色矩形,并在1000ms后画黄色矩形
1500ms后,黄色矩形画出来了

怎样像上面一样,还是每隔500ms画一次?
……

如果让 drawGreenRect 自己调用自己?

  1. function step () {
  2. console.log(new Date());
  3. setTimeout(step, 1000);
  4. }
  5. step();

1s走一次的log

问题:没法停

条件语句

var running = true;

function step () {
  console.log(new Date());
  if (running) {
    setTimeout(step, 1000);
  }
}

step();

在Console试试输入:running = false

我们可以提前设置好条件,让机器自己判断

var startTime = new Date();
var duration = 3000;

function step () {
  console.log(new Date());
  var elapsed = new Date() - startTime;

  if (elapsed < duration) {
    setTimeout(step, 1000);
  }
}

step();

现在我们加上绘制函数,就可以看到动画了:

function step () {
  console.log(new Date());
  var elapsed = new Date() - startTime;

  rect(elapsed / 10, 10, 50, 50, 'blue');

  if (elapsed < duration) {
    setTimeout(step, 1000);
  }
}

有点奇怪?
把上一次的图像清除掉~

// ...
ctx.clearRect(0, 0, canvas.width, canvas.height);
rect(elapsed / 10, 10, 50, 50, 'blue');
// ...

把setTimeout的时间进一步缩短

一般取16ms是因为,60帧/秒被认为是流畅的动画。而1000 / 60 ≈ 16。比16ms慢的话,动画就要不流畅了。

这总算是你的第一个动画了吧~

3s太长了,我想2s完成动画

改成2s?

运动没到底就停了……

设置运动

让我们理一理
Q: 刚才,我们是怎么让矩形运动的?
A: 每隔16ms变一下 x

Q: x 是怎么变的?
A: 依赖流逝的时间,时间 0 -> 3000ms,x 0 -> 300

所以我们现在的问题是:

时间 0 -> 2000ms,x 还要 0 -> 300

怎么做?
简单地说:
Canvas 动画 - 图1
不就行了嘛~

注意到,如果

Canvas 动画 - 图2

Canvas 动画 - 图3
这样,x 就可以简单地写作:
Canvas 动画 - 图4

[0, 1] 的区间很好用,比如,我们可以把它看作是百分比。
这样,x 不需要知道具体的时间,只要知道它该运动到n%就可以了。

上面的计算,就是函数的映射:从 [0, 1] 映射到 [0, 300]

改一下代码:

var startTime = new Date();
var duration = 3000;

function step () {
  // console.log(new Date());
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  var elapsed = new Date() - startTime;

  var t = elapsed / duration;

  var x = t * 300;

  rect(x, 10, 50, 50, '#0b9');

  if (elapsed < duration) {
    setTimeout(step, 1000);
  }
}

step();

下一个问题:

我要 x 从50运动到300怎么办?
即 [0, 1] -> [50, 300]

Canvas 动画 - 图5%20%2B%2050#card=math&code=x%20%3D%20t%20%2A%20%28300%20-%2050%29%20%2B%2050&height=20&id=f2dc603c&width=167)

// ...
var t = elapsed / duration;

var from = 50;
var to = 350;

var x = t * (to - from) + from;

rect(x, 10, 50, 50, '#0b9');
// ...

函数返回值

注意到这里:

var x = t * (to - from) + from;

对于不同的值,我们每次都要这么写,有些累赘。
我们可以包装成一个函数,让代码更简洁点。这回我们需要函数返回值:

function getTweenValue (t, from, to) {
  return t * (to - from) + from;
}

return 后面的结果,会成为这个函数的“返回值”。

  • 返回值只能有1个,更多返回值都会被忽略。

  • 返回值不一定是数值,可以是各种东西,甚至一个函数。

// ...
var t = elapsed / duration;

var x = getTweenValue(t, 50, 350);

rect(x, 10, 50, 50, '#0b9');
// ...

甚至,我们可以不用 x 这个变量:

rect(getTweenValue(t, 50, 350), 10, 50, 50, '#0b9');

getTweenValue 放在不同参数上试试?比如 y ,或者 height

rect(50, 10, 50, getTweenValue(t, 50, 350), '#0b9');

比较一下:

rect(50, 10, 50, 50, '#0b9'); // 语句1

rect(50, 10, 50, getTweenValue(t, 50, 350), '#0b9'); // 语句2

没有什么神奇的,仅仅是把时间映射到了位置或大小,就产生了动画。

Easing

以上的动画,是匀速的运动。
我们知道,现实世界的运动,不总是匀速的。考虑重力,有加速运动;考虑摩擦,有减速运动。
如果让我们的动画类似于现实世界,会加速或减速,它看起来就更自然。
缓动函数(Easing)就是这样的函数。

回想一下重力加速度与高度的关系:
Canvas 动画 - 图6

我们发现,高度(距离)随着时间,变化越来越快了。
因为是模拟,我们只需要找到一种方法,让值越变越快就行了。

回顾一下 getTweenValue ,输入一个[0, 1]的值,输出值在[from , to]。虽然越变越快,但是起始和终点没有变化。所以,我们就只能改变 t 了。

t 不是流逝时间的百分比吗?怎么变呢?

想一下,我们刚才把时间映射到了[0, 1]。是线性的。能不能不是线性,还在[0, 1]之间?

Canvas 动画 - 图7

对吧,还在[0, 1]之间。

// var duration = 800;
var t1 = t * t;
rect(50, 10, 50, getTweenValue(t1, 0, 350), '#0b9');

我们把 t1 = t * t 这一句也包装成一个函数。这就是easing函数。这一个例子是”easeInQuad”。

参考:http://greweb.me/2012/02/bezier-curve-based-easing-functions-from-concept-to-implementation/

var Easing = {
  linear: function (t) { return t },
  easeInQuad: function (t) { return t*t },
  easeOutQuad: function (t) { return t*(2-t) },
  easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
  easeInCubic: function (t) { return t*t*t },
  easeOutCubic: function (t) { return (--t)*t*t+1 },
  easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
  easeInQuart: function (t) { return t*t*t*t },
  easeOutQuart: function (t) { return 1-(--t)*t*t*t },
  easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
  easeInQuint: function (t) { return t*t*t*t*t },
  easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
  easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
}

可能你对上面的写法有点陌生,不过只要知道怎样使用就可以了:

// 把上面的Easing复制到文件最开始

rect(50, 10, 50, getTweenValue(Easing.easeInQuad(t), 0, 350), '#0b9');

可能你对三目运算符(a ? b:c)也感到奇怪,其实很简单:

function foo (v) { return v ? 1 : 0; }
// 等于
function bar () {
  if (v) {
    return 1;
  } else {
    return 0;
  }
}

看一个函数的例子 easeInOutQuad
Canvas 动画 - 图8

easing函数的特点:定义域和值域都在[0, 1]

为什么?

……

简单的柱形图

for (var i = 0; i < 5; ++i) {
  rect(i * 60, 10, 50,
    getTweenValue(Easing.easeInOutQuint(t), 0, (i + 1) * 100),
    '#0b9');
}

照顾你们给个完整的

var Easing = {
  linear: function (t) { return t },
  easeInQuad: function (t) { return t*t },
  easeOutQuad: function (t) { return t*(2-t) },
  easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
  easeInCubic: function (t) { return t*t*t },
  easeOutCubic: function (t) { return (--t)*t*t+1 },
  easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
  easeInQuart: function (t) { return t*t*t*t },
  easeOutQuart: function (t) { return 1-(--t)*t*t*t },
  easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
  easeInQuint: function (t) { return t*t*t*t*t },
  easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
  easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
}

var canvas = document.getElementById('mycanvas');
canvas.width = 400;
canvas.height = 600;
var ctx = canvas.getContext('2d');

function rect (x, y, width, height, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, width, height);
}

function getTweenValue (t, from, to) {
  return t * (to - from) + from;
}

var startTime = new Date();
var duration = 800;

function step () {
  // console.log(new Date());
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  var elapsed = new Date() - startTime;

  var t = elapsed / duration;

  for (var i = 0; i < 5; ++i) {
    rect(i * 60, 10, 50,
      getTweenValue(Easing.easeInOutQuint(t), 0, (i + 1) * 100),
      '#0b9');
  }

  if (elapsed < duration) {
    setTimeout(step, 16);
  }
}

step();