【青训营】前端动画实现

242 阅读11分钟

这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

授课老师:蒋翔老师

目录:

  1. 动画的基本原理
  2. 前端动画分类
  3. 实现前端动画
  4. 相关实践

01. 动画的基本原理

动画是通过快速连续排列彼此差异极小的连续图像来制造运动错觉和变化错觉的过程。--维基百科

  1. 常见的前端动画技术 Sprite动画、CSS动画、JS动画、SVG动画和WebGL动画

  2. 应用分类 UI动画、基于Web的游戏动画和动画数据可视化

  3. 动画相关基础

  • 计算机图形学: 计算机视觉的基础,涵盖点、线、面、体、场的数学构造方法。
    1. 几何和图形数据的输入、存储和压缩。
    2. 描述纹理、曲线、光影等算法。
    3. 物体图形的数据输出(图形接口、动画技术),硬件和图形的交互技术。
    4. 图形开发软件的相关技术标准。
  • 计算机动画:计算机图形学的分支,主要包含2D、3D动画。
  1. 动画原理

无论动画多么简单,始终需要定义两个基本状态,即开始状态和结束状态。没有它们,我们将无法定义插值状态,从而填补了两者之间的空白。

相关概念

  • 帧:连续变换的多张画面,其中的每一幅画面都是一帧。
  • 帧率:用于度量一定时间段内的帧数,通常的测量单位是FPS (frame per second) 。
  • 帧率与人眼:一般每秒10-12帧人会认为画面是连贯的,这个现象称为视觉暂留。对于一些电脑动画和游戏来说低于30FPS会感受到明显卡顿,目前主流的屏幕、显卡输出为60FPS,效果会明显更流畅。

空白的补全方式:

  • 补间动画 传统动画,主画师绘制关键帧,交给清稿部门,清稿部门的补间动画师补充关键帧进行交付。(类比到这里, 补间动画师由浏览器来担任,如keyframe, transition)

  • 逐帧动画(Frame By Frame) : 从词语来说意味着全片每一帧逐帧都是纯手绘。(如css的steps实现精灵动画)

02. 前端动画分类

  • css动画
  • svg 动画
  • js 动画
  • 如何选择

CSS 动画

CSS animation是常见的CSS动画实现方式

CSS animation复合属性包含如下属性:

animation-name

animation-name属性指定应用的一系列动画,每个名称代表一个由@keyframes定义的动画序列。

简单的animation-name可以单纯指定动画的名字,默认值为none。名称由大小写敏感的字母a-z、数字0-9、下划线(_)和/或横线(-)组成。第一个非横线字符必须是字母,数字不能在字母前面,不允许两个横线出现在开始位置。

animation-name: none;/* 默认值 */
animation-name: test_05; 
animation-name: -specific;
animation-name: sliding-vertically;

也可以指定多个名称,代表复合的动画:

animation-name: test1, animation4;
animation-name: none, -moz-specific, sliding;

当然,还可以设置它是否使用继承值或者默认值

animation-name: initial
animation-name: inherit
animation-name: unset

问:既然本身都是none了,那么重新设置animation:none是否还有意义呢?

答:比如我的动画正在执行,如果我需要直接终止它,只需要通过某个行为将animation-name设置为none,那么整个动画就结束了。

animation-duration

一个动画周期的时长,单位为秒(s)或者毫秒(ms),无单位值无效。负值也无效。

如果存在多个动画序列,那么也是逗号分隔,写明时间就好了。

animation-duration:2s,1s,500ms;

animation-timing-function

CSS内置有一些缓动的函数,通过调用缓动函数达到缓冲效果。animation-timing-function属性定义CSS动画在每一动画周期中执行的节奏。

/* Keyword values */
animation-timing-function: ease;
animation-timing-function: ease-in;
animation-timing-function: ease-out;
animation-timing-function: ease-in-out;
animation-timing-function: linear;
animation-timing-function: step-start;
animation-timing-function: step-end;

/* Function values */
animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);
animation-timing-function: steps(4, end);
animation-timing-function: frames(10);

