js动画

看了很多视频、文章,最后却通通忘记了,别人的知识依旧是别人的,自己却什么都没获得。此系列文章旨在加深自己的印象,因此此系列文章大都是将别人的文章连复制带写而来,若有侵权,请及时通知,必定立即删除。

其实Web动画的实现原理跟早期的运动影片很类似,都是通过将一张张的赛璐珞片以较快速度播放,从而模拟出连贯的物体运动。而这一张张的赛璐珞片就类似于投影运动媒体的帧的概念,而几乎所有投影运动媒体都是通过帧来实现的

几乎所有的程序动画都会表现为某种形式的循环,我们会创建一个展现一系列图像的流程图以实现逐帧动画,其中每一帧只需要绘制出来即可。

为了实现动画,需要为每一帧执行以下操作:

  1. 执行该帧所要调用到的代码;

  2. 将所有对象绘制到出来;

  3. 重复这一过程渲染下一帧

下面主要讨论一下JavaScript中动画循环函数 —— setTimeout()、setInterval()和requestAnimationFrame(),他们的对应取消循环函数分别是clearTimeout()、clearInterval()和cancelAnimationFrame()

setTimeout实现循环动画的原理:

(function drawFrame() {
    var timer = null;
    var delayTime = 1000 / 60;
    // 帧渲染和帧绘制 ...
    timer = setTimeout(drawFrame, delayTime);
    // 停止循环
    if( /* 停止条件成立 */ ) {
        clearTimeout(timer);
    }
})();

setInterval()

var timer = null;
var delayTime = 1000/ 60;
timer = setInterval(drawFrame, delayTime);

function drawFrame() {
    // 帧渲染和帧绘制 ...
    // 停止循环
    if( /* 动画停止条件成立 */ ) {
        clearInterval(timer);
    }
}

setInterval却没有被所调用的函数所束缚,它只是简单地每隔一定时间就重复执行一次所调用的函数。而setTimeout受所调用函数的影响,只有执行完成该次的函数调用,才能继续执行下一次的函数调用。

如果要求在每隔一个固定的时间间隔后就精确地执行某动作,那么最好使用setInterval。

如果不想由于连续调用产生互相干扰的问题,尤其是每次函数的调用需要繁重的计算以及很长的处理时间,那么最好使用setTimeout

requestAnimationFrame()的原理其实与setTimeout和setInterval类似,通过递归调用同一方法来不断更新画面以达到动画效果,但它优于setTimeout和setInterval的地方在于它是由浏览器专门为动画提供优化实现的API,并且充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。

不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。

requestAnimationFrame的语法如下:

requestAnimationFrame(callback) //callback为回调函数

requestAnimationFrame动画的实现原理与setTimeout类似,都是使用一个回调函数作为参数,且这个回调函数会在浏览器重绘之前调用。具体如下

(function drawFrame() {
    var timer = null;
    // 帧渲染和帧绘制 ...
    timer = requestAnimationFrame(drawFrame);
    // 停止循环
    if( /* 停止条件成立 */ ) {
        cancelAnimationFrame(timer);
    }
})();
var bind = (function(ele, eventType, callback) {
    if(ele.addEventListener) {
        // W3C标准写法
        return ele.addEventListener(eventType, callback, false);
    }else if(ele.attachEvent) {
        // 兼容IE6~8
        return ele.attachEvent(eventType, callback);
    }else {
        // 兼容IE5-
        return ele["on" + eventType] = callback;
    }
})();

var unbind = (function(ele, eventType, callback) {
    if(ele.removeEventListener) {
        // W3C标准写法
        return ele.removeEventListener(eventType, callback, false);
    }else if(ele.detachEvent) {
        // 兼容IE6~8
        return ele.detachEvent(eventType, callback);
    }else {
        // 兼容IE5-
        return ele["on" + eventType] = null;
    }
})();

常见的事件类型

鼠标事件:

onmousedown, onmouseup, onclick, ondbclick, onmousewheel, onmousemove, onmouseover, onmouseout;

触摸事件:

ontouchstart, ontouchend, ontouchmove;

键盘事件:

onkeydown, onkeyup, onkeypress;

页面相关事件:

onabort(图片在下载时被用户中断), onbeforeunload(当前页面的内容将要被改变时触发), onerror(出现错误时触发), onload(内容加载完成时触发), onmove(浏览器窗口被移动时触发), onresize(浏览器的窗口大小被改变时触发), onscroll(滚动条位置发生变化时触发), onstop(浏览器的停止按钮被按下时触发此事件或者正在下载的文件被中断时触发), onunload(当前页面将被改变时触发);

表单相关事件

onblur(元素失去焦点时触发), onchange(元素失去焦点且元素内容发生改变时触发), onfocus(元素获得焦点时触发), onreset(表单中reset属性被激活时触发), onsubmit(表单被提交时触发);oninput(在input元素内容修改后立即被触发,兼容IE9+)

编辑事件

onbeforecopy:当页面当前的被选择内容将要复制到浏览者系统的剪贴板前触发此事件;

onbeforecut:当页面中的一部分或者全部的内容将被移离当前页面[剪贴]并移动到浏览者的系统剪贴板时触发此事件;

onbeforeeditfocus:当前元素将要进入编辑状态;

onbeforepaste:内容将要从浏览者的系统剪贴板传送[粘贴]到页面中时触发此事件;

onbeforeupdate:当浏览者粘贴系统剪贴板中的内容时通知目标对象;

oncontextmenu:当浏览者按下鼠标右键出现菜单时或者通过键盘的按键触发页面菜单时触发的事件;

oncopy:当页面当前的被选择内容被复制后触发此事件;

oncut:当页面当前的被选择内容被剪切时触发此事件;

onlosecapture:当元素失去鼠标移动所形成的选择焦点时触发此事件;

onpaste:当内容被粘贴时触发此事件;

onselect:当文本内容被选择时的事件;

onselectstart:当文本内容选择将开始发生时触发的事件;

拖动事件

ondrag:当某个对象被拖动时触发此事件 [活动事件];

ondragdrop:一个外部对象被鼠标拖进当前窗口时触发;

ondragend:当鼠标拖动结束时触发此事件;

ondragenter:当对象被鼠标拖动的对象进入其容器范围内时触发此事件;

ondragleave:当对象被鼠标拖动的对象离开其容器范围内时触发此事件;

ondragover:当某被拖动的对象在另一对象容器范围内拖动时触发此事件;

ondragstart:当某对象将被拖动时触发此事件;

ondrop:在一个拖动过程中,释放鼠标键时触发此事件;

事件的常见应用

获取鼠标位置

每个鼠标事件都有两个属性用于确定鼠标当前位置:pageX和pageY。但是IE6~8不知持这两个属性,需要用到clientX和clientY。

其中,pageX和pageY的鼠标位置是相对于document文档的,而clientX和clientY的鼠标位置是相对于浏览器屏幕的。为了实现各平台统一,兼容性写法可以如下:

/ 初始化鼠标位置,这里的鼠标位置默认是相对于document文档的
var mouse = {
    x: 0,
    y: 0
}; 
function getMouse(event) {
    var event = event || window.event;
    if(event.pageX || event.pageY) {
        x = event.x;
        y = event.y;
    }else {
        var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        x = event.clientX + scrollLeft;
        y = event.clientY + scrollTop;
    }
    mouse.x = x;
    mouse.y = y;

    return mouse;
}

触摸位置

一个触摸点可以被想象成鼠标光标,不过鼠标光标会一直停留在屏幕上,而手指却会从设备上按下、移动以及释放,所以某些时刻光标会从屏幕上消失。另外,触摸屏上不存在mouseover等效的触摸事件。同一时间可能发生多点触摸,某个触摸点的信息会保存在触摸事件的一个数组中。

获取触摸位置的方法见下:

// 触摸位置声明
var touch = {
    x: null,
    y: null,
    isPress: false
}

function getTouch (event) {
    var x, y, 
    touchEvent = event.touches[0]; //获取触摸位置的第一个触摸点
    var event = event || window.event;
    if(touchEvent.pageX || touchEvent.pageY) {
        x = touchEvent.pageX;
        y = touchEvent.pageY;
    }else {
        var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        x = touchEvent.clientX + scrollLeft;
        y = touchEvent.clientY + scrollTop;
    }
    touch.x = x;
    touch.y = y;

    return touch;
}

常用的方法是,如果不存在有效的触摸点是,x和y的值应设置为null。

