1
头图

前端项目直面客户,为了更好的交互体验,免不了需要使用动画来增香提味。在此分享自如动画的尝试与探索。

一、简单的过渡效果transition

使用transition实现:位移、旋转、缩放、透明度等简单过渡效果。
image

优点:流畅、简单。

缺点:仅能实现简单动画,仅能监控动画结束(transitionend),无法实现用户交互。

transitionend(结束)事件监控:

<template>
 <div class="box1" ref="box1" :class="{move: isMove}" @click="isMove=!isMove"></div>
</template>
<script lang="tsx">
import { Component, Vue } from 'vue-property-decorator';
@Component({ name: 'index' })
export default class Index extends Vue {
 private isMove: boolean = false;
 private transitions: any = {
    'transition':['transitionend'],
    'OTransition':['otransitionend'],
    'MozTransition':['mozTransitionEnd'],
    'WebkitTransition':['webkitTransitionEnd']
 };
 mounted(){
     let _style = (this.$refs.box1 as any).style;
     for(let t in this.transitions){
         if( _style[t] !== undefined ){
         this.transitions[t].map((element:string)=>{
             (this.$refs.box1 as any).addEventListener(element, ()=>{this.callBack(element)}, false);
             });
         }
     }
 }
 callBack(type:string){
     // do something
     console.log(type); // transitionend
 }
}
</script>
<style lang="scss" scoped>
.box1{
     width: 100px;
     height: 100px;
     background: #999;
     transition: all 0.5s;
     &.move{
        transform: translateX(100px);
     }
}
</style>

二、逐帧动画

image
动画的速度曲线 animation-timing-function :
image

除了以上常规用法,还有一个实用的函数:

阶梯函数:steps(n,direction),这个函数能够起到定格动画的效果。阶梯函数不像其他定时函数那样,平滑的过渡,而是以帧的方式过渡

n:阶梯数(必须是一个正整数),它将动画的总时长按照阶梯数等距划分

direction:可选值为start或end,默认end

start表示动画的第一帧会被立即执行,直接从第二帧开始,然后以第一帧结束;

end则表示动画从第一帧开始到正常结束;
image
优点:能实现较复杂的动画

缺点:图片资源容易过大,

仅能监控动画开始(transitionstart)、结束(transitionend)、重复(animationiteration),无法实现用户交互。

1、实现

1)生成雪碧图(横向纵向均可),在线生成雪碧图地址:

https://www.toptal.com/develo...
image
2)使用animation的steps实现动画:

<template>
 <div class="canvas">
 <div  v-for="(item,index) in list" :key="index" :class="`step ${item.className||''} ani-${item.imageDirection=='v' ? 'tb' : 'lr'}-step-${item.steps||1}`"
 :style="{
 'backgroundImage':`url(${item.backgroundImage})`,
 'width':`${item.width}px`,
 'height':`${item.height}px`,
 'top':`${item.y/75}rem` ,
 'left':`${item.x/75}rem` ,
 }"></div>
 </div>
</template>
<script lang="tsx">
import { Component, Vue } from 'vue-property-decorator';
import { girl, paper, son, run } from '../assets';
interface AnimateItem{
     backgroundImage: string; // 图片地址
     x: number; // 定位x轴,750宽度下px值
     y: number; // 定位y轴,750宽度下px值
     width: number; // 宽,单位px
     height: number; // 高,单位px
     imageDirection?: 'v' | 'h'; // 图片是横向还是纵向
     steps?: number; // 图片steps
     className?: string; // class类名
}
@Component({ name: 'index' })
export default class Index extends Vue {
 private list: Array<AnimateItem> = [
 { backgroundImage: son , x: 185, y: 20, width: 185, height: 319, imageDirection: 'v', steps: 18},
 { backgroundImage: paper , x: 15, y: 30, width: 115, height: 175, imageDirection: 'v', steps: 24, className: 'paper'},
 { backgroundImage: girl , x: 115, y: 589, width: 320, height: 391.5, steps: 12},
 { backgroundImage: run , x: 515, y: 569, width: 88.83333, height: 54, steps: 6},
 ];
}
</script>
<style scoped lang="scss">
.canvas {
     position: relative;
     width: 100%;
     height: r(1305);
     .step {
         position: absolute;
         // 一定不能写该属性,否则动画会有问题
         // background-repeat: no-repeat;
     }
     .paper{
         transform-origin: left top;
         transform: scale(.65);
    }
}
// 高图
@each $step,$time in (18,1.8),(24,4) {
 .ani-tb-step-#{$step}{
     background-size: 100% auto;
     animation: step-tb-#{$step} $time*1s steps($step) infinite;
 }
};
// 宽图
@each $step,$time in (12,1.5),(6,3) {
 .ani-lr-step-#{$step}{
     background-size: auto 100%;
     animation: step-lr-#{$step} $time*1s steps($step) infinite;
 }
};
// 高图动画
@each $steps in 18,24 {
 @keyframes step-tb-#{$steps} {
 0% {
    background-position: 0 0;
 }
 100% {
    background-position: 0 #{-1*$steps*100%};
 }
 }
}
// 宽图动画
@each $steps in 12,6 {
 @keyframes step-lr-#{$steps} {
 0% {
    background-position: 0 0;
 }
 100% {
    background-position: #{-1*$steps*100%} 0;
 }
 }
}
</style>