/* Multiple animations */
animation-timing-function: ease, step-start, cubic-bezier(0.1, 0.7, 1.0, 0.1);

/* Global values */
animation-timing-function: inherit;
animation-timing-function: initial;
animation-timing-function: unset;

animation-delay

animation-delay CSS属性定义动画于何时开始,即从动画应用在元素上到动画开始的这段时间的长度。

0s是该属性的默认值,代表动画在应用到元素上后立即开始执行。否则,该属性的值代表动画样式应用到元素上后到开始执行前的时间长度;

定义一个负值会让动画立即开始。但是动画会从它的动画序列中某位置开始。例如,如果设定值为-1s,动画会从它的动画序列的第1秒位置处立即开始。

animation-iteration-count

定义执行次数,如果存在小数点,该小数代表最后一次执行的百分比。

/* 无限循环  */
animation-iteration-count: infinite;
/* 表示 2次 + 第3次播放到一半 */
animation-iteration-count: 2.5;
/* 第一次赋予该动画属性就播放2次,第2次不播放,第三次无限循环*/
animation-iteration-count: 2, 0, infinite;

animation-direction

指示动画是否反向播放。可选值如下:

  • normal
    • 每个循环内动画向前循环,换言之,每个动画循环结束,动画重置到起点重新开始,这是默认属性。
  • alternate
    • 动画交替反向运行,反向运行时,动画按步后退,同时,带时间功能的函数也反向,比如,ease-in 在反向时成为 ease-out。计数取决于开始时是奇数迭代还是偶数迭代
  • reverse
    • 反向运行动画,每周期结束动画由尾到头运行。
  • alternate-reverse
    • 反向交替, 反向开始交替。
    • 动画第一次运行时是反向的,然后下一次是正向,后面依次循环。决定奇数次或偶数次的计数从1开始。

animation-fill-mode

  • none

    • 当动画未执行时,动画将不会将任何样式应用于目标,而是已经赋予给该元素的 CSS 规则来显示该元素。这是默认值。
  • forwards执行完了停留在最后的状态

    • 目标将保留由执行期间遇到的最后一个关键帧计算值。 最后一个关键帧取决于animation-directionanimation-iteration-count的值: | animation-direction | animation-iteration-count | last keyframe encountered | | --------------------- | --------------------------- | ------------------------- | | normal | even or odd | 100% or to | | reverse | even or odd | 0% or from | | alternate | even | 0% or from | | alternate | odd | 100% or to | | alternate-reverse | even | 100% or to | | alternate-reverse | odd | 0% or from |
  • backwards : 执行完了之后停留在第一帧状态

    • 动画将在应用于目标时立即应用第一个关键帧中定义的值,并在animation-delay期间保留此值。 第一个关键帧取决于animation-direction的值:
    animation-directionfirst relevant keyframe
    normal or alternate0% or from
    reverse or alternate-reverse100% or to
  • both

    • 动画将遵循forwardsbackwards的规则,从而在两个方向上扩展动画属性。

image.png

image.png

第一个参数是time 缓冲函数

形体变化, 变化位置:origin

image.png

transform: - translate(移动) - scale(缩放) - rotate (旋转) - skew(倾斜)

image.png

Transition API: image.png

keyframe实现动画

image.png

CSS实现逐帧动画(steps)

CSS动画总结:

  • 优点:简单、高效、声明式的、不依赖于主线程,采用硬件加速(GPU) 简单地控制keyframe animation播放和暂停。
  • 缺点:不能动态修改或定义动画,内容不同的动画无法实现同步,多个动画彼此无法堆叠。
  • 适用场景:简单的h5活动/宣传页。
  • 推荐库:animation.css、shake.css等 。

svg动画

svg是基于XML的矢量图形描述语言,它可以与CSS和JS较好的配合,实现svg动画通常有三种方式: SMIL、JS、CSS

  1. SMIL : Synchronized Multimedia Integration Language (同步多媒体集成语言)

image.png

image.png

  1. JS

