浅谈 web 端动画的实现

浅谈 web 端动画的实现

本文也发布在知乎 zhuanlan.zhihu.com/p/373244605

开发移动端项目时,特别是活动页面,免不了会涉及动画。而且好的动画会给产品加分。本文总结了 web 端常见动画实现的方式,希望读者在阅读完本文后,掌握动画开发技巧,更加自信地解决交互设计师提出的需求。

Toolbox

实现 web 端动画有多种方式,常见的有 GIF/APNG,CSS3,JavaScript,lottie,SVG,canvas 等。具体在业务中使用何种方式,需要综合考虑实现成本与运行效率。需要对动画进行较多控制的,使用 JavaScript 或 lottie,其他使用 CSS3 与 GIF/APNG。

APNG

在上述的动画方式中,GIF/APNG 无疑最省力。相较于 GIF,APNG 更有优势(参考 这篇文章 )。简单的场景下,使用 APNG 与 setTimeout 也可以实现流程的控制。 不过于在实际使用的时候,注意以下问题:

  • APNG 预加载

通常 APNG 图片尺寸较大,最好提前加载。

  • APNG 不重复播放

已缓存的 APNG 图片再次显示时,动画不播放。你可以在链接中通过 query 参数来重新加载图片。 Codepen 示例

CSS3

大家应该已经在项目中广泛应用 CSS3 动画,transition/animation 是动画利器。常见的过渡效果通过 CSS 都可以实现。如果没有思路的话,不妨去 Animate.css 或者 Animista.net 看看,也许答案就在上面。

几点提示

  • animation-fill-mode

该属性设置在动画结束后,保持终止状态还是恢复初始状态。经常使用 animation-fill-mode: forwards; 来保持终止状态。

  • animation-function 中的 steps 函数

一般情况下,动画的过渡是连续的。通过 steps 函数可以让动画「断断续续」(每次切换一帧),实现帧动画的效果。借助该特性,实现一个简单的倒计时。 Codepen 示例

  • 好用的 clip-path

clip-path 属性用于控制元素的显示区域。虽然使用 overflow: hidden; 也可以实现部分效果,但是代码量会增多,且对于多边形的裁剪就无能为力。 Codepen 示例

  • 动画性能

尽可能多地使用 transform,有需要时使用 will-change 属性。如果同时要对大量的 DOM 元素做动效,或许你应该尝试使用 Canvas 而非 CSS。

Vue 的封装

Vue (特指 2.x) 对于动画进行了封装,提供 transition 与 transition-group 组件,过渡/动画用一套 API。Vue 中的过渡两个特性值得关注:

  • mode

设置 mode 可以同时对离开与出现的元素添加过渡效果。Codepen 示例

  • transition-group

当你要给多个元素做动画时,可以使用 transition-group组件。移动端场景中常见的跑马灯使用该组件实现。Codepen 示例

Lottie、SVG

对于复杂的动画,推荐使用 Lottie ,并且动画制作软件 AE 支持导出 Lottie 文件。Lottie 使用方式极其简单,导入 JSON 文件完成动画的创建。

import lottie from 'lottie-web';
import animData from './animData.json';

const anim = lottie.loadAnimation(animData);
复制代码

使用 lottie-api ,你甚至可以编辑原有的动画。 Codepen 示例

Lottie 相当于使用 JS 来播放动画,你可以对动画的播放速度,次数,帧数,顺序进行精准的控制。事件或者方法参考 官方文档 ,这里不再赘述。

注意事项

如果 Lottie JSON 文件引入图片资源,要去调整图片的路径,避免出现 404 问题。当然更好的方法是使用自动化工具比如 lottie-loader 来处理 JSON 文件,调整图片路径。在使用了 lottie-loader 的前提下,你可以直接把 JSON 文件当做组件来用。

// 将 JSON 当做 Vue 组件来使用
import MyAnimate from './data.json'

export default {
    components: { MyAnimate }
}
复制代码

Lottie JSON 字段含义

遗憾的是, Lottie 官方文档并没有介绍 JSON 中各字段的含义,只能在代码仓库中找到一些 JSON schema 描述文件 。下面对部分字段做简要描述,当你有定制化需求时,或许需要:

{
    "fr"20,        // 每秒播放的帧数
    "ip"0,         // 动画开始的开始帧数
    "op"40,        // 动画开始的结束帧数
    "w"700,        // 动画内容区域宽度
    "h"500,        // 动画内容区域高度
    "assets": [{    // 图片资源,为避免图片 404 问题,你可能需要编辑这里
        "w"120,   // 图片宽度
        "h"120,   // 图片高度
        "u""images/"// 图片路径
        "p""img_0.png"// 图片名称
    }]
}
复制代码