image

// src/assets/index.ts
export { default as girl } from './banner/girl.png'
export { default as paper } from './banner/paper.png'
export { default as son } from './banner/son.png'
export { default as run } from './banner/run.png'
预览地址:
https://topic.ziroom.com/2021...

css3动画注意事项:

1)动画元素使用绝对定位(absolute/fixed),使其脱离文档流,有效避免重排。

2)使用transform:translateY/X来移动元素,而不是修改left、margin-left等属性

3)逐帧动画(雪碧图+animation steps)元素宽高使用px

4)一定不能写 background-repeat: no-repeat; 属性,否则动画会有问题

5)出现卡顿或闪烁,可以开启硬件加速

.cube {
     -webkit-backface-visibility: hidden;
     -moz-backface-visibility: hidden;
     -ms-backface-visibility: hidden;
     backface-visibility: hidden;
     -webkit-perspective: 1000px;
     -moz-perspective: 1000px;
     -ms-perspective: 1000px;
     perspective: 1000px;
     /* Other transform properties here */
}

在webkit内核的浏览器中,另一个行之有效的方法是:

.cube {
     -webkit-transform: translate3d(0, 0, 0);
     -moz-transform: translate3d(0, 0, 0);
     -ms-transform: translate3d(0, 0, 0);
     transform: translate3d(0, 0, 0);
     /* Other transform properties here */
}

2、animationstart(开始)、animationend(结束)、animationiteration(重复运动)事件监控:

<template>
 <div class="box1" ref="box1" :class="{move: isMove}" @click="isMove=!isMove"></div>
</template>
<script lang="tsx">
import { Component, Vue } from 'vue-property-decorator';
@Component({ name: 'index' })
export default class Index extends Vue {
 private isMove: boolean = false;
 //监听animation动画开始、结束、重复运动方法
 private animations = {
     'animation':['animationstart', 'animationend', 'animationiteration'],
     'OAnimation':['oanimationstart', 'oanimationend', 'oanimationiteration'],
     'MozAnimation':['mozAnimationStart', 'mozAnimationEnd', 'mozAnimationIteration'],
     'WebkitAnimation':['webkitAnimationStart', 'webkitAnimationEnd', 'webkitAnimationIteration']
 };
 mounted(){
 let _style = (this.$refs.box1 as any).style;
     for(let t in this.animations){
         if( _style[t] !== undefined ){
             this.animations[t].map((element:string)=>{
             (this.$refs.box1 as any).addEventListener(element, ()=>{this.callBack(element)}, false);
             });
         }
     }
 }
 callBack(type:string){
     console.log(type);
     let _type = type.toLowerCase();
     if(_type.endsWith('animationend')){
        this.isMove=false;
    }
 }
}
</script>
<style lang="scss" scoped>
.box1{
     width: 100px;
     height: 100px;
     background: #999;
     &.move{
        animation: move 1s 2; // 循环2次
     }
}
@keyframes move {
 0%{
    transform: translateX(0px);
 }
 50%{
    transform: translateX(100px);
 }
 100%{
    transform: translateX(0px);
 }
}
</style>

打印结果:
image
注:

让动画停留在某一帧(一般是最后一帧)的方法:

1)监控animationend,添加新的class类,设置background-position属性(例如:0 -300%)。(animation-fill-mode:forwards;针对transform方式修改的属性在动画结束时可以保留状态,background-position的改变不会起作用)

三、Lottie(扁平化风格,路径剪辑动画)

Lottie是Airbnb推出的支持Web、Android、iOS等多平台的动画库。设计师在AE上完成动画后,可以使用它在AE中的插件Bodymovin输出一个Json格式的文件,Json文件中就包含了制作动画所包含的各种图层元素及效果的动画关键帧等内容。

lottie官方:https://airbnb.design/lottie/

支持功能列表:https://airbnb.io/lottie/#/su...
image

四、SVGA

image
效果演示:

https://topic.ziroom.com/2021...

SVGA 是一种跨平台的开源动画格式,同时兼容 iOS / Android / Flutter / Web。

SVGA 除了使用简单,性能卓越,同时让动画开发分工明确,各自专注各自的领域,大大减少动画交互的沟通成本,提升开发效率。动画设计师专注动画设计,通过工具输出 svga 动画文件,提供给开发工程师在集成 svga player 之后直接使用。

SVGA官网:http://svga.io/index.html
image

1、集成指南(将播放器集成至 iOS / Android / Flutter / Web / WeChat):

SVGAPlayer-iOShttps://github.com/svga/SVGAP...

SVGAPlayer-Androidhttps://github.com/svga/SVGAP...

SVGAPlayer-Flutterhttps://github.com/svga/SVGAP...

SVGAPlayer-Webhttps://github.com/svga/SVGAP...

