jjzjj

前端动画实现以及原理浅析

袋鼠云数栈前端 2023-03-28 原文

背景

如今的前端是一个涉猎领域很广的职业。作为一名前端,我们不仅要开发管理系统、数据中台、还要应对年报开发、节日活动等场景。不仅要会增删改查,编写表单,还要具备开发动画、H5 游戏等能力。能做出很 Cool 的动画效果,也是一种前端特有的成就感。所以,我们从动画的实现方法入手,了解浏览器的渲染,以及如何提升动画的性能。

我们先来看 2 个 H5 案例 :
【一镜到底】

<->手机扫码体验

【年报】



【其他 H5 优秀案例】
https://www.h5anli.com

第一部分 常见的动画实现手段

1.1 gif 实现

定义:
GIF 文件的数据是一种基于 LZW 算法的连续色调的无损压缩格式,gif 格式的特点是一个 gif 文件可以存多幅彩色图像,当数据逐幅读出并展示都在屏幕上,就可以构成一个简单的动画。
最高支持 256 种颜色。由于这种特性,GIF 比较适用于色彩较少的图片,比如页面卡通 icon、标志等等。

使用:

![file](https://img2022.cnblogs.com/other/2332333/202205/2332333-20220519101607717-1405608413.png)

优点:
1.制作的成本很低;
2.兼容性好;
3.方便开发使用。

缺点:
1.画质上:色彩支持少,图像毛边严重;
2.交互上:不能控制动画的播放暂停,没有灵活性;
3.大小上:由于是无损压缩,每帧被完整的保存下来,导致文件很大。

1.2 css3 补帧动画

1.2.1 transition 过渡动画

使用:

.box {
    border: 1px solid black;
    width: 100px;
    height: 100px;
    background-color: #0000ff;
    transition: width 2s, height 2s, background-color 2s, transform 2s;
}

.box:hover {
    background-color: #ffcccc;
    width: 200px;
    height: 200px;
    transform: rotate(180deg);
}

场景:

常与 :hover, :active 等伪类使用,实现相应等动画效果。

1.2.2 animation 关键帧动画

使用:

.bounce1 {
    left: -40px;
    animation: bouncy1 1.5s infinite;
}
.bounce2 {
    left: 0;
    animation: bouncy2 1.5s infinite;
}
.bounce3 {
    left: 40px;
    animation: bouncy3 1.5s infinite;
}
@keyframes bouncy1 {
    0% {
        transform: translate(0px, 0px) rotate(0deg);
    }
    50% {
        transform: translate(0px, 0px) rotate(180deg);
    }
    100% {
        transform: translate(40px, 0px) rotate(-180deg);
    }
}

场景:
比如:loading 展示,代码如上。

优点:
1、无需每一帧都被记录,通过关键帧设置,方便开发;
2.实现简单,通常 UI 可以直接给到 css 文件,前端只需要导入即可【移动端注意屏幕适配】。

缺点:
1.css 没法动画交互,无法得知当前动画执行阶段;
2.transition: 需要触发,无法自动播放;
3.animation 兼容性需要加前缀,导致代码量成倍增长;
4.对于复杂动画的实现,导入的 css 文件过大,影响页面的渲染树生成,从而阻塞渲染。比如实现一个摇钱树的效果,css 文件达到百 kb,就要采取一些必要的压缩手段,缩减文件大小。

1.3 js 逐帧动画

JS 动画的原理是通过 setTimeout 或 requestAnimationFrame 方法绘制动画帧,从而动态地改变
网页中图形的显示属性(如 DOM 样式,canvas 位图数据,SVG 对象属性等),进而达到动画的目的。

demo1:
------- js 实现一个正方形从左到右的移动动画 -----

  1. setTimeout 实现
const element2 = document.getElementById('raf2');
const btn2 = document.getElementById('btn2');
let i = 0;
let timerId;
function move () {   
    element2.style.marginLeft = i + 'px'
    timerId = setTimeout(move, 0)
    i++;
    if (i > 200) {
        clearTimeout(timerId)
    }
}
btn2.addEventListener('click',function () {
    move()
})
  1. requestAnimationFrame 实现
const element = document.getElementById('raf');
const btn1 = document.getElementById('btn1');
let r = 0;
let rafId;
function step () {
    element1.style.marginLeft = r+ 'px';
    rafId = window.requestAnimationFrame(step);
    r++;
    if (r > 200) { // 在两秒后停止动画
        cancelAnimationFrame(rafId);
    }
}
btn1.addEventListener('click', function () {
    step();
})

可以看出,实现的方式都是控制 dom 的 margin-left 样式,执行动画。

问题 1.1:demo1 中看出 setTimeout 的执行很快。这是为什么呢?请接着往后看~

第二部分 浏览器如何渲染与动画的渲染

2.1 浏览器的帧原理

问题 2:当 url 输入到一个页面展示出来经过了哪些过程?

这里我们忽略 http 请求静态文件之前的步骤,着重看浏览器渲染帧是怎么做的,从而找到浏览器是如何渲染动画的。

借助 chrome-performance【执行 raf.html】 同样可以看出上图不同阶段在 performance 里面的标注。⚠️注意:不是每帧都总是会经过管道每个部分的处理。实际上,不管是使用 JavaScript、CSS 还是网络动画,在实现视觉变化时,管道帧对指定帧的运行通常有三种方式:

【以下截图是以时间线为主轴,进行绘制】

-当修改一些会触发 layout 的属性,则会导致后面的同样被更新。

  • 当修改只改变 paint 的属性, 则不会重新 layout。

  • 如果改一些不涉及布局也不涉及重绘的数据,则可以直接进行合成渲染。

像 CSS 属性具体可以查询这个网站 ,去查阅哪些属性会引起怎样的帧管道:https://csstriggers.com/

例如:transform 变换,它是一个不会触发布局与绘制的变化的,所以使用它的时候,直接进入第三种状态,在合成之后,直接进入 Composite 阶段,是一个很好的优化手段。

问题 3: 控制台上显示出 requestAnimationFrame(rAF)的执行,那么这个 rAF 执行与浏览器帧有什么关系呢?我们接着往下看。

2.2 requestAnimationFrame 执行

我们还是运行 demo1 的代码:

可以看到 rAF 执行在 layout 与 paint 之前,在每帧只执行了一次 rAF,调用回调函数执行动画。

从 rAF 的执行时机,可以看出 setTimeout 的执行时机与 rAF 的不同。

我们通过对不同方式实现方块移动动画的 performance 抓取,可以看到:

  • setTimeout 单位帧截图:

  • rAF 单位帧截图:

对比两者可以看出,在 16.7ms 的时间里,seTimeout 执行了 4 次,导致此时设置的 marginLeft 和上一次渲染前 marginLeft 的差值要大于 1px 的。

而 raf 可以看出 marginLeft 和上一次渲染前 marginLeft 的差值要等于 1px 的。

从 rAf 的性能,可以看出 setTimeout 的性能会较差一点

那么如果 JS 执行的时间过长,导致在本该绘制一帧的时候,没有绘制,延迟到下一帧的执行绘制的时候,就会造成动画的卡顿。【这里可以跳到第三部分性能问题,就知道直观的看到卡顿】

从而可以总结出:

1.setTimeout 时间不准确,因为他的执行取决于主线程执行的时间。

2.如果计时器频率高于浏览器刷新的频率,即使代码执行了,浏览器没有刷新,也是没有显示的,出现掉帧情况,不流畅。

而 raf 解决了 setTimeout 动画带来的问题:

1.浏览器刷新屏幕时自动执行,无需设置时间间隔
和 setTimeout 一样是 n 毫秒之后再执行,但这个 n 毫秒,自动设置成浏览器刷新频率,浏览器刷新一次,执行一次,不需要手动设置;
浏览器不刷新,就不执行,没有排队掉帧的情况。

2.高频函数节流
对于 resize、scroll 高频触发事件来说,使用 requestAnimationFrame 可以保证在每个绘制区间内,函数只被执行一次,节省函数执行的开销。
如果使用 setTimeout、setInterval 可能会在浏览器刷新间隔中有无用的回调函数调用,浪费资源。

第三部分 性能分析以及高效能的动画

3.1 性能分析

通过 chrome-performance 可以看整体的 fps、GPU 的情况,也可以逐帧去分析影响 scripting\rendering\painting 时间的因素,从而有针对性的提高动画的性能。

demo3:
----- 小方块的上下运动 -----

demo 的在线地址:https://googlechrome.github.io/devtools-samples/jank/

源码截图:

未优化【每个方块都需要强制 layout 去计算 position】:

点击 Optimize 按钮优化后【只读一次,并存在 pos 变量中】:

再次优化【添加 transform:translateZ(0),提高层级】:

以上就是一个动画逐步优化的小案例:具体操作可以查看原文:
https://developer.chrome.com/docs/devtools/evaluate-performance/

3.2 如何优化动画性能

根据上文的渲染机制的讨论,我们可以看出,影响动画渲染的因素就是帧管道所经历的各个阶段,从中我们可以总结一些用来优化动画性能的手段:

  1. 提升每一帧的性能
  • 避免频繁的重排
  • 避免大面积的重绘
  • 优化 JS 的性能
  1. fps 稳定,避免掉帧,跳帧的情况
  • 不在连续动画中,添加高耗能的操作
  • 如果无法避免,看可以在动画的开头或者结尾进行操作
  1. 开启 GUP 加速

第四部分 常用的动画库

综上的实现方式可以支持部分的动画开发,比如点击交互,轮播器、以及纯动画的展示,比如摇钱树、烟花等。

如果需要强交互,或者是需要一个重力世界的时候,原生 JS 的实现相对于困难。可以利用一些动画库,来进行开发,这些动画基于 canvas 与 webGL 实现的。

  1. Pixi.js
  • 添加场景
  • 添加玩家
  • 添加自身动作
  • 添加交互
  1. phaser.js
    物理系统、重力系统
    可以模仿下落状态

  2. 其他:
    create.js、three.js 3d 渲染、layaAir、Egret 3d 游戏引擎等,可以根据不同的场景需要,选择不同的框架使用。

总结

  1. 动画的实现手段

  1. 浏览器渲染的简单流程

  1. 开发动画分析性能参考 performance 的使用

鸣谢

非常感谢木杪、千寻对本文的校正与建议,同时感谢琉易、霜序等伙伴在业务产品技术上帮助与支持。

有关前端动画实现以及原理浅析的更多相关文章

  1. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 阿里云国际版免费试用:如何注册以及注意事项 - 2

    作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。​关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐

  9. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