JS前端可视化canvas动画原理及其推导实现


Posted in Javascript onAugust 05, 2022

前言

到目前为止我们的 fabric.js 雏形已经有了,麻雀虽小五脏俱全,我们不仅能够在画布上自由的添加物体,同时还实现了点选和框选,并且能够对它们做一些变换,不过只有变换这个操作还不够灵活,要是能够让物体动起来就好了,于是就引入了这个章节的主题:动画,以及动画最核心的一个问题,如何保证在不同的电脑上达到同样的动画效果?然后说干就干,立马开撸?。

虽然我写的是系列文章,但每个章节单独食用是木问题的,所以,请放心大胆的看?。

动画的本质

先来看看在 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); // 开始动画
        });
    }
}

相信上面的注释应该解释的清清楚楚、明明白白。不过还是要着重讲下其中的两个点:

  • 一个是为什么使用 requestAnimationFrame 这个 api 来完成动画,这应该也是个老生常谈的问题了,因为 setInterval 和 setTimeout 不准,很容易出问题,比如执行时机不准确、切换页面回来会堆积执行、不流畅等,并且它们也不是专门为动画而生(当然如果你不习惯用 requestAnimationFrame 也可以直接把它换成 setTimeout,方便自己理解);
  • 而 requestAnimationFrame 是按帧率刷新的,跟着帧率走的期间我们就可以不用做很多无用功,能够更好的知道绘制下一帧的最佳时机,也比较流畅。它们的一个最主要的区别就是:
  • setInterval 和 setTimeout 是主动告诉浏览器什么时候去绘制;
  • 而 requestAnimationFrame 则是浏览器在它觉得可以绘制下一帧的时候通知我们(你品,你细品,就有那味了)。

当然我们肯定不能直接傻傻的像下面这样调用??:

// 假设要从左到右运动
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 值。

可能有同学还是不懂这个缓动函数,其实是因为被上面的公式唬住了,公式都是推导之后的简便写法,直接去看式子是很难理解的,单凭公式在脑海中想象出动画效果也不太现实,所以这里给大家简单推导一下这种式子怎么来的,以最简单的匀速运动为例子,看看下面这张图??:

JS前端可视化canvas动画原理及其推导实现

上面这个过程很显然,也不用怎么推导,下面我们来看另一个更加通用的例子,首先随便拿一个函数 y = x * x(其他的也行),顺便简单画下函数图像??:

JS前端可视化canvas动画原理及其推导实现

绿色代表起点,也就是动画起始值,红色代表终点,也就是动画结束值。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,就说明有中间点有某一时刻会小于起始点,大概是这么个意思?。

缓动函数有个很大的特点,就是起点和终点位置是确定的,中间位置你可以随便算,可快可慢,可以超出终点,也可以小于起点,具体什么效果,你可以随便写个方程运行试试,然后再根据效果调试。相信你肯定见过下面这种类型的图:

JS前端可视化canvas动画原理及其推导实现

现在再看看,不知道会不会感到稍微亲切一点点嘞??

小结

本章我们主要讲解了 canvas 中动画的实现,其中最重要的一点就是如何在不同帧率达到同样的动画效果,那就是要以时间为维度来进行度量,用 canvas 做的游戏也是一样,时间每向前 tick 一次(滴答的意思,挺形象的叫法,古老时钟的那种感觉),画布就会向前推进一次(重新绘制)。

然后再补充两个小点:

  • 通常情况下动画的发生总是伴随着画布的重新绘制,但是默认情况下 fabric.js 并不会自动帮我们重新绘制,需要我们手动调用(可以看看开篇代码中 onChange 的回调是咋写的),这是因为如果画布中有很多物体在运动,默认自动重新绘制的话会降低性能。
  • 动画不仅仅可以作用于位置,还可以作用于各种属性,比如透明度、颜色等,其实只要是个数值就能够进行动画。并且归功于我们之前将数据和视图分离的架构,这个章节所做的一切也仅仅是改变数据而已,并不涉及画布绘制的内容。

然后这里是简版 fabric.js 的代码