SVGAPlayer-WeChathttps://github.com/svga/SVGAP...

2、使用方法(Web)

1)安装SVGAPlayer
​npm install svgaplayerweb --save
2)导入
import SVGA from 'svgaplayerweb';
3)入需支持音频播放,引入
<script src="https://cdn.jsdelivr.net/npm/howler@2.0.15/dist/howler.core.min.js"></script>
4)添加容器
<div id="testCanvas" style="styles..."></div>

<canvas id="testCanvas" width="750" height="750"></canvas>
5)加载动画

var parser = new SVGA.Parser(); // 创建解析器
var player = new SVGA.Player('#testCanvas'); // 创建播放器
// 只能加载跨域允许文件
parser.load("../kingset.svga", videoItem => {
 player.setVideoItem(videoItem);
 player.startAnimation();
}, error => {
 // alert(error.message);
})

3、播放器SVGA.Player

用于控制动画的播放和停止
1)属性:

loops: number; 
// 动画循环次数,默认值为 0,表示无限循环
​
clearsAfterStop: boolean; 
// 默认值为 true,表示当动画结束时,清空画布。
​
fillMode: "Forward" | "Backward"; 
// 默认值为 Forward,可选值 Forward / Backward,
// 当 clearsAfterStop 为 false 时,
// Forward 表示动画会在结束后停留在最后一帧,
// Backward 则会在动画结束后停留在第一帧。

2)方法:
动态图片(只能加载跨域允许文件)

// setImage(urlORbase64: string, forKey: string)
// urlORbase64:图片地址
// forKey: ImageKey
player.setImage('../avatar.png', '99')

动态文本

// setText(textORMap: string | {text: string,size?: string,family?: string,color?: string,offset?: { x: number, y: number }}, forKey: string)
// forKey: ImageKey
// 默认文字样式:14px 黑色
player.setText({
 text: '我的女王',
 family: 'Arial',
 size: "30px",
 color: "#fff",
 offset: {x: -10, y: 2}
}, 'banner');

播放动画

// startAnimation(reverse: boolean = false);
// reverse: 是否反向播放动画
player.startAnimation();

播放 [location, location+length] 指定区间帧动画

// startAnimationWithRange(range: {location: number, length: number}, reverse: boolean = false);
// reverse: 是否反向播放
player.startAnimationWithRange({location: 15, length: 35}, false);

暂停在当前帧
pauseAnimation();
停止播放动画,如果 clearsAfterStop === true,将会清空画布
​stopAnimation();
强制清空画布
clear();
清空所有动态图像和文本
clearDynamicObjects()
3)回调方法:
动画停止播放时回调
onFinished(callback: () => void): void;
动画播放至某帧后回调

// onFrame(callback: (frame: number): void): void;
// frame: 当前帧
player.onFrame(frame=>{
 if(frame==50){
 // do something
 }
});

动画播放至某进度后回调
onPercentage(callback: (percentage: number): void): void;
注:

1)只能加载跨域允许文件,包括svga文件和图片文件(上传到服务器上,远程访问)

4、apng图片及好用的辅助工具

1)什么是apng图片?

APNG是普通png图片的升级版(可以动的png),它的后缀依然是.png,可以展示动态,支持全彩和透明(最重要),向下兼容PNG(包含动态的情况下体积会比普通静态png大出数倍,但文件体积比gif小且效果更好,可以压缩)。

2)apng VS gif

颜色画质透明兼容性
gif8 位 256 色(色阶过渡糟糕,图片具有颗粒感)GIF 每个像素只有 8 bit,也就是说只有 256 种颜色,于是很多人误以为 GIF 不支持 24 bit RGB,但实际上,GIF 的限制是每一帧最多只能有 256 种颜色,但是每种颜色可以是 24 bit 的。不支持 Alpha 透明通道,边缘有杂边。不支持半透明,只支持完全透明或者完全不透明,如果把一个边缘是半透明的图片转换成 GIF,就会出现另一个答案中提到的杂边问题 。ALL
apng24 位真彩色图片支持 8 位 Alpha 透明通道,透明度可以有 256 级Firefox、Safari、Chrome

3)apng VS 逐帧动画

逐帧动画apng展示
文件image戳此查看apng按扭>>
文件体积222kb压缩前:200kb;压缩后:112kb

4)一款高效、好用的图片处理工具——Cherry

官网:https://yyued.github.io/cherry/
功能:
image

总结

1)简单补间动画(位移、旋转、缩放、透明度):
使用css3,保证性能是第一位

2)稍复杂小动画(动画简单、尺寸小,循环无交互):
使用apng,好实现、体验好(设计师导出序列帧或者svga,使用文末提到的工具转成apng图片,注意如果动画复杂且尺寸较大,apng的图片会非常大)

3)复杂动画(动画复杂,尺寸较大,或者有交互):
使用svga,文件体积小,开发成本低,效果好,能实现交互。

作者简介:暴力燕,自如大前端开发中心-客端研发组

image


自如大前端
0 声望1 粉丝

引用和评论

0 条评论