美文网首页
JavaScript中动画与特效的实现原理

JavaScript中动画与特效的实现原理

作者: 深漂浪子 | 来源:发表于2019-06-03 15:49 被阅读0次

    一、JavaScript中的动画原理

    动画效果可以通过两种方式来实现:一种是通过JavaScript间接的操作css,每隔几秒执行一次,另外一种是利用纯css实现,该方法在css3成熟后广泛应用.这里主要将js里面的动画:

    JavaScript动画用的最多的是setInterval()、setTimeout()和requestAnimationFrame():
    1 setTimeout()和setInterval ()主要是自身会执行动画效果,它们在里面放入function和时间参数,然后既可以设置事件;
    2 requestAnimationFrame(回调函数):像setTimeout、setInterval一样,requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据自己的频率进行一次重绘,它接收一个回调函数作为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间作为参数。由于requestAnimationFrame的功效只是一次性的,所以若想达到动画效果,则必须连续不断的调用requestAnimationFrame,就像我们使用setTimeout来实现动画所做的那样。
    requestAnimationFrame函数会返回一个资源标识符,可以把它作为参数传入cancelAnimationFrame函数来取消.requestAnimationFrame的回调,跟setTimeout的clearTimeout很相似。 可以这么说,requestAnimationFrame其实就是setTimeout的性能增强版。
    javascript 代码

        <button  id="btn">清除</button>
            var id;
            var time = new Date();
    
            requestAnimationFrame(function step(){
                console.log(new Date() - time);
                time = new Date();
                id = requestAnimationFrame(step);
            });
            btn.onclick = function (){
                cancelAnimationFrame(id )
            }
    

    1.3简单动画的问题
    1.3.1 setTimeout和setInterval的深入理解
    setTimeout():如下面这段代码,输出结果其实是1 2 3,而不是 1 3 2,因为setTimeout()其实在执行的时候会先存储这个结果但不会立即输出(即使时间间隔是0也这样)而是等待页面加载完成后再输出结果
    javascript 代码

    console.log("1");
            setTimeout(function(){
              console.log("3")
             },0);
             console.log("2");
            //输出结果是什么?
            //1 2 3
    
            function fn() {
            setTimeout(function(){
                console.log(&#39;can you see me?&#39;);
            },1000);
            while(true) {}
            }
            //输出结果是什么?  不输出结果
    

    1.3.2 简单动画的变慢问题
    当setTimeout、setInterval甚至是requestAnimationFrame()在循环里面要做很长的处理时,就会出现动画时间变慢的结果,使它本该在固定时间内结束而结果却是不尽人意的延迟
    实例1:让滑块自动向右移动
    javascript 代码

      var left = 0;
            var id = setInterval(function (){
            left += 10;
            box.style.left = left + "px";
            if(left >= 1000){
                clearInterval(id);
            }
            for(var i = 0; i < 10000; i++){
                console.log("a");
            }
            }, 20)
    

    实例2:

    javascript 代码

     function step() {
                var temp = div.offsetLeft + 2;
                div.style.left = temp + "px";
                window.requestAnimationFrame(step);
                for (var i = 0; i < 50000; i++) {
                console.log("再牛逼的定时器也得等到我执行完才能执行")
                }
            }
            window.requestAnimationFrame(step);
    

    1.4 使用动画的正确姿势
    其实是 “位移”关于“时间”的函数:s=f(t)
    动画变慢的结果其实是采用增量的方式来执行了动画,为了更精确的控制动画,更合适的方法是将动画与时间关联起来
    javascript 代码

     var box = document.querySelector("div");
            var dis = 1000;
            var duration = 5000;
            var statTime = new Date();
    
            requestAnimationFrame(function step(){
                var time = new Date() - statTime;
                time = time >= duration ? duration : time;
                box.style.left = dis * (time / duration) + "px";
    
                if(time >= duration){
                    return;
                }
                requestAnimationFrame(step)
                for(var i = 0; i < 100000; i++) console.log("a");
            })
    

    动画通常情况下有终止时间,如果是循环动画,我们也可以看做特殊的——当动画达到终止时间之后,重新开始动画。因此,我们可以将动画时间归一(Normalize)表示:
    javascript 代码

     //duration 是动画执行时间   isLoop是否为循环执行。
            function startAnimation(duration, isLoop){
              var startTime = Date.now();
    
              requestAnimationFrame(function change(){
                // 动画已经用去的时间占总时间的比值
                var p = (Date.now() - startTime) / duration;
    
                if(p >= 1.0){
                  if(isLoop){ // 如果是循环执行,则开启下一个循环周期。并且把开始时间改成上个周期的结束时间
                    startTime += duration;
                    p -= 1.0; //动画进度初始化
                  }else{
                    p = 1.0;    //如果不是循环,则把时间进度至为 1.0 表示动画执行结束
                  }
                }
                console.log("动画已执行进度", p);
                if(p < 1.0){ //如果小于1.0表示动画还诶有值完毕,继续执行动画。
                  requestAnimationFrame(change);
                }
              });
            }
    

    示例1:用时间控制动画周期精确到2s中

    javascript 代码

    block.addEventListener("click", function() {
              var self = this,
                  startTime = Date.now(),
                  duration = 2000;
              setInterval(function() {
                var p = (Date.now() - startTime) / duration;
                // 时间已经完成了2000的比例,则360度也是进行了这么个比例。
                self.style.transform = "rotate(" + (360 * p) + "deg)";
              }, 100);
            });
    

    示例2:让滑块在2秒内向右匀速移动600px

    javascript 代码

     block.addEventListener("click", function(){
              var self = this, 
                  startTime = Date.now(),
                  distance = 600, 
                  duration = 2000;
    
              requestAnimationFrame(function step(){
                var p = Math.min(1.0, (Date.now() - startTime) / duration);
                self.style.transform = "translateX(" + (distance * p) +"px)";
                if(p < 1.0) {
                  requestAnimationFrame(step);
                }
              });
            });
    
    ss.png

    二、常见动画效果实现

    2.1 匀速水平运动
    用时间来控制进度 s=S∗p
    2.2 匀加速(减速)运动
    1)加速度恒定,速度从0开始随时间增加而均匀增加。
    2)匀加速公式:大写S:要移动的总距离 p:归一化的时间进度 s=S∗p*p
    javascript 代码

    // 2s中内匀加速运动2000px
                block.addEventListener("click", function() {
                        var self = this,
                            startTime = Date.now(),
                            distance = 1000,
                            duration = 2000;
    
                        requestAnimationFrame(function step() {
                            var p = Math.min(1.0, (Date.now() - startTime) / duration);
                            self.style.transform = "translateX(" + (distance * p * p) + "px)";
                            if(p < 1.0) requestAnimationFrame(step);
                        });
                    });
    

    3)匀减速运动公式:s=S∗p∗(2−p)
    javascript 代码

    //2s中使用速度从最大匀减速到0运动1000px
            block.addEventListener("click", function(){
              var self = this, startTime = Date.now(),
                  distance = 1000, duration = 2000;
    
              requestAnimationFrame(function step(){
                var p = Math.min(1.0, (Date.now() - startTime) / duration);
                self.style.transform = "translateX(" 
                  + (distance * p * (2-p)) +"px)";
                if(p < 1.0) requestAnimationFrame(step);
              });
            });
    

    2.3 水平抛物运动
    匀速水平运动和自由落体运动的组合。
    javascript 代码

    block.addEventListener("click", function(){
                  var self = this, startTime = Date.now(),
                      disX = 1000, disY = 1000, 
                      duration = Math.sqrt(2 * disY / 10 / 9.8) * 1000;   // 落到地面需要的时间  单位ms
                    //假设10px是1米,disY = 100米
    
                  requestAnimationFrame(function step(){
                    var p = Math.min(1.0, (Date.now() - startTime) / duration);
                    var tx = disX * p;  //水平方向是匀速运动
                    var ty = disY * p * p;  //垂直方向是匀加速运动
    
                    self.style.transform = "translate(" 
                      + tx + "px" + "," + ty +"px)";
                    if(p < 1.0) requestAnimationFrame(step);
                  });
                });
    

    2.3 2.4 正弦曲线运动
    正弦运动:x方向匀速,垂直方向是时间t的正弦函数
    javascript 代码

    block.addEventListener("click", function(){
                  var self = this, startTime = Date.now(),
                      distance = 800,
                      duration = 2000;
    
                  requestAnimationFrame(function step(){
                    var p = Math.min(1.0, (Date.now() - startTime) / duration);
                    var ty = distance * Math.sin(2 * Math.PI * p);
                    var tx = 2 * distance * p;
                    self.style.transform = "translate("+ tx + "px," + ty + "px)";
                    if(p < 1.0) requestAnimationFrame(step);
                  });
                });
    

    2.5 圆周运动
    圆周运动公式:x=R.sin(2∗π∗p),y=R.cos(2∗π∗p)
    javascript 代码

    block.addEventListener("click", function() {
                  var self = this,
                      startTime = Date.now(),
                      r = 100,
                      duration = 2000;
    
                  requestAnimationFrame(function step() {
                    var p = Math.min(1.0, (Date.now() - startTime) / duration);
                    var tx = r * Math.sin(2 * Math.PI * p),
                        ty = -r * Math.cos(2 * Math.PI * p);
    
                    self.style.transform = "translate(" +
                      tx + "px," + ty + "px)";
                    requestAnimationFrame(step);
                  });
                });
    

    三、动画算子(easing)

    对于一些比较复杂的变化,算法也比较复杂,就要用到动画算子。动画算子 是一个函数,可以把进度转化成另外一个值。其实也就是一种算法。
    我们总结一下上面的各类动画,发现它们是非常相似的,匀速运动、匀加速运动、匀减速运动、圆周运动唯一的区别仅仅在于位移方程:
    1.匀速运动:s=S∗p
    2.匀加速运动:s=S∗pp
    3.匀减速运动:s=S∗p∗(2−p)
    4.圆周运动x轴:x=R∗sin(2∗PI∗p)
    5.圆周运动y轴:y=R∗cos(2∗PI∗p)
    我们把共同的部分 S 或R 去掉,得到一个关于 p 的方程 ,这个方程我们称为
    动画的算子(easing),它决定了动画的性质。
    1.匀速算子:e=p
    2.匀加速算子:e = p
    p=p^2
    3.匀减速算子:e=p∗(2−p)
    4.圆周算子x轴:e=sin(2∗PI∗p)
    5.圆周算子y轴:e=cos(2∗PI∗p)

    一些常用的动画算子
    javascript 代码

    //easing.js库封装
            var pow = Math.pow,
                BACK_CONST = 1.70158;
            // t指的的是动画进度 归一化的时间  前面的p
            Easing = {
            // 匀速运动
                linear: function (t){
                    return t;
                },
            // 匀加速运动
                easeIn: function (t){
                    return t * t;
                },
            // 减速运动
                easeOut: function (t){
                    return (2 - t) * t;
                },
            //先加速后减速
                easeBoth: function (t){
                    return (t *= 2) < 1 ? .5 * t * t : .5 * (1 - (--t) * (t - 2));
                },
            // 4次方加速
                easeInStrong: function (t){
                    return t * t * t * t;
                },
            // 4次方法的减速
                easeOutStrong: function (t){
                    return 1 - (--t) * t * t * t;
                },
            // 先加速后减速,加速和减速的都比较剧烈
                easeBothStrong: function (t){
                    return (t *= 2) < 1 ? .5 * t * t * t * t : .5 * (2 - (t -= 2) * t * t * t);
                },
            //
                easeOutQuart: function (t){
                    return -(Math.pow((t - 1), 4) - 1)
                },
            // 指数变化 加减速
                easeInOutExpo: function (t){
                    if (t === 0) return 0;
                    if (t === 1) return 1;
                    if ((t /= 0.5) < 1) return 0.5 * Math.pow(2, 10 * (t - 1));
                    return 0.5 * (-Math.pow(2, -10 * --t) + 2);
                },
            //指数式减速
                easeOutExpo: function (t){
                    return (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1;
                },
            // 先回弹,再加速
                swingFrom: function (t){
                    return t * t * ((BACK_CONST + 1) * t - BACK_CONST);
                },
    
            // 多走一段,再慢慢的回弹
                swingTo: function (t){
                    return (t -= 1) * t * ((BACK_CONST + 1) * t + BACK_CONST) + 1;
                },
    
            //弹跳
                bounce: function (t){
                    var s = 7.5625,
                        r;
    
                    if (t < (1 / 2.75)){
                        r = s * t * t;
                    }else if (t < (2 / 2.75)){
                        r = s * (t -= (1.5 / 2.75)) * t + .75;
                    }else if (t < (2.5 / 2.75)){
                        r = s * (t -= (2.25 / 2.75)) * t + .9375;
                    }else{
                        r = s * (t -= (2.625 / 2.75)) * t + .984375;
                    }
    
                    return r;
                }
            };
    

    四、使用面向对象封装动画

    为了实现更加复杂的动画,我们可以将动画进行 简易 的封装,要进行封装,我们先要抽象出动画相关的要素:

    动画生命周期:开始、进程中、结束
    javascript 代码

    /*my_animator01.js框架
              参数1:动画的执行时间
              参数2:动画算子. 如果没有传入动画算子,则默认使用匀速算子
              参数3:动画执行的时候的回调函数(动画执行的要干的事情)
             */
            function Animator(duration,  easing,doSomething) {
                 this.duration = duration;
                this.easing = easing;
                this.doSomething = doSomething;
            }
            Animator.prototype = {
                /*开始动画的方法,
                 参数:一个布尔值
                 true表示动画不循环执行。
                */
                start: function (count){ // 参数表示动画播放的周期的个数
            if(count  0){//如果还有周期未结束则继续执行
                        startTime = new Date();
                    }else{
                        return;//完成,动画结束
                    }
                }
                self.id = requestAnimationFrame(step);//执行下一帧动画
            });
        },
            stop:function () {//动画结束
                cancelAnimationFrame(this.id);
            }
        }
    

    初步修改版:
    javascript 代码

    function Animator(durations, easings, callbacks){
            this._init(durations, easings, callbacks);
        }
    
        Animator.prototype = {
            _init: function (durations, easings, callbacks){
                this.durations = durations;
                this.easings = easings;
                this.callbacks = callbacks;
            },
            start: function (count){
                if (!count || typeof count != "number" || count  0){
                                startTime = new Date();
                                index = 0;
                            }else{
                                return;
                            }
                        }
                    }
                    that.id = requestAnimationFrame(step);
                });
            },
            stop: function (){
                cancelAnimationFrame(this.id);
            }
        }
    

    终极版:
    javascript 代码

    function  Animator(opts){
            this._init(opts);
        }
        Animator.prototype={
            _init:function (opts) {
              this.opts=opts;
            },
            start:function (count) {
                if(!count||count<=0 || typeof count!="number") return;
                var opts=this.opts;
                var startTime=new Date();
                var index=0,
                    that=this;
               this.id=requestAnimationFrame(function step() {
                    var p=Math.min(1,(new Date()-startTime)/opts[index].duration);
                    var e=opts[index].easing(p);
                        opts[index].callback(e);
                    if(p == 1){
                        index++;
                        if(index<opts.length){
                            startTime = new Date();
                        }else{
                            count--;
                            if(count>0){
                                startTime = new Date();
                                index=0;
                            }else {
                                return;
                            }
                        }
                    }
                    that.id = requestAnimationFrame(step);
                });
            },
            stop:function () {
                cancelAnimationFrame(this.id);
            }
        }
    

    五、逐帧动画

    有时候,我们不但要支持元素的运动,还需要改变元素的外观,比如飞翔的小鸟需要扇动翅膀,这类动画我们可以用逐帧动画来实现:
    html 代码

    <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Title</title>
            <style>
                div{
                    width: 250px;
                    height: 142px;
                    overflow: hidden;
                }
            </style>
        </head>
        <body>
        <div><img src="bird.png" alt=""></div>
        <script src="easing.js"></script>
        <script src="my_animator03.js"></script>
        <script>
            var img=document.querySelector("img");
            var x=0;//初始化img位置
            var y=0;
            //原地的移动
            setInterval(function () {
                img.style.transform="translate("+(-244*(x%4))+"px,"+(-146*(y%2))+"px)";
                x++;//先从上面图片走,走完在从下面回来
                if(x%4==0){
                    y++;
                }
            },100);
        /*
            var animator=new Animator(5000,Easing.linear,function (e) {
                img.parentNode.style.transform="translate("+document.body.offsetWidth*e+"px,"+(-142*(y%2))+"px)";
            })
            animator.start(Number.POSITIVE_INFINITY);*/
    
            //水平垂直的移动
            new Animator([{
                duration:5000,
                easing:Easing.linear,
                callback:function (e) {
                    img.parentNode.style.transform=
                        "translate("+(document.body.offsetWidth-200)*e+"px,"+600*e+"px)" +
                        " rotate(0deg)";
                }
            },
            {
                duration:5000,
                easing:Easing.linear,
                callback:function (e) {
                    img.parentNode.style.transform=
                        "translate("+(document.body.offsetWidth-200)*(1-e)+"px,"+600*(1-e)+"px)" +
                        " rotateY(180deg)";//到达右下角反转
                }
            }]).start(Number.POSITIVE_INFINITY)
    
        /*var x=0;
        setInterval(function () {
            x+=10;
            img.parentNode.style.transform="translate("+document.body.offsetWidth*e+"px,"+(-142*(y%2))+"px)";
        },20)*/
        </script>
        </body>
        </html>
    

    相关文章

      网友评论

          本文标题:JavaScript中动画与特效的实现原理

          本文链接:https://www.haomeiwen.com/subject/inbcxctx.html