element.addEventListener("touchstart", function(event) {
    touch.isPressed = true;
}, false);
element.addEventListener("touchsend", function(event) {
    touch.isPressed = false;
    touch.x = null;
    touch.y = null;
}, false);
element.addEventListener("touchsend", function(event) {
    if(touch.isPressed) {
        getTouch (event);
    }
}, false);

获得键盘码可以使用event.keyCode。具体实现如下:

var keyCode;
function getKeyCode(event) {
    var event = event || window.event;
    keyCode = event.keyCode;
    return keyCode;
}

多数Web动画都是由一帧帧的状态通过较快速度的播放模拟出来的,所以循环计时函数在这里就起到了连接连贯的帧状态的作用。而动画更多时候需要用户交互,所以事件和事件监听尤显重要。最后我列出了几个经常用到几个与事件相关的封装应用,方便自己查阅和调用

最普通的动画就是匀速的动画,每次增加固定的值。但是生活中很多运动并不是匀速运动的,而是有加速度改变的运动。在Web动画中,缓动动画有时候会让网站增色不少。

在CSS3中可以使用ease, ease-in, ease-out, ease-in-out 或者 cubic-bezier(n,n,n,n)来实现缓动动画。而且目前也有一些jQuery封装了缓动动画的Move.js, Velocity.js和Tween.js等。在实际项目中使用这些库文件或者CSS3属性可以大大提高开发效率。但是在学习中,为了了解JS缓动动画的真正原理,我觉得有必要尝试用原生的JS实现之。

总的来说,缓动动画都是把对象从已有位置移动到目标位置的过程,在这个过程中,加速度或者速度会随与目标位置的远近而变化。

缓动动画的一些具体动画曲线可以查看这里《缓动函数》,感受一下~

一. 一般实现缓动的策略如下:

1 . 为运动确定一个比例系数,这是一个小于1且大于0的小数;

2 . 确定目标点;

3 . 计算出物体当前位置与目标点位置的距离;

4 . 计算速度,例如缓入动画中,速度 = 距离 × 比例系数,这时比例系数为运动的加速度;

5 . 用当前位置加上速度来计算新的位置;

6 . 重复第3到第5步,知道物体到达目标;

缓入动画

先看看这些代码片段以及他们的含义:

1 . 确定一个小数作为比例系数,这个比例系数为加速度(标量)。当系数越接近于1,物体移动得越快;当系数越接近于0,物体移动得越慢。

var easing = 0.05;
2 . 确定目标点。这里用targetX和targetY来定义:

var targetX = canvas.width / 2,
targetY = canvas.height / 2;
3 . 计算物体到目标点的距离。创建小球名为ball,用ball的x、y减去目标点的x、y就能得到距离。

var dx = targetX - ball.x,
dy= targetY - ball.y;
4 . 速度 = 距离 × 比例系数。

var vx = dx * easing,
vy= dy * easing;
5 . 用当前位置加上速度来计算新的位置。

ball.x += vx;
ball.y += vy;
6 . 因为最后几步需要重复执行,所以会把这些代码放在drawFrame函数里面

改进版缓入动画:加入拖拽效果

改进版缓入动画

二. 何时停止缓动动画

当计算一个单目标点的简单缓动时,物体最终会到达这个目标点,缓动也就完成了。但是,即使在前面的几个例子里,即使该物体看起来已经停止了,计算缓动动画的代码还是一直在执行(不信的可以在缓动动画中加入打印代码如console.log("hello world!"),打开控制台就会看到健步如飞的"hello world!"会打印出来~)。这样比较浪费系统资源。一旦物体到达了目标点,代码就应该不再执行了。这个功能很简单,只需要在动画循环里面判断一下物体是否到达目标点即可

if(ball.x === targetX && ball.y === targetY) {
    // 停止缓动动画代码
    window.cancelAnimationFrame(animRequest);
}

事实上,由于ball.x和ball.y可能是小数,随着vx和vy越来越小越趋近于0,事实上它离目标点越来越近,但是理论上永远不会到达目标点,而是无穷趋于目标点的小数。一般分辨率的电脑的显示的最小精度是1px(除了一些高分屏精度为0.1px),不能精确显示无穷多位小数的距离。到底多近才是足够近?这就需要判断物体到目标点的距离是否小于特定值了。我们可以根据实际情况使用Math.ceil()、Math.floor()或Math.round()来对小数进行取整操作,以取接近目标点的值