使用JS来操作SVG动画自不必多说,目前也有很多现成的类库。例如老牌的Snap.svg以及anime.js,都能让我们快速制作SVG动画。当然,除了这些类库,HTML本身也有原生的Web Animation实现。使用Web Animation也能让我们方便快捷地制作动画。 文字形变: codepen.io/jiangxiang/… Path实现写字动画: codepen.io/jiangxiang/…

CSS filter 属性 url:定义svg的id image.png

JS笔画的原理

image.png

stroke-dashoffset、stroke-dasharray配合使用实现笔画效果。

属性stroke-dasharray可控制用来描边的点划线的图案范式。它是一个数列,数与数之间用逗号或者空白隔开,指定短划线和缺口的长度。如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。

因此,5,3,2等同于5,3,2,5,3,2。 stroke-dashoffset属性指定了dash模式到路径开始的距离。

image.png

SVG我们经常使用animation, transform, transition来实现动画,它比JS更 加简单方便。

优点:通过矢量元素实现动画,不同的屏幕下均可获得较好的清晰度。 可以实现一些特殊的效果:描字,形变,墨水扩散等。 缺点:使用方式较为复杂,过多使用可能会带来性能问题。

如何选择

image.png

image.png

总结:

  • 当您为UI元素采用较小的独立状态时,使用CSS。
  • 在需要对动画进行大量控制时,使用JavaScript.
  • 在特定的场景下可以使用SVG,可以使用CSS或JS去操作SVG变化。

03. 实现前端动画

  1. JS动画函数封装
  2. 简单动画
  3. 复杂动画

image.png

image.png

Date.now()容易被篡改

image.png

image.png

duration持续时间 duration:1000 动画的持续时间,单位是毫秒 返回Promise的原因 动画可以是连续的,支持通过then函数或await进行顺序调用。

说明: 1.为什么使用performance.now()而非Date.now() ? performance.now(会以恒定速度自增,精确到微妙级别,不易被篡改。

setTimeout是跟着线程走的,如果遇到线程阻塞,会影响动画效果,导致丢帧。

image.png

通过比例尺缩小

image.png

image.png

平抛

image.png

旋转 + 平抛

image.png

拉弓

image.png

贝塞尔曲线,网站说明:www.jasondavies.com/animated-be…

image.png

复杂动画

弹跳小球

image.png

image.png

04. 动画实践

动画代码示例(灵感来源):

  • codepen.com
  • codesandbox.com 设计网站:
  • dribbble.com 动画制作工具(一般都是UE、UI同学使用)
  • 2D : Animate CC、After Effects
  • 3D : Cinema 4D、Blender、Autodesk Maya

几种动画的使用库推荐

SVG :

  • Snap.svg -现代SVG图形的JavaScript库。
  • Svg.js -用于操作和动画SVG的轻量级库。 JS :
  • GSAP - JavaScript动画库。
  • TweenJS - 一个简单但功能强大的JavaScript补间/动画库。CreateJS 库套件的一部分。
  • Velocity - 加速的JavaScript 动画。 CSS :
  • Animate.css - CSS动画的跨浏览器库。容易使用。

Canvas :

  • EaselJS - EaselJS是一个用于在HTML5中构建高性能交互式2D内容的库。
  • Fabric.js -支持动画的JavaScript画布库。
  • Paperjs -矢量图形脚本的瑞士军刀- Scriptographer使用HTML5 Canvas移植到JavaScript和浏览器。
  • Pixijs -使用最快、最灵活的2D WebGL渲染器创建精美的数字内容。

还有跨端的库:Lottie

image.png

动画的优化

image.png

CSS3硬件加速又叫做GPU加速,是利用GPU进行渲染,减少CPU操作的一种优化方案。由于GPU中的transform等CSS属性不会触发repaint,所以能大大提高网页的性能。 CSS中的以下几个属性能触发硬件加速:

  1. transform
  2. opacity
  3. filter
  4. Will-change 如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可 以使用一些小技巧来诱导浏览器开启硬件加速。

作业:封装animation的播放,暂停,取消,反转,速率更改的API。

// 示例动画
const draw = (progress) => {
    ball.style.transform = `translate(${progress}px, 0)`;
}
// 沿着x轴匀速运动
const animation = animate({
    duration: 1000,
    easing(timeFraction) {
      return timeFraction * 100;
    },
    draw,
});

// 分别实现以下动画API
animation.play();   // 开始播放(动画默认不开始,需要手动执行play)
animation.pause();  // 暂停,执行play会继续执行
animation.cancel(); // 终止动画,直接回到终止态
animation.reverse(); // 倒序播放动画
animation.playRate(0.6); // 以原速的0.6倍执行动画

个人的半成品code(封装得很差,功能也有待完善):

HTML结构代码:就一个盒子 + 几个触发API的按钮。

<div id="box"></div>
<button onclick="animation.play()">play</button>
<button onclick="animation.pause()">pause</button>
<button onclick="animation.cancel()">cancel</button>
<button onclick="animation.reverse()">reverse</button>
<button onclick="animation.playRate(0.5)">playRate(0.5)</button>
<button onclick="animation.playRate(1.5)">playRate(1.5)</button>

CSS:把box以绝对定位设置一下位置,顺便变成一个球。

#box {
    position: absolute;
    width: 100px;
    height: 100px;
    top: 100px;
    border: 2px solid red;
    border-radius: 50%;
    background-color: red;
}

