最终结果:https://codepen.io/yuqianma/pen/WYVNEe
前面我们发现了,画布可以被清除;不清除画布的话,图形会一直保留在上面。
你一定猜到了,动画就是通过不断地绘制-清除产生的。
时间
当然,如果我们一次性把事情都干完,也就没有动画的效果了。
在这里,我们引入一个可以让事情延迟的API:setTimeout
Have a try!
console.log('ping', new Date());function pong() {console.log('pong', new Date());}setTimeout(pong, 2000);
new Date()可以获取当前时间
把上面的修改一下
function green() {rect(70, 10, 50, 50, 'green');}function yellow() {rect(70, 10, 50, 50, 'yellow');}setTimeout(green, 500);setTimeout(yellow, 1000);
500ms后,画个绿色的矩形
1000ms后,画个黄色的矩形
你的第一个、没有X用的动画完成了
我们发现,动画需要让它不断地运行下去。
那么我们需要不断地调用 setTimeout
setTimeout(fn1, 500);setTimeout(fn2, 1000);setTimeout(fn3, 1500);// ...
用循环?
看上去也可以,不过,要多少个调用?
如果延时结束时,再调用一次延时,会怎么样呢?
function green() {rect(70, 10, 50, 50, 'green');setTimeout(yellow, 1000);}function Yellow() {rect(70, 10, 50, 50, 'yellow');}setTimeout(green, 500);
发生了什么?
500ms后,画绿色矩形,并在1000ms后画黄色矩形
1500ms后,黄色矩形画出来了
怎样像上面一样,还是每隔500ms画一次?
……
如果让 drawGreenRect 自己调用自己?
function step () {console.log(new Date());setTimeout(step, 1000);}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
怎么做?
简单地说:
不就行了嘛~
注意到,如果
令
则
这样,x 就可以简单地写作:
[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]
%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)就是这样的函数。
回想一下重力加速度与高度的关系:
我们发现,高度(距离)随着时间,变化越来越快了。
因为是模拟,我们只需要找到一种方法,让值越变越快就行了。
回顾一下 getTweenValue ,输入一个[0, 1]的值,输出值在[from , to]。虽然越变越快,但是起始和终点没有变化。所以,我们就只能改变 t 了。
t 不是流逝时间的百分比吗?怎么变呢?
想一下,我们刚才把时间映射到了[0, 1]。是线性的。能不能不是线性,还在[0, 1]之间?
对吧,还在[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
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();