if(Math.ceil(ball.x) === targetX && Math.ceil(ball.y) === targetY) {
    // 停止缓动动画代码
    window.cancelAnimationFrame(animRequest);
}

三. 移动的目标点

在前面的例子中,目标点只有一个,并且是固定的。

然而目标点可以是移动的。我们在每一帧都会重新计算距离,然后根据距离计算速度,代码并不关心物体是否到否目标点或者目标点是否在移动,它只需在播放的每一帧的时候知道目标点的位置,然后计算距离和速度。

小球跟随鼠标运动的例子中,我们把鼠标位置作为目标点,只需要把前面例子中的targetX和targetY分别替换为鼠标的位置mouse.x和mouse.y即可

小球跟随鼠标运动

缓动不仅仅适用于运动,还可以操作很多其他属性。只要这个属性是可以用数字表示的,就可以操作它。例如:

4.1 Demo1. 颜色缓动动画

尝试在24位颜色上使用缓动,要设置红、绿、蓝的初始值和目标值,用缓动改变每一种单独的颜色,然后再把他么合并为单个颜色

// 初始化变量
var red = 255,
    green = 0,
    blue = 0,
    redTarget = 0,
    greenTarget = 0,
    blueTarget = 255;

// 使用缓动动画
red += Math.ceil((redTarget - red) * easing);
green += Math.ceil((greenTarget - green) * easing);
blue += Math.ceil((blueTarget - blue) * easing);

// 最后把这三个单色值合并成一个颜色
ball.fillStyle = "rgb(" + red +"," + green + "," + blue + ")";

变颜色

4.2 Demo2. 透明度缓动动画

将缓动应用在alpha上,设置alpha的初始值和目标值,然后使用缓动动画实现淡入淡出的效果,最后把它拼接成一个RGBA字符串:

var alpha = 0,
targetAlpha = 1;

// 使用缓动动画
alpha += (targetAlpha - alpha) * easing;
ball.fillStyle = "rgba(" + red +"," + green + "," + blue + "," + alpha + ")";

demo

五. 高级缓动

我们上面用到的都是简单缓动,即物体只有一个加速度easing。而事实上我们可以完全可以通过使easing为非定值,来实现自定义物体的任意运动状态,譬如先加速且接近物体时减速等。

一些高级缓动函数可以参考:

1 . Tween.js的源码:https://github.com/tweenjs/tween.js/blob/master/src/Tween.js

2 . jquery.easing.js的源码:https://github.com/gdsmith/jquery.easing/blob/master/jquery.easing.js

六. 总结

缓动动画是比例速度,通过修改每一帧的速度来计算出当前值,通过加速度easing可以控制独特的动画效果。简单缓动动画不难,关键是要动手练习。高级缓动动画,可以自己实验出一种特效,或者多看看Tween.js和jquery.easing.js等一些类库的缓动动画实现以汲取经验

弹动动画:

缓动和弹动都是那对象从已有位置移动到目标位置的方法。但是缓动是指物体滑动到目标点就停下来;而弹动是指物体来回反弹一段时间后,最终停在目标点的运动。

弹动,大多数时候,物体的加速度与它到目标点的距离是成比例的。

来看一个在现实中弹动的例子:在橡皮筋的一头系上一个小球,另一头固定起来。小球的目标点就是它初始静止悬空的那个位置点。将小球拉开一小段距离然后松开,刚松手那一瞬间,它的速度为0,但是橡皮筋给它施加了外力,把它拉向目标点;如果小球尽可能地拉远,橡皮筋对它施加的外力就会变得越大。松手后,小球会急速飞过目标点。但是,当它飞过目标点以后,橡皮筋又把它往回拉,使其加速度减小,它飞得越远,橡皮筋施加的力就越大;最终,它的速度降为0,又掉头往回飞。由于受到摩擦力的影响,反复几次后,小球的运动逐渐慢下来,停在目标点上。

一. 一维坐标上的弹动

1 . 首先需要一个变量存储弹性比例系数,取值为0~1,较大的弹性比例常熟会表现出较硬的弹簧效果。

var spring = 0.1,
targetX = canvas.width / 2,
vx = 0;
2 . 接下来,计算小球到目标点的距离

