前端动画原理与实现

@LeeGenD

概念

  • 根据时间的变化,连续地改变元素的样式造成的动态效果

基本要素

  • 动画时长:
  • 动画进程:
  • 缓动函数(easing):
  • 元素样式函数:
T
TT
p = \frac{t}{T}
p=tTp = \frac{t}{T}
e=f(p)
e=f(p)e=f(p)
[attribute] = G(e)
[attribute]=G(e)[attribute] = G(e)

缓动函数

让运动看起来更自然

线性缓动函数

e=p
e=pe=p
function easing(p) {
    return p;
}

曲线缓动函数

e=p^4
e=p4e=p^4
function easing(p) {
    return Math.pow(p,4);
}

正弦缓动函数

e=sin(\frac{\Pi}{2} \cdot p)
e=sin(Π2p)e=sin(\frac{\Pi}{2} \cdot p)
function easing(p) {
    return Math.sin(0.5 * Math.PI * p);
}

贝塞尔曲线缓动函数

一种只需要很少的控制点就能生成复杂平滑曲线的方法

任选三个控制点A、B、C并连接

D点为AB上的任意一点

贝塞尔曲线缓动函数

一种只需要很少的控制点就能生成复杂平滑曲线的方法

当D点运动时,取AD:AB=BE:BC

连接DE,并取点F,使DF:DE=AD:AB

贝塞尔曲线缓动函数

一种只需要很少的控制点就能生成复杂平滑曲线的方法

找到所有的F点,则为贝塞尔曲线

多维贝塞尔曲线

AE:AB = BF:BC = CG:CD = EH:EF = FI:FG = HJ:HI

贝塞尔曲线缓动函数

#div1 {
    transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
}

ease 
cubic-bezier(0.25, 0.1, 0.25, 1.0)

ease-in-out 
cubic-bezier(0.42, 0, 0.58, 1.0)

ease-in 
cubic-bezier(0.42, 0, 1.0, 1.0)

ease-out 
cubic-bezier(0, 0, 0.58, 1.0)

缓动函数示例

速查表:http://easings.net/zh-cn

元素样式函数

改变元素样式,形成动画

基础位置移动

/*
* 以下例子中我们默认:
* 最大运动距离S为200px
* 缓动函数为线型缓动函数,即e=p
*/
var S = 200;
function easing(p) {
    return p;
}

基础位置移动

/**
* x=S*t/T
* 求导后 v=S/T 即速度恒定
*/
function animation(p) {
    return S * p;
}

匀速运动

x=S \cdot p
x=Spx=S \cdot p

基础位置移动

匀速运动

x=S \cdot p
x=Spx=S \cdot p

基础位置移动

/**
* x=S*(t/T)^2
* 求导后 v=2S/(T^2)*t 即匀加速
*/
function animation(p) {
    return S * p * p;
}

匀加速运动

x=S \cdot p^2
x=Sp2x=S \cdot p^2

基础位置移动

匀加速运动

x=S \cdot p^2
x=Sp2x=S \cdot p^2

基础位置移动

function animation(p) {
    return S * p * (2-p);
}

匀减速运动

x=S \cdot p \cdot (2-p)
x=Sp(2p)x=S \cdot p \cdot (2-p)

基础位置移动

匀减速运动

x=S \cdot p \cdot (2-p)
x=Sp(2p)x=S \cdot p \cdot (2-p)

基础位置移动

function animation(p) {
    return S * Math.sin(2 * Math.PI * p);
}

简谐振动

x=S \cdot sin(2\Pi \cdot p)
x=Ssin(2Πp)x=S \cdot sin(2\Pi \cdot p)

基础位置移动

简谐振动

x=S \cdot sin(2\Pi \cdot p)
x=Ssin(2Πp)x=S \cdot sin(2\Pi \cdot p)

组合位置移动

斜线运动

x=S \cdot p
x=Spx=S \cdot p
y=S \cdot p
y=Spy=S \cdot p

组合位置移动

抛物线运动

x=S \cdot p
x=Spx=S \cdot p
y=S \cdot p^2
y=Sp2y=S \cdot p^2

组合位置移动

正弦线运动

x=S \cdot p
x=Spx=S \cdot p
y=S \cdot sin(2\Pi \cdot p)
y=Ssin(2Πp)y=S \cdot sin(2\Pi \cdot p)