fr , ipop 可知动画播放一个周期需要 2s。通过调用 setSeed(2) 可以将播放时间降为 1s;通过 play(frame) 或者 goToAndPlay(frame) 类似的方法设置动画从某一帧开始播放。

你可以编写一个简单的 lottie-loader 来处理 assets 中的图片路径

动画实践指南

借助上文中提到了多种工具,处理简单的动画不在话下。对于稍微复杂的动画,要用 JS 去编写动画逻辑。以下面的动画效果为例,来讲解实现思路。

效果图尺寸较大,可以点击 这里 预览效果。

通过分析发现,主要的逻辑集中在红包上。红包有停止,暂停两种状态;按照某个频率从顶部还会落下新的红包;按照某个频率移除可视区范围外的红包;移动的过程中旋转红包。 编写代码时,为了提高灵活性,最常用的策略是数据抽象过程抽象。先进行数据抽象,把红包雨整个区域用数据来描述。

// 动画区域
class Stage {
  // ...
  children: Packet[]
  durationnumber
  destroyedboolean
}

class Packet {
  xnumber
  ynumber
  degreenumber
  rotation'clockwise' | 'anticlockwise'
  status'idle' | 'moving' | 'removed'
}
复制代码

接下来,进行过程抽象,添加方法,让数据动起来。由于存在多种频率不同的动画,把一种动画想象成一个 task,用 async 函数描述过程。

class Stage {
  init() { }

  // 按照一定的频率添加新的红包
  async add() {
    while (true) {
      if (this.destroyed) return
      await wait(200)
      // 执行动画逻辑 ...
    }
  }
  // 与 add 类似,让红包移动,同时做清理工作
  async animateAndClear() { }
}

class Packet {
  // 设置停止
  stop() { }
}
复制代码

通过前面两步,完成了对整个动效的抽象。此时,动画已经是「运动」的了。接下来都是 View 层的工作。借助我们已经掌握的技能,View 层不难实现。 在项目开发过程中,使用的框架是 Vue。如果你想用 Canvas 去实现,数据层可以复用,只需要针对 View 层做适配工作即可。

状态的复用与组件抽象

React 与 Vue3 都提供了对 Hook 的支持,数据或者处理数据的逻辑(Hook)是可以复用的。在基础组件库日益丰富的当下,View 层的工作以组合基础组件为主,相当多的业务逻辑是处理数据。回到刚才的例子,我们将先考虑状态的设计,再剥离状态,类似于对 Lottie/Hook 的简单模仿。毕竟调整数据的配置比编写业务代码成本低得多。

Hook 与动画

Vue 的封装/mode小节中的示例如果用 Hook( ReactSpring ) 来实现的话,核心代码如下:

import { useTransition, animated } from "react-spring";

export default function App() {
  // ...
  const transitions = useTransition(show, null, {
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  });

  return <div>...</div>
}
复制代码

完整的代码

从上面的例子可以看出,我们只需要设置好初始终止状态,然后再与 View 层做绑定,整个动画就完成了,便捷程度堪比 CSS3。在先前 Vue 的实现中,依赖 transition 组件的功能,而 ReactSpring 则是整个过渡的状态提供给你,你决定如何去渲染。

如果后续需要对该场景进行封装,那么大概会这样拆分:

  • 过渡状态抽象成 Hook,对外导出
  • 基于 Hook 实现一个组件,默认导出该组件
  • 用户如果对组件不满意,是完全可以基于 Hook 实现新的组件,代码量并不大。

随着 Hook 概念的推广,我们也要逐渐学习掌握它。开发项目过程中,有意识地去思考如何对数据进行抽象,如何更好的管理数据。

本文未涉及的内容

本文主要讨论的是 web 端简单的动画实现方式。对于更为复杂的场景,如 HTML 5 游戏,为了性能与开发效率的考量,建议使用 pixi.js 或者 phaser 之类的游戏框架。

总结

  • 丰富你的工具箱,掌握更多工具
  • 对需求进行拆分,对数据抽象/过程进行抽象,提高可维护性
  • 学习 Hook,思考状态逻辑如何复用

Supongo que te gusta

Origin juejin.im/post/7077535015944323102
Recomendado
Clasificación