JS:利用ES6关键字class来封装Animation类:

// 获取DOM
const ball = document.getElementById('box');

// 平移动画
const draw = (progress) => {
    ball.style.transform = `translate(${progress}px, 0)`;
}
// 封装类
class Animation {
    //构造函数:缓冲函数,绘制函数,持续时间
    constructor({
        easing,
        draw,
        duration
    }) {
        this.status = false; //播放状态
        this.start = 0; //起始时间
        this.pauseTime = 0; //暂停时间
        this.rate = 1; //播放速率
        this.direction = true; // 方向
        
        this.easing = easing; //缓冲函数
        this.draw = draw; //绘制函数
        this.duration = duration; //动画时间
        this.animation = null; // 动画Promise
    }
    //播放
    play() {
        this.status = true;
        if (!this.animation) {
            this.start = performance.now();
            this.animation = this.animate();
            console.log("play");

        } else {
            let time = performance.now();
            this.start = time - (this.pauseTime - this.start);
            console.info("replay");
            console.log("last paused time :", this.pauseTime, "  now  time : ", time);
        }
    }
    //暂停
    pause() {
        this.status = false;
        console.log("paused");
    }
    //取消
    cancel() {
        this.status = false;
        this.start = performance.now();
        this.animation = null;
        console.log("cancel");
    }
    //逆转
    reverse() {
        this.direction =!this.direction;
    }
    //速率
    playRate(speed) {
        this.rate = speed;
    }
    //设置动画
    animate() {
        let _this = this;
        return new Promise(resolve => {
            requestAnimationFrame(function animate(time) {
                if (_this.status) { //如果当前状态为播放
                    // console.log(time);

                    _this.pauseTime = time;
                    // timeFraction goes from 0 to 1
                    let timeFraction = (time - _this.start) / (_this.duration / _this.rate);
                    if (timeFraction > 1) {
                        timeFraction = 1;

                    }
                    if(!_this.direction){//逆转
                        timeFraction = 1 - timeFraction; 
                    }
                    // calculate the current animation state
                    let progress = _this.easing(timeFraction)

                    draw(progress); // draw it

                    if (timeFraction < 1) {
                        requestAnimationFrame(animate);
                    } else {
                        resolve();
                        console.log("resolve");
                        _this.animation = null;
                    }

                } else {
                    requestAnimationFrame(animate);
                }
            });
        });
    }
}

// 沿着x轴匀速运动
const animation = new Animation({
    duration: 5000,
    easing(timeFraction) {
        return timeFraction * 500;
    },
    draw,
});
console.log(animation);