组合位置移动

圆周运动

x=S \cdot cos(2\Pi \cdot p)
x=Scos(2Πp)x=S \cdot cos(2\Pi \cdot p)
y=S \cdot sin(2\Pi \cdot p)
y=Ssin(2Πp)y=S \cdot sin(2\Pi \cdot p)

帧动画

动态改变每一帧显示的图片,形成动画效果

var imgList = ['./sprite/stand/1.png',
    './sprite/stand/2.png',...]
var listSize = imgList.length;
function animation(p) {
    var index = parseInt((listSize - 1)* p);
    return imgList[index];
}

帧动画

动态改变每一帧显示的图片,形成动画效果

效果示例

JS动画

  • DOM动画

  • canvas动画

定时器

setTimeout/setInterval

/**
 * @param {function} callback 需要延迟执行的函数
 * @param {number} time 延迟执行时间
 * @returns {number} 作为一个唯一的标识符.你可以
将该值作为参数传给 window.clearTimeout() 来取消这
个回调函数。
 */
function setTimeout(callback, time) {};

requestAnimationFrame

/**
 * @param {function} callback 需要执行的函数
 * @returns {number} 作为一个唯一的标识符.你可以
将该值作为参数传给 window.cancelAnimationFrame()
来取消这个回调函数。
 */
function requestAnimationFrame(callback) {};
  • 由于浏览器中js是单线程的,而setTimeout/setInterval只有在线程空闲时才会执行,所以延迟时间并不准确
  • 优化并行的动画动作,把能够合并的动作放在一个渲染周期内完成,如JS动画和CSS动画/变换或SVG SMIL动画,从而呈现出更流畅的动画效果。
  • 页面最小化,或者被Tab切换了,将停止requestAnimationFrame,减少CPU,内存的压力

动画对象封装

/**
* @description
* 动画对象
* @param {number} duration 运动时间
* @param {function} progress 运动函数
* @param {function} easing 缓动函数
*/
function Animation(duration, progress, easing) {
    this.duration = duration || 1000;
    this.progress = progress || function(e) {};
    this.easing = easing || function(p) {
        return p
    };
}
Animation.prototype = {
    start: function(callback) {
        var startTime = new Date();
        var _this = this;
        var runAnimation = function() {
            var p = (new Date() - startTime) / _this.duration;
            p = p > 1 ? 1 : p;
            var e = _this.easing(p);
            _this.progress(e);
            if (p == 1) {
                callback && callback();
            } else {
                window.requestAnimationFrame(runAnimation);
            }
        }
        runAnimation();
    }
}

动画对象封装

// 匀速运动(示例)
new Animation(150, function(e) {
    var x = 200 * e;
    var y = 0;
    bullet.style.transform = 'translate(' + x + 'px,' + y + 'px)';
});

动画对象封装

// 先旋转,然后匀速运动(示例)
var a1 = new Animation(1000, function(e) {
    ele.style.transform = 'rotate(' + 360 * e + 'deg)';
});
var a2 = new Animation(1000, function(e) {
    ele.style.transform = 'translate(' + 200 * e + 'px,0)';
});
a1.start(function() {
    a2.start();
});

示例:子弹击出

子弹匀速运动

子弹速度骤降,匀速运动

木板向后旋转

子弹自由落体

击中木板

击中铁板

示例:子弹击出

子弹匀速运动

子弹速度骤降,匀速运动

木板向后旋转

子弹自由落体

击中木板

击中铁板

// 子弹匀速运动
var bulletFly1 = new Animation(150, function(e) {
    var x = 200 * e;
    var y = 0;
    bullet.style.transform = 'translate(' + x + 'px,' + y+ 'px)';
});
// 子弹匀速运动2
var bulletFly2 = new Animation(750, function(e) {
    var x = 200 + 300 * e;
    var y = 0;
    bullet.style.transform = 'translate(' + x + 'px,' + y+ 'px)';
});
// 木板倒下
var woodenWallFall = new Animation(200, function(e) {
    woodenWall.style.transform = 'rotate(' + 90 * e + 'deg)';
});
// 子弹落地
var bulletFall = new Animation(400, function(e) {
    var x = 200 + 300 - 50 * e;
    var y = 80 * e * e;
    bullet.style.transform = 'translate(' + x + 'px,' + y+ 'px)';
});
bulletFly1.start(function() {
    bulletFly2.start(function() {
        bulletFall.start();
    });
    woodenWallFall.start();
});

