虽然我写的是系列文章,但每个章节单独食用是木问题的,所以,请放心大胆的看????。
先来看看在 canvas 库中调用动画的一般方式吧,比如我们要让一个矩形动起来,大体是下面这样的用法:
rect.animate(
{ top: 50, left: 400, angle: 45 }, // 要动画的属性
{ duration: 1000, onChange: canvas.renderAll.bind(canvas) } // 动画执行时间和手动渲染
);
代码浅显易懂,然后我们来想想动画的本质是什么,为什么我们能够看到动画效果呢?这个大家应该都有所了解,不就是画布重新绘制了吗,只要重绘的足够多足够快,根据人的视觉残留效应,就形成了动画。
没错,大体就是这个原因,但我们可以更具体一点,想想画布为什么要重新绘制呢?不就是因为画布中某个物体的某个值改变了,所以我们才要更新一下画面,以此来表示它动了。这个物体状态值的改变才是动画的根本原因????。
比如一个物体要花 1s 的时间从 left=100 的地方移动到 left=200 的地方,只要我不断修改 left 值,然后不断 renderAll 就能看到物体从左往右移动了。这很好理解,但是有个新问题出现了,它应该怎样移动呢?匀速、加速还是减速?又或者是其他方式呢?其实都可以,具体要看你希望这个 left 怎么变,以怎样的规律变化。
既然动画的本质就是值的改变,那这个值的改变和哪些因素有关呢?根据刚才的例子我们可以知道大概有以下四个因素:
startValue
endValue
duration
easing
(一个熟悉的单词出现了)显然动画也是一个通用的东西,所以我们把它写在 Util 工具类里,代码不多,直接食用就行:
interface IAnimationOption {
/** 初始值 */
startValue?: number;
/** 最终值 */
endValue?: number;
/** 执行时间 */
duration?: number;
/** 缓动函数 */
easing?: Function;
/** 动画一开始的回调 */
onStart?: Function;
/** 属性值改变都会进行的回调 */
onChange?: Function;
/** 属性值变化完成进行的回调 */
onComplete?: Function;
}
class Util {
static animate(options: IAnimationOption) {
window.requestAnimationFrame((timestamp: number) => { // requestAnimationFrame 会有个默认参数 timestamp,单位毫秒,表示开始去执行回调函数的时刻
// 初始化一些变量
let start = timestamp || +new Date(), // 开始时间
duration = options.duration || 500, // 动画时间
finish = start + duration, // 结束时间
time, // 当前时间
onChange = options.onChange || (() => {}), // 值改变进行的回调
easing = options.easing || ((t, b, c, d) => -c * Math.cos((t / d) * (Math.PI / 2)) + c + b), // 缓动函数,不用管名字,简单理解为一个普通函数即可,它会返回一个数值
startValue = options.startValue || 0, // 初始值
endValue = options.endValue || 100, // 结束值
byValue = options.byValue || endValue - startValue; // 值的变化范围
function tick(ticktime: number) { // tick 的主要任务就是根据当前时间更新值
time = ticktime || +new Date();
let currentTime = time > finish ? duration : time - start; // 当前已经执行了多久时间(介于0~duration)
onChange(easing(currentTime, startValue, byValue, duration)); // 根据当前时间和 easing 函数算出当前的动画值是多少,easing 理解成一个普通函数就行,它会返回一个值,就像这样:curVal = f(x) = easing(currentTime)
if (time > finish) { // 动画结束
options.onComplete && options.onComplete(); // 动画完成的回调
return;
}
window.requestAnimationFrame(tick); // 循环调用 tick,不断更新值,从而形成了动画
}
options.onStart && options.onStart(); // 动画开始前的回调
tick(start); // 开始动画
});
}
}
相信上面的注释应该解释的清清楚楚、明明白白。不过还是要着重讲下其中的两个点:
当然我们肯定不能直接傻傻的像下面这样调用:
// 假设要从左到右运动
let left = 100;
function tick() {
left++; // 更新值
window.requestAnimationFrame(tick);
}
tick();
因为每个屏幕刷新频率不一样,如果像上面这样写,在有的电脑上就会快一些,有的电脑上就会慢一些,不仅如此在页面切换到后台的时候帧率也会降低,就会导致各种问题,这显然不是我们期望的。
所以要怎么做呢?
我们应该是以时间为维度来播放动画,因为时间对我们来说流逝的速度是一样的,所以在动画一开始的时候需要记录下开始时间 start
,之后动画播放到哪里都会以这个开始时间为基准,回头看看刚才代码中计算当前动画执行了多长时间的方式:
let currentTime = time > finish ? duration : time - start;
就是以 start
为基准的,这点很重要。
第二点是关于 easing 函数,虽然好像接触过,但还是会有很多同学对此感到疑惑,所以接下来我会专门讲下这方面的内容,比如:这个函数是干嘛的、是怎么推导的、最终又是得到什么结果、和我们平时说的缓动函数是一个东西吗等等之类的。
在讲解 onChange(easing(currentTime, startValue, byValue, duration)) 这个东西之前,我们先来看看如何让每个物体都具有动画的方法,就是在物体基类中扩展就行了,瞟一眼就行:
class FabricObject { // 物体基类
_animate(property, to, options: IAnimationOption = {}) { // 某个属性要变化到哪里
options = Util.clone(options);
let currentValue = this.get(property); // 获取初始值
if (!options.from) options.from = currentValue; // 一般不传初始值的话就默认取当前属性值
Util.animate({
startValue: options.from,
endValue: to,
easing: options.easing, // 决定了值如何变化,常用的就缓动和弹动
duration: options.duration,
onChange: (value) => { // value 是 easing 函数的返回值,本质就是值的计算,value = easing()
this.set(property, value); // 重新设置属性值
options.onChange && options.onChange(); // 值改变之后,调用 onChange 回调就会重新渲染画布,数据和视图分开的优点又体现了出来
},
onComplete: () => {
this.setCoords(); // 更新物体自身的一些坐标值等
options.onComplete && options.onComplete(); // 动画结束的回调
},
});
}
}
然后再强调一下,动画的核心就是值的变化,Util.animate
中的 easing
函数其实就是计算动画播放到 (0, duration)
中间某一时刻的值是多少,仅此而已。再来简单说下 easing 函数吧,一般可以叫它缓动函数。
它是首先是一个函数,并且会返回一个数值,类似于 y = f(x)
,在我们的例子中就是 value = easing(time, beginValue, changeValue, duration)。这个函数有四个参数(当前时间、初始值、变化量 = 结束值-初始值、动画时间),返回的是当前时间点所对应的值 value,显然后面三个参数是已知的,也是固定的,唯一会变化的就是当前时间,它的取值范围就是从 0 到 duration。
执行动画的时候其实就是改变这个当前时间,根据当前时间我们代入 easing 函数就能够得到对应的 value 值。
可能有同学还是不懂这个缓动函数,其实是因为被上面的公式唬住了,公式都是推导之后的简便写法,直接去看式子是很难理解的,单凭公式在脑海中想象出动画效果也不太现实,所以这里给大家简单推导一下这种式子怎么来的,以最简单的匀速运动为例子,看看下面这张图:
上面这个过程很显然,也不用怎么推导,下面我们来看另一个更加通用的例子,首先随便拿一个函数 y = x * x
(其他的也行),顺便简单画下函数图像:
绿色代表起点,也就是动画起始值,红色代表终点,也就是动画结束值。x 轴就是动画时间,y 轴就是当前的动画值,为了方便和统一,我们需要把时间换算成 [0, 1]
的范围,0 就是起点,1 就是终点,y 轴代表的值也是一样的道理。
然后我们的起点和终点就是(0,0)和(1,1)点
(注意:虽然xy的范围都是0到1,看起来是个正方形,但它们的单位或者说表达的意思是不一样的,不要混淆了),起点和终点是固定不变的,中间的曲线可以随便怎么画,那怎么将它写成一个缓动函数呢?
我们先看看 x 轴代表什么,x 是一个取值范围从0到1的变量,看看我们的缓动函数有啥变量呢,就一个 currentTime,但是 currentTime 的取值范围是从 [0, duration]
,所以我们需要把它映射成[0, 1]
,其实也就是把 currentTime / duration 就行,然后用 currentTime / duration 代替 x;
那 y 呢,y 根据 x 算出来的值,代表的是当前这个时间点所对应的值,也就是我们缓动函数的 value 值,它的取值范围在 [startValue, startValue + byValue]
之间,所以我们也需要将其变成[0, 1]
,所以 value 的值变成了这样(value - startValue) / byValue
,那么现在 y 值也有了,我们就可以将它们直接代入 y = x * x
这个初始公式,就像这样:
① y = x * x
代入 x、y
② (value - startValue) / byValue = (currentTime / duration) * (currentTime / duration)
整理一下
③ value = (currentTime / duration) * (currentTime / duration) * byValue + startValue
简化一下(简化英文单词而已)
④ value = (t, b, c, d) => ((t/d) * (t/d) * c + b)
这个效果其实就是 easeInQuad
先慢后快的缓入效果,其他函数也是一样的推导方式,只要你能写出来。不过即便知道了怎么推导,你也很难有个直观的效果,其实常见和常用的就那么几个,网上也有大把封装好的和演示的,有个印象就行(比如可以搜一下 Tween.js)。
当然你也可以看函数图像简单猜一下效果,具体就是看每一点的斜率,斜率越趋近于水平就越慢,斜率越趋近于竖直就越快;如果你的函数曲线中有 y 值超出了 1,就说明中间点在某一时刻会超过终点,如果有 y 值小于 0,就说明有中间点有某一时刻会小于起始点,大概是这么个意思。
缓动函数有个很大的特点,就是起点和终点位置是确定的,中间位置你可以随便算,可快可慢,可以超出终点,也可以小于起点,具体什么效果,你可以随便写个方程运行试试,然后再根据效果调试。相信你肯定见过下面这种类型的图:
现在再看看,不知道会不会感到稍微亲切一点点嘞?????
本章我们主要讲解了 canvas 中动画的实现,其中最重要的一点就是如何在不同帧率达到同样的动画效果,那就是要以时间为维度来进行度量,用 canvas 做的游戏也是一样,时间每向前 tick 一次(滴答的意思,挺形象的叫法,古老时钟的那种感觉),画布就会向前推进一次(重新绘制)。
然后再补充两个小点:
然后这里是简版 fabric.js 的代码
到此,关于“canvas动画原理、实现和推导是什么”的学习就结束了,希望能够解决大家的疑惑,另外大家动手实践也很重要,对大家加深理解和学习很有帮助。如果想要学习更多的相关知识,欢迎关注群英网络资讯站,小编每天都会给大家分享实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
长按识别二维码并关注微信
更方便到期提醒、手机管理