var dx = targetX - ball.x;
3 . 计算加速度。在这个例子中,我们设置小球的加速度与距离成正比,即加速度 = 小球到目标点的距离 × 弹性比例系数。

var ax = dx * spring;
4 . 把加速度累加到速度上,然后把速度累加到小球的当前位置上:

vx += ax;
ball.x += vx;
在开始写代码前,先模拟一下整个过程,假设ball.x = 0,初速度vx = 0,目标点的位置targetX = 100,弹性比例系数spring = 0.1。下面是执行过程:

(1) 第一轮,加速度ax = (100 – 0) * 0.1 = 10,把ax加载vx上得速度vx = 10,把vx加在小球的当前位置上得到ball.x = 10;

(2) 第二轮,加速度ax = (100 – ball.x) * 0.1 = 9,由此得到vx = 10 + 9 = 19,ball.x = 10 + 19 = 29;

(3) 第三轮,ax = 7.1, vx = 26.1,ball.x = 55.1;

(4) 第四轮, ax = 4.49,vx = 30.59,ball.x = 85.69;

(5) 第五轮, ax = 1.431,vx = 44.9,ball.x = 130.69:

(6) 第六轮,ax = -3.069,vx = 41.831,ball.x = 88.859;

… …

随着小球一帧一帧地靠近目标,加速度变得越来越小,但是速度一直在增加;

五轮过后,小球越过了目标点后,加速度变成反向加速度,并且逐渐增加,导致速度逐渐减小,最终速度为0后,反向加速度达到极大值。此时速度将变成反向速度。

demo

但是!但是,问题是小球永远都不会停下来,因为小球的摆动幅度不变。而我们希望实现的例子中,小球的弹动会越来越慢,直到停止下来。在实际生活中,小球的弹动势能大多是由于摩擦力的存在而转化成内能,最后使小球停下。所以,在这里,我们也模拟摩擦力,创建摩擦力系数friction,取值范围为0~1。

var friction = 0.95;

然后把vx * friction,得到当前的速度vx。

vx * = friction;

demo

二. 二维坐标上的弹动

上面一个例子是让小球在x轴上运动。如果我们想让小球同时在x轴和y轴上运动,就需要引入二维坐标上的弹动。事实上很简单,只需要把目标点、速度和加速度扩展到二维坐标系上即可。

代码与上面例子雷同不再重复,直接上效果:
demo

与前一个例子唯一不同的是增加了一条y轴。但是现在小球看起来仍然像是一维运动,虽然小球同时在x轴和y轴上运动,但它仍然是一条直线。原因是它的初速度为0,也仅受一个把它拉向目标点的外力,所以它沿着直线运动。为了动画更丰富一点,可以尝试修改vx、vy或者不同x、y轴的friction值。自己尝试一下吧。

三. 目标点移动的弹动

目标点移动,我们很容易就想到把鼠标当成目标点。在上一篇介绍缓动动画时,有一个小球跟随鼠标的缓动动画。让小球跟随鼠标弹动同样很简单,只要把targetX和targetY替换为当前坐标即可。效果很炫酷,但是代码基本没变。只要在前面的例子中改动如下两行:

var dx = targetX - ball.x;
var dy = targetY - ball.y;

修改为:

var dx = mouse.x - ball.x;
var dy = mouse.y - ball.y;

demo

好吧,上面这个例子不够带劲儿,希望使小球看起来像是栓在橡皮筋上,此时只要在上面的基础上再小球圆心与当前鼠标位置画线即可。

context.beginPath();
context.strokeStyle = "#71A4AD";
context.moveTo(ball.x, ball.y);
context.lineTo(mouse.x, mouse.y);
context.stroke();

demo

弹动和缓动非常类似,都是使用循环函数逐帧绘制从当前位置到目标位置的运动效果。不同的是缓动是指速度与距离成比例,而弹动是加速度与距离成比例关系。但是要模拟出更加真实的弹动,可能需要加入类似摩擦力系数的因子,把速度逐渐降下,直到停止运动

一个简单的动画库:

var box = document.getElementById('box');
        tween(box,{
            left:1000,
            top:500,
            opacity:0.2,
            width:50,
            height:50
        },2000,3,function(){
            utils.css(this,'background','aqua');
        })




