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 相关文章推荐
jQuery数据显示插件整合实现代码
Oct 24 Javascript
js跨浏览器实现将字符串转化为xml对象的方法
Sep 25 Javascript
js实现iPhone界面风格的单选框和复选框按钮实例
Aug 18 Javascript
Node.js中JavaScript操作MySQL的常用方法整理
Mar 01 Javascript
angular实现form验证实例代码
Jan 17 Javascript
JavaScript Canvas绘制圆形时钟效果
Aug 20 Javascript
使用Math.max,Math.min获取数组中的最值实例
Apr 25 Javascript
bootstrap table表格插件使用详解
May 08 Javascript
Angularjs过滤器实现动态搜索与排序功能示例
Dec 13 Javascript
VS Code转换大小写、修改选中文字或代码颜色的方法
Dec 15 Javascript
vue插件draggable实现拖拽移动图片顺序
Dec 01 Javascript
使用Taro实现小程序商城的购物车功能模块的实例代码
Jun 05 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
是 WordPress 让 PHP 更流行了 而不是框架
2016/02/03 PHP
HTML-CSS群中单选引发的“事件”
2007/03/05 Javascript
JavaScript 验证浏览器是否支持javascript的方法小结
2009/05/17 Javascript
拖动布局之保存布局页面cookies篇
2010/10/29 Javascript
基于jQuery的动态增删改查表格信息,可左键/右键提示(原创自Zjmainstay)
2012/07/31 Javascript
2014年最火的Node.JS后端框架推荐
2014/10/27 Javascript
Javascript基础教程之JavaScript语法
2015/01/18 Javascript
在AngularJS应用中实现一些动画效果的代码
2015/06/18 Javascript
JavaScript中this详解
2015/09/01 Javascript
jQuery实现浮动层随浏览器滚动条滚动的方法
2015/09/22 Javascript
使用Javascript写的2048小游戏
2015/11/25 Javascript
jQuery 获取遍历获取table中每一个tr中的第一个td的方法
2016/10/05 Javascript
详解webpack 打包文件体积过大解决方案(code splitting)
2018/04/10 Javascript
layer.confirm取消按钮绑定事件的方法
2018/08/17 Javascript
使用vue根据状态添加列表数据和删除列表数据的实例
2018/09/29 Javascript
微信小程序实现多选功能
2018/11/04 Javascript
JavaScript之数组扁平化详解
2019/06/03 Javascript
Vue+ElementUI table实现表格分页
2019/12/14 Javascript
vuex分模块后,实现获取state的值
2020/07/26 Javascript
Python基本数据类型详细介绍
2014/03/11 Python
Python pickle模块用法实例
2015/04/14 Python
详解Python中的array数组模块相关使用
2016/07/05 Python
python jieba分词并统计词频后输出结果到Excel和txt文档方法
2018/02/11 Python
Python简单读写Xls格式文档的方法示例
2018/08/17 Python
Python定时发送天气预报邮件代码实例
2019/09/09 Python
numpy.ndarray 实现对特定行或列取值
2019/12/05 Python
使用pyinstaller逆向.pyc文件
2019/12/20 Python
python统计函数库scipy.stats的用法解析
2020/02/25 Python
关键字final的用法
2013/10/02 面试题
学校运动会广播稿100条
2014/09/14 职场文书
2014企业领导班子四风对照检查材料思想汇报
2014/09/17 职场文书
免职证明样本
2014/10/23 职场文书
党员民主生活会材料
2014/12/15 职场文书
《圆的面积》教学反思
2016/02/19 职场文书
Java 在线考试云平台的实现
2021/11/23 Java/Android
Go语言特点及基本数据类型使用详解
2022/03/21 Golang