示例:子弹击出

子弹匀速运动

子弹速度骤降,匀速运动

木板向后旋转

子弹自由落体

击中木板

击中铁板

css3动画

  • transition

  • keyframes

transition

/**
* transition-property: none | all | property;
* transition-duration: time;
* transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
* transition-delay: time;
*/
#id {
    transition: property duration timing-function delay;
}
/* transition使用示例 */
.box {
    width: 50px;
    height: 50px;
    background-color: #ade;
    transition: all 2s;
    -moz-transition: all 2s;/* Firefox 4 */
    -webkit-transition: all 2s;/* Safari 和 Chrome */
    -o-transition: all 2s;/* Opera */
}

.box.active {
    width: 300px;
    background-color: #ead;
}

transition

keyframes

@keyframes animationname {keyframes-selector {css-styles;}}
描述
animationname 动画名称
Keyframes-selector 动画帧的时长的百分比
css-styles CSS 样式属性
/* 元素匀速向下移动 */
@keyframes mymove
{
    from {top:0px;}
    to {top:200px;}
}

keyframes

#id{
    animation: name duration timing-function delay iteration-count direction;
}
描述
name 动画名称
duration 动画时长
timing-function 速度曲线(linear/ease/ease-in/其他)
delay 延迟时间
iteration-count 运行次数 (n/infinite)
direction 方向(normal/alternate)

keyframes

/* keyframes使用示例 */
.box {
    animation: cleanTable 5s infinite;
}

@keyframes cleanTable {
    0% {
       top: 0px;
        background: #fff;
    }
    16.6% {
        top: 160px;
        background: #ccc;
    }
    33.3% {
        top: 30px;
        background: #999;
    }
    50% {
        top: 130px;
        background: #666;
    }
    66.6% {
        top: 60px;
        background: #333;
    }
    83.3% {
        top: 100px;
        background: #000;
    }
    100%{
        top: 0px;
        background: #fff;
    }
}

其他

/* 告诉浏览器将要发生变化的属性是什么,并使用GPU来渲染变化 */
#id {
  transition: transform 0.3s;
}
#id:hover {
  will-change: transform;
}
#id:active {
  transform: scale(1);
}
/* keyframes动画结束监听 */
dom.addEventListener('webkitAnimationEnd', function(){
  // 动画完成回调
});

will-change

webkitAnimationEnd

动画性能优化

减小reflow与repaint

  • reflow:页面上元素的位置是联动的,当我们调解一个元素的大小或者坐标值,可能会触发其它元素位置的变化,称为回流或重排(reflow)

  • repaint:当元素的外观改变,如颜色、背景色等,需要重新绘制,称为重绘(repaint)

减小reflow与repaint

  • 尽量使用css的transform

    • ​tansform可以用来改变元素位置、大小、旋转等

    • tansform不会引发其它元素的reflow

  • 尽量为js动画元素添加absolute、fixed的定位

  • 使用canvas

尽量使用CSS3动画

  • 浏览器会对CSS3动画进行优化

  • 避免多余的JS逻辑代码

  • 适用于简单动画

/* CSS3 keyframes动画 */
@keyframes animationname {keyframes-selector {css-styles;}}
/**
* CSS3 transition动画
* transition-property: none | all | property;
* transition-duration: time;
* transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
* transition-delay: time;
*/
#id {
    transition: property duration timing-function delay;
}

3D硬件加速

  • 对需要运行动画的元素,开启GPU渲染

element.style {
    transform: translate3d(-10px,-10px,0);
}
element.style:hover {
  will-change: transform;
}

使用raf

  • 优化并行的动画动作,把能够合并的动作放在一个渲染周期内完成,如JS动画和CSS动画/变换或SVG SMIL动画,从而呈现出更流畅的动画效果。

  • 页面最小化,或者被Tab切换了,将停止requestAnimationFrame,减少CPU,内存的压力

requestAnimationFrame

Q&A

前端动画原理与实现

By leegend

前端动画原理与实现

  • 355