~function(){

    var dc = {
        // 匀速:
        Linear:function(t,b,c,d){
            return c * t / d + b;
        },
        // 指数衰减的反弹缓动
        Bounce: {
            easeIn: function (t, b, c, d) {
                return c - dc.Bounce.easeOut(d - t, 0, c, d) + b;
            },
            easeOut: function (t, b, c, d) {
                if ((t /= d) < (1 / 2.75)) {
                    return c * (7.5625 * t * t) + b;
                } else if (t < (2 / 2.75)) {
                    return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
                } else if (t < (2.5 / 2.75)) {
                    return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
                } else {
                    return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
                }
            },
            easeInOut: function (t, b, c, d) {
                if (t < d / 2) {
                    return dc.Bounce.easeIn(t * 2, 0, c, d) * .5 + b;
                }
                return dc.Bounce.easeOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
            }
        },
        // 二次方缓动
        Quad: {
            easeIn: function (t, b, c, d) {
                return c * (t /= d) * t + b;
            },
            easeOut: function (t, b, c, d) {
                return -c * (t /= d) * (t - 2) + b;
            },
            easeInOut: function (t, b, c, d) {
                if ((t /= d / 2) < 1) {
                    return c / 2 * t * t + b;
                }
                return -c / 2 * ((--t) * (t - 2) - 1) + b;
            }
        },
        // 三次方缓动
        Cubic: {
            easeIn: function (t, b, c, d) {
                return c * (t /= d) * t * t + b;
            },
            easeOut: function (t, b, c, d) {
                return c * ((t = t / d - 1) * t * t + 1) + b;
            },
            easeInOut: function (t, b, c, d) {
                if ((t /= d / 2) < 1) {
                    return c / 2 * t * t * t + b;
                }
                return c / 2 * ((t -= 2) * t * t + 2) + b;
            }
        },
        // 四次方缓动
        Quart: {
            easeIn: function (t, b, c, d) {
                return c * (t /= d) * t * t * t + b;
            },
            easeOut: function (t, b, c, d) {
                return -c * ((t = t / d - 1) * t * t * t - 1) + b;
            },
            easeInOut: function (t, b, c, d) {
                if ((t /= d / 2) < 1) {
                    return c / 2 * t * t * t * t + b;
                }
                return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
            }
        },
        // 五次方缓动
        Quint: {
            easeIn: function (t, b, c, d) {
                return c * (t /= d) * t * t * t * t + b;
            },
            easeOut: function (t, b, c, d) {
                return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
            },
            easeInOut: function (t, b, c, d) {
                if ((t /= d / 2) < 1) {
                    return c / 2 * t * t * t * t * t + b;
                }
                return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
            }
        },
        // 正弦曲线缓动
        Sine: {
            easeIn: function (t, b, c, d) {
                return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
            },
            easeOut: function (t, b, c, d) {
                return c * Math.sin(t / d * (Math.PI / 2)) + b;
            },
            easeInOut: function (t, b, c, d) {
                return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
            }
        },
        // 指数曲线缓动
        Expo: {
            easeIn: function (t, b, c, d) {
                return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
            },
            easeOut: function (t, b, c, d) {
                return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
            },
            easeInOut: function (t, b, c, d) {
                if (t == 0) return b;
                if (t == d) return b + c;
                if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
                return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
            }
        },
        // 原圆形线缓动
        Circ: {
            easeIn: function (t, b, c, d) {
                return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
            },
            easeOut: function (t, b, c, d) {
                return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
            },
            easeInOut: function (t, b, c, d) {
                if ((t /= d / 2) < 1) {
                    return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
                }
                return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
            }
        },
        //超出范围的三次方缓动
        Back: {
            easeIn: function (t, b, c, d, s) {
                if (s == undefined) s = 1.70158;
                return c * (t /= d) * t * ((s + 1) * t - s) + b;
            },
            easeOut: function (t, b, c, d, s) {
                if (s == undefined) s = 1.70158;
                return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
            },
            easeInOut: function (t, b, c, d, s) {
                if (s == undefined) s = 1.70158;
                if ((t /= d / 2) < 1) {
                    return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
                }
                return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
            }
        },
        //ָ指数衰减的正弦曲线缓动
        Elastic: {
            easeIn: function (t, b, c, d, a, p) {
                if (t == 0) return b;
                if ((t /= d) == 1) return b + c;
                if (!p) p = d * .3;
                var s;
                !a || a < Math.abs(c) ? (a = c, s = p / 4) : s = p / (2 * Math.PI) * Math.asin(c / a);
                return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
            },
            easeOut: function (t, b, c, d, a, p) {
                if (t == 0) return b;
                if ((t /= d) == 1) return b + c;
                if (!p) p = d * .3;
                var s;
                !a || a < Math.abs(c) ? (a = c, s = p / 4) : s = p / (2 * Math.PI) * Math.asin(c / a);
                return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);
            },
            easeInOut: function (t, b, c, d, a, p) {
                if (t == 0) return b;
                if ((t /= d / 2) == 2) return b + c;
                if (!p) p = d * (.3 * 1.5);
                var s;
                !a || a < Math.abs(c) ? (a = c, s = p / 4) : s = p / (2 * Math.PI) * Math.asin(c / a);
                if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
                return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b;
            }
        }
        
    }

    // 实现多方向的运动动画:操作元素,目标位置(是一个对象,存储每一个方向的目标位置)
    // effect:支持以下的情况
    // ["Linear","Circ.easeInOut","Elastic.easeOut","Back.easeOut","Expo.easeIn"]
    // 1。如果传递进来的是一个数字:0->Linear,1->Circ.easeInOut,2->Elastic.easeOut
    // 2.如果传进来的是一个数组:["Circ","easeInOut"] ->dc.Circ.easeInOut
    // 3.如果不传递的话,默认使用Linear
    function move(curEle,target,duration,effect,callBack){

        //处理我们需要的动画效果
        var tempEffect = dc.Linear;
        if(typeof effect == 'number'){
            switch (effect) {
                case 0:
                    tempEffect = dc.Linear;
                    break;
                case 1:
                    tempEffect = dc.Bounce.easeIn;
                    break;
                case 2:
                    tempEffect = dc.Quart.easeInOut;
            }
        }else if(effect instanceof Array){
            tempEffect = effect.length >=2? dc[effect[0]][effect[1]] : dc[effect[0]];
        }else if(typeof effect == 'function'){ 
            callback = effect;
        }
        console.log(tempEffect);

        clearInterval(dc.timer);
        // 根据target获取每一个方向的初始值begin和总距离change
        var begin = {};
        var change = {};
        for(var key in target){
            //key : top/left
            if(target.hasOwnProperty(key)){
                // top = 50  ,这个值是实时的值,会不断变化
                // target[key]是个固定值,参数值
                begin[key] = utils.css(curEle,key);
                change[key] = target[key] - begin[key];
            }
        }
        // 实现多方向的运动动画
        var time = 0;
        dc.timer = setInterval(function(){
            time += 10;
            if(time > duration){
                utils.css(curEle,target);
                clearInterval(dc.timer);
                // 在动画结束的时候,传递回调函数
                // typeof callBack === "function" ? callBack() : null;
                callBack && callBack.call(curEle);
                return;
            }

            // 获取当前位置并设置样式
            for(var key in target){
                if(target.hasOwnProperty(key)){
                    var curPos = tempEffect(time,begin[key],change[key],duration);
                    utils.css(curEle,key,curPos);
                }
            }
        },10);
    }
    window.tween = move;
}();

参考链接:

JavaScript动画详解(一) —— 循环与事件监听

JavaScript动画详解(二) —— 缓动动画

JavaScript动画详解(三) —— 弹动动画

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 前言 又到了炎热的7月,很久没有更新技术文章了,原因是上月月底实习结束,从公司离职。然后最近在弄自己的项目和考驾照...
    大力有话说阅读 9,037评论 4 14
  • JS动画 动画的原理盒子自身的 offsetLeft + 步长;封装动画封装匀速动画:Math.abs()绝对值 ...
    AnnQi阅读 537评论 0 3
  • JavaScript 动画框架 框架封装 相信大家在很多门户网站上都可以看到动画的交互效果,通过这些动画生动地体现...
    蝉翅的空响阅读 1,212评论 0 1
  • 这一篇是接着上一篇 翻译。原文点击。上一篇主要讲了animate的基本原理。这一篇主要将几种常见的delta。 进...
    wpzero阅读 457评论 0 10
  • 2016年11月24日 星期四 今天感恩节,感恩我们相遇在AAA 我们能够完成任何我们想做的事情,但不是所有的事情...
    zkishi阅读 736评论 0 0