以上就是JS前端可视化canvas动画原理及其推导实现的详细内容,更多关于JS前端可视化canvas动画的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
新浪的图片新闻效果
Jan 13 Javascript
jQuery表格行换色的三种实现方法
Jun 27 Javascript
基于MVC3方式实现下拉列表联动(JQuery)
Sep 02 Javascript
使用mouse事件实现简单的鼠标经过特效
Jan 30 Javascript
javascript中alert()与console.log()的区别
Aug 26 Javascript
JavaScript继承模式粗探
Jan 12 Javascript
关于jQuery EasyUI 中刷新Tab选项卡后一个页面变形的解决方法
Mar 02 Javascript
vue Render中slots的使用的实例代码
Jul 19 Javascript
jQuery实现浏览器之间跳转并传递参数功能【支持中文字符】
Mar 28 jQuery
Vue 递归多级菜单的实例代码
May 05 Javascript
Node.js控制台彩色输出的方法与原理实例详解
Dec 01 Javascript
如何在vue中使用jointjs过程解析
May 29 Javascript
JS前端使用canvas实现扩展物体类和事件派发
Aug 05 #Javascript
JS前端canvas交互实现拖拽旋转及缩放示例
Aug 05 #Javascript
canvas 中如何实现物体的框选
Aug 05 #Javascript
JS前端使用canvas实现物体的点选示例
Aug 05 #Javascript
前端canvas中物体边框和控制点的实现示例
Aug 05 #Javascript
JS前端轻量fabric.js系列物体基类
Aug 05 #Javascript
JS前端轻量fabric.js系列之画布初始化
Aug 05 #Javascript
You might like
一个简单计数器的源代码
2006/10/09 PHP
使用apache模块rewrite_module (转)
2007/02/14 PHP
Bootstrap+PHP实现多图上传功能实例详解
2018/04/08 PHP
offsetHeight在OnLoad中获取为0的现象
2013/07/22 Javascript
Javascript四舍五入Math.round()与Math.pow()使用介绍
2013/12/27 Javascript
JavaScript模块随意拖动示例代码
2014/05/27 Javascript
一个支持任意尺寸的图片上下左右滑动效果
2014/08/24 Javascript
js实现基于正则表达式的轻量提示插件
2015/08/29 Javascript
快速学习JavaScript的6个思维技巧
2015/10/13 Javascript
Bootstrap每天必学之警告框插件
2016/04/26 Javascript
jQuery+Pdo编写login登陆界面
2016/08/01 Javascript
JavaScript实现图片瀑布流和底部刷新
2017/01/02 Javascript
javascript兼容性(实例讲解)
2017/08/15 Javascript
JavaScript获取移动设备型号的实现代码(JS获取手机型号和系统)
2018/03/10 Javascript
react项目实践之webpack-dev-serve
2018/09/14 Javascript
OpenLayer3自定义测量控件MeasureTool
2020/09/28 Javascript
[08:04]TI4西雅图DOTA2前线报道 海涛探访各路人马
2014/07/09 DOTA
查看Python安装路径以及安装包路径小技巧
2015/04/28 Python
Python备份目录及目录下的全部内容的实现方法
2016/06/12 Python
基于Python中capitalize()与title()的区别详解
2017/12/09 Python
Python针对给定字符串求解所有子序列是否为回文序列的方法
2018/04/21 Python
解决django同步数据库的时候app models表没有成功创建的问题
2019/08/09 Python
在Python中字符串、列表、元组、字典之间的相互转换
2019/11/15 Python
Python super()函数使用及多重继承
2020/05/06 Python
python 爬取腾讯视频评论的实现步骤
2021/02/18 Python
LN-CC中国:高端男装和女装的奢侈时尚目的地
2019/09/14 全球购物
斯洛伐克电子产品购物网站:DATART
2020/04/05 全球购物
2014庆六一活动方案
2014/03/02 职场文书
共产党员公开承诺书
2014/03/25 职场文书
实习单位鉴定评语
2014/04/26 职场文书
行政文员实习自我鉴定范文
2014/09/14 职场文书
死亡赔偿协议书
2015/01/28 职场文书
优秀党员先进事迹材料2016
2016/02/29 职场文书
读《钢铁是怎样炼成的》有感:百炼方成钢
2019/11/05 职场文书
MySql 8.0及对应驱动包匹配的注意点说明
2021/06/23 MySQL
HTML5页面打开微信小程序功能实现
2022/09/23 HTML / CSS