JavaScript

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > JS前端可视化canvas动画

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

作者:尤水就下

这篇文章主要为大家介绍了JS前端可视化canvas动画原理及其推导实现,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

到目前为止我们的 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 怎么变,以怎样的规律变化。

动画的实现

既然动画的本质就是值的改变,那这个值的改变和哪些因素有关呢?根据刚才的例子我们可以知道大概有以下四个因素:

显然动画也是一个通用的东西,所以我们把它写在 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 的代码

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

您可能感兴趣的文章:
阅读全文