【青训营】- 前端动画实现

410 阅读14分钟

前端动画实现

一.动画的基本原理

1.动画是什么

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

在很早以前的古人就开始有动画了,下面是五千年前古人的画,用今天的技术把这几张连续拼接起来就能看到一只运动的🐏

image-20210902094318205.png

比如我们常说的"帧",一般电影和电视就是每秒24帧,就是每秒钟有24张图片,因为相邻的图片差异化非常小,播放的速度又快,我们的视觉残留效果就会呈现一个连续的画面.

2.动画的发展史

最开始的计算机动画就是连续的图片,使用gif格式让图片存贮为可以动的图像.后来flash的兴起,可以制作相对复杂,效果更好的动画,一时间flash动画风靡全球,网页上动画基本被flash一家承包.

直到苹果公司的移动设备出现,苹果公司认为flash动画播放时,对内存和处理器的占用非常大,这会对脆弱的移动设备产生损害,于是开始弃用flash.由于苹果公司的市场份额越占越大,于是世界各地纷纷跟着弃用flash.

flash动画衰落的原因还有一个就是adobe公司把flash的运营交给国内厂商,国内厂商不思进取,flash优化做的贼差,又是一堆广告,渐渐的被逐出市场

在flash退出后,一时间没有更好的替代品,大家纷纷用回gif的图片代替动画,直到CSS3的问世,网页上可以用css代码写动画之后,gif才慢慢被替代

 

3.计算机动画原理

计算机图形学:

计算机视觉的基础,涵盖点、线、面、体、场的数学构造方法。

  1. 几何和图形数据的输入、存储和压缩
  2. 描述纹理、曲线、光影等算法
  3. 物体图形的数据输出(图形接口、动画技术),硬件和图形的交互技术
  4. 图形开发软件的相关技术标准

计算机动画:

计算机图形学的分支,主要包含2D、3D动画

image-20210902101945403.png

image-20210902101957085.png

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

举个栗子:上面的球球开始在左边位置,结束的时候在右边位置,那么中间是怎么运动的,这个我们可以自定义插值。可以让它直线运动,也可以曲线运动,甚至在画面外溜达一分钟掉下来都可以。但是如果只有任意一个状态,我们不知道这个小球结束位置在哪和开始位置在哪,就没办法定义它的状态。

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

手机上叫做刷新率,比如现在最高为120Hz刷新率,即每秒屏幕刷新120次,动画效果非常流畅

image-20210902103134379.png

上面小球的例子,我们把小球两个位置状态中间的帧补上,形成一个连续的动画,这个就叫补间动画

  • 补间动画(关键帧动画)

    传统动画,主画师绘制关键帧(例如人物的关键动作的中间帧,抬手到落手),交给清稿部门,清稿部门的补间动画师补充关键帧进行交付(人物抬手到落手中间的动作状态)。(在网页的渲染中,补间动画师的角色由浏览器来担任,如keyframe,transition)

  • 逐帧动画(Frame By Frame)

    从词语的字面意思来说,就是全篇每一帧都是纯手绘。(如steps实现的雪碧图)

二.前端动画的分类

前端的动画大致分为三类,一个css3的新的动画特性,svg是对元素进行绘画,js动画是利用js代码获取dom元素,改变dom元素的状态和位置形成动画

1.CSS

CSS的形体变化———— Transform API

> 只能转换由盒模型定位的元素。根据经验,如果元素具有dispaly:block,则由盒模型定位元素。
<transform-function> == 
<matrix()> | 
<translate()> | 
<translateX()> | 
<translateY()> | 
<scale()> | 
<scaleX()> | 
<scaleY()> | 
<rotate()> | 
<skew()> | 
<skewX()> | 
<skewY()> | 
<matrix3d()> | 
<translate3d()> | 
<translateZ()> | 
<scale3d()> | 
<scaleZ() | 
<rotate3d()> | 
<rotateX()> | 
<rotateY()> | 
<rotateZ()> | 
<perspective> 

Translate(移动)

image-20210902104739204.png

该变换由二维向量构成。它的坐标定义了元素在每个方向上移动了多少

transform: translate(200px)   /* 每个方向移动200px*/
transform: translate(50%)   /* 每个方向移动物体自身的50% *//* 可以对每个方向上的单位自定义,每个方向的单位计算都是独立的,不会互相影响 */
transform:translate(100px,200px)  /* x方向100px y方向200px */
transform:translate(100px,50%)  /* x方向100px y方向物体y方向长度的50% */
transform:translate(30%,200px)  /* 物体x方向长度的30% y方向200px */

scale(缩放)

image-20210902105422890.png

/*只有一个参数的时候 是沿x轴缩小50% */
transform:scale(0.5)
​
/*两个参数时,第一个是x方向,第二个是y方向  沿x轴方向缩小原来的50%,沿y轴方向放大原来的2倍*/
transform:scale(0.5,2)

rotate(旋转)

image-20210902105731936.png 旋转的中心点默认为center,物体的中心点

transform:rotate(30deg)
/* 沿顺时针方向旋转30度 */

skew(倾斜)

image-20210902111630045.png

transform:skew(30deg,10deg)

参数表示倾斜角度,单位deg

一个参数时:表示水平方向的倾斜角度

两个参数时:第一个参数表示水平方向上的倾斜角度,第二个参数表示垂直方向的倾斜角度

首先需要说明的是skew的默认远点transform-origin是这个物体的中心点

CSS实现补间动画(Transform API ,Keyframe)

image-20210902112042769.png

Transition API (过渡动画)

dom加载完成或class反生变化时触发

div {
    transition:<property> <duration> <timing-function> <delay>
}
/* transition-property  指定哪或哪些CSS属性用于过渡
    transition-duration 指定过渡时长
    transition-timing-function  指定一个函数,定义属性值怎么变化
    transition-delay  指定延迟,即属性开始变化时与过渡开始发生时之间的时长
*/

keyframe实现动画

关键帧@keyframe at-rule 规则通过在动画序列中定义关键帧(或waypoints)的样式来控制CSS动画序列中的中间步骤。和转换 transition相比,关键帧keyframe可以控制动画序列的中间步骤

@keyframes slidein {
    from {
        transform: translateX(0%)
    }
    to {
        transform: translateX(100%)
    }
}
/*实现物体在x轴上,从0位置移动到100% 的地方*/@keyframes identifier {
    0% {top:0;left:0;}
    30% {top:50px}
    68%,72% {left:50px}
    100% {top:100px;left:100%}
}
/*上面对物体的变换设置的更加详细,制定了出去开始和结束以外的30%处、68%、72%处的关键帧状态*/

创建好了关键帧的状态,还需要让物体动起来

#alice {
    animarion: identifier  infinite 3s linear
}
/*指定名为identifier的动画,在3s内完成,缓动转态为线性变化*/

CSS实现逐帧动画

image-20210902113826024.png

上面的api图可以看到,step里面有两个参数,一个数代表实现这个动画一共需要几步,1就是一步完成,后面的start意思是以结束状态为起始状态,从上图的左上部分可以看到,小黑点的结束状态在右上,因为只有一步完成,起始状态就是结束状态,那么在动画开始之前就同步完成。end就是不以结束状态为起始状态,在上图的右上部分可以看到,小球的结束状态在右上,起始状态在左下,在动画开始之前起始状态都没变,开始之后一步走到结束状态。其他步骤以此类推

.sprite {
    position:absolute;
    height:500px;
    width:420px;
    transform:scale(0.4);
    animation:0.9s run-h steps(14) infinite;
    background-image:url(test.png);
}
​
@keyframe run-h {
    to{
        background-position:-5888px 0
    }
}

精灵图.gif

这是一个精灵图,精灵图就是一个非常大的图片,里面会有非常多的子画面,我们控制精灵图在视图窗口的位移来显示不同的区域

上面代码就是将这个非常长的精灵图分为14步进行位移,0.9s完成位移,循环播放,就会出现上面动态的小精灵

总结

优点:简单高效,声明式的,不依赖于主线程,部分采用硬件加速(GPU),简单的控制keyframe animation播放暂停

缺点:不能动态修改或定义动画内容,不同的动画无法实现同步,多个通话彼此无法堆叠

适用场景:简单的h5活动/宣传页

推荐库:animation.css shake.css等

2.SVG

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

SMIL

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

<svg width="320" height="320" xmlns="http://www.w3.org/2000/svg">
    <g>
        <text font-family="microsoft yahei" font-size="120" y="160" x="160"></text>
        <animateTransform attributeName="transform" brgin="10s" type="rotate" from="0 160 160" repeatCount="indefinete" />
    </g>
</svg>

image-20210902120444463.png

兼容性不是很好,咱不讨论此方法

JS

使用JS来操作SVG动画,网上已经有很多现成的类库。例如老牌的Snap.svg 以及anime.js 都能让我们快速的制作SVG动画。当然,除了这些类库,HTML本身也有原生的Web Animation 实现。使用 Web Animation也能让我们方便快捷的制作动画

文字形变:codepen.io/jiangxiang/…

Path实现写字动画:codepen.io/jiangxiang/…

文字.gif

描边.gif

js笔画原理

stroke-dashoffset、stroke-dasharray  配合使用实现笔画效果
​
stroke-dasharray:<length> | <percentage> (需要填充的路径)
​
stroke-dashoffset:<length> | <percentage> (dash模式开始位置的偏移量)

path路径 - d属性(路径描述)

*大小写字母跟随的是绝对坐标x,y,小写为相对坐标dx,dy

M/m绘制起始点

L/l绘制一条线段

C/c为绘制贝塞尔曲线

Z/z将当前点与起始点用直线连接

image-20210902145741678.png

计算path的长度 - path.getTotallLength()

计算path上某个点的坐标 - path.getPointAtLength(lengthNumber)

例子:codepen.io/jiangxiang/…

CSS

css主要是使用animation,transform,transition来实现动画,它比JS更加简单方便

总结

优点:通过矢量元素实现动画,不同的屏幕下均可获得较好的清晰度。可以实现一些特殊效果:描字,形变,墨水扩散等

缺点:使用方式较为复杂,过多使用可能会带来性能问题

3.JS动画

image-20210902143127266.png

JS可以实现复杂的动画,也可以操作canvas动画API上进行绘制

总结

优点:

  • 使用灵活,同样在定义一个动画的keyframe序列时,可以根据不同的条件调节若干参数(JS动画函数)改变动画方式。(CSS会有非常多的代码冗余)
  • 对比与CSS的keyframe粒度更粗,css本身的时间函数是有限的,这块JS都可以做弥补
  • CSS很难做到两个以上的状态转化(要么使用关键帧,要么需要多个动画延时触发,再想要对动画循环播放或暂停倒序等,复杂度极高)

缺点:

  • 使用到JS运行时,调优方面不如CSS简单,CSS调优方式固定
  • 对于性能和兼容性较差的浏览器,CSS可以做到优雅降级,而JS需要额外代码兼容

三.实现前端动画

JS动画函数封装

函数选择

requestAnimationFrame vs setTimeout vs srtInterval

JavaScript动画应该通过requestAnimationFrame,该内置方法允许设置回调函数以在浏览器准备绘制时运行。通过这很快,但确切的时间取决于浏览器

当页面在后台时,根本没有重绘,所以回调不会运行:动画将被暂停并且不会消耗资源

function animate({timing,draw,duration}){
    let start = performance.now()
    
    requestAnimationFrame(function animate(time){
        //timeFraction为时间段,值为0-1之间
        let timeFraction = (time - start) / duration
        if(timeFraction > 1 ) timeFraction = 1
        
        // 计算当前的动画状态
        let progress = timing(timeFraction)
        
        draw(progress)   //绘制
        
        if(timeFraction < 1 ){
            requestAnimationFrame(animate)
        }
    }) 
    
}

以上就是一个简单的js动画封装函数,timeFraction是时间段,表示当前动画完成的状态,大于1代表完成,小于1再给一个timing函数

performance.now()和其他时间函数(如Date.now())的区别

performance.now()会以恒定速度自增,精确到微秒,不易被篡改。获取从0到1的时间分数,返回动画进度,通常是0-1

duration 以毫秒为单位的总动画时间

draw 绘制动画的函数

简单动画

image-20210902152328488.png

动画可以简单的类比成速度*时间

匀速运动

image-20210902153739760.png

const draw = (progress)=>{
    train.style.transform = `translate(${progress}px,0)`
}
​
//沿着x轴匀速运动
animate({
    duration:1000,
    timing(timeFraction){
        return timeFraction * 500
    },
    draw
})

如上所示,当时间确定,速度不变的情况下,就是匀速运动

重力(匀加速运动)

image-20210902155322226.png

image-20210902155332524.png

上面我们可以看到,重力是一个匀加速直线运动,加速度不变,速度是越来越快的

const draw = (progress) =>{
    train.style.transform = `translate(0,${500 *(progress - 1)}px)`
}
​
//沿着x轴匀速运动
animate({
    duration:1000,
    timing(timeFraction){
        return timeFraction ** 2
    },
    draw,
})

摩擦力(匀减速运动)

image-20210902155334793.png

与重力相反,摩擦力运动是匀减速运动,摩擦力恒定,减速度不变,速度越来越小

//初始高度500px
const draw = (progress) =>{
    train.style.transform = `translate(0,${500 *(progress - 1)}px)`
}
​
//沿着x轴匀速运动
animate({
    duration:1000,
    timing(timeFraction){
        //初始速度系数为2
        return timeFraction *(2 - timeFraction)
    },
    draw,
})

平抛(x轴匀速,y轴加速)

image-20210902153739760.png image-20210902155741005.png

平抛可以分解为水平方向上的匀速运动,加上垂直方向上的重力运动

const draw = (progress) =>{
    train.style.transform = `translate(${500 *(progress)}px,${500 *(progress - 1)}px)`
}
​
//沿着x轴匀速运动  和沿着y轴加速运动
animate({
    duration:1000,
    timing(timeFraction){
        //初始速度系数为2
        return {
            x: timeFraction,
            y: timeFraction ** 2
        },
    },
    draw,
})

旋转 + 平抛 (x轴匀速,y轴加速,旋转匀速)

const draw = (progress) =>{
    train.style.transform = `translate(${500 *(progress)}px,${500 *(progress - 1)}px) rotate(${2000 * progress.rotate}deg)`
}
​
//沿着x轴匀速运动  和沿着y轴加速运动  本身自转
animate({
    duration:1000,
    timing(timeFraction){
        //初始速度系数为2
        return {
            x: timeFraction,
            y: timeFraction ** 2,
            rotate:timeFraction,
        },
    },
    draw,
})

拉弓(x轴匀速 + y轴初始速度为负的匀加速)

image-20210902162702331.png

通过上面的运动曲线,我们也可以类比为上抛运动,上抛相比平抛,多了一个在竖直方向上的一个匀减速运动

const back  = (x,timeFraction) =>{
    return Math.pow(timeFraction,2) * ((x +1) * timeFraction - x)
}
const draw = (progress) =>{
    train.style.transform = `translate( ${ 200 * progress.x }px , ${ - 500 * progress.y }px )`
}
animate({
    duration:1000,
    timing(timeFraction){
        return {
            x:timeFraction,
            y:back(2,timeFraction),
        }
    },
    draw
})

弹跳小球

使用缓动函数实现

使用缓动函数实现

const bounce = (timeFraction) =>{
    if(timeFraction < 1 / 2.75) {
        return 7.5625 * timeFraction * timeFraction
    }else if(timeFraction < 2 / 2.75){
        return 7.5625 * (timeFraction - = 1.5 /2.75) * timeFraction + 0.75
    }else if(amount < 2.5 / 2.75){
        return 7.5625 * (timeFraction - = 2.5 /2.75) * timeFraction + 0.9375
    }else{
        return 7.5625 * (timeFraction - = 2.625 /2.75) * timeFraction + 0.984375
    }
}
const draw = (progress)=>{
    train.style.transform = `translate(${ 200 * progress.x }px, ${200 * (progress.y - 1)}px)`
}
animate({
    duration:1000,
    timing(timeFraction){
        return {
            x:timeFraction,
            y:bounce(timeFraction),
        }
    },
    draw
})

自动衰减实现

(async function(){
    let damping = 0.7,
        duration = 1000,
        height = 300
    
    while(height > 1){
        const down = (progress) =>{
            train.style.transform = `translate(0,${height * (progress - 1)}px)`
        }
        await animate({
            duration:duration,
            timing(timeFraction){
                return timeFraction ** 2
            },
            draw:down
        })
        height *= damping ** 2
        duration *= damping
        const up = (progress) =>{
            train.style.transform = `translate(0,${- height * progress}px)`
        }
        await animate({
            duration:duration,
            timing(timeFraction){
                return timeFraction * (2 - timeFraction)
            },
            draw: up
        })
    }
})

.相关实践

动画资源

动画代码示例:

codepen.com

codesandbox.com

设计网站:

dribbble.com

动画制作工具

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 是一个用于在HTML5中构建高性能交互式2D内容的库

-Fabric.js 支持动画的JavaScript画布库

-Paper.js 矢量图形脚本的瑞士军刀

-Scripttographer 使用HTML5 Canvas移植到JavaScript和浏览器

-Pixijs 使用最快、最灵活的2D WebGL渲染器创建精美的数字内容

工作实践

动画帧-> 代码 & 设计文件 -> 代码的转换

-需要完全前端自己开发

使用已经封装好的动画库,从开发成本和体验角度出发进行取舍

-设计不是很空

清晰度,图片格式可以指定,动画尽量给出示意机或者相似案例参考

索要精灵资源、资源等需要帮忙压缩

-设计资源充足

要求设计导出lottie格式文件

Lottie是可应用于Android,iOS,Web和Windows的库,通过Bodymovin解析AE动画,并导出可在移动端和web端渲染动画的json文件

动画的优化

性能优化

关于页面渲染的性能可以参考这篇文章

为什么css动画比JavaScript控制动画高效?

文章详细讲述了css在渲染时的性能问题

在实际的应用里,最为简单的一个注意点就是,触发动画的开始不要用display:none属性值,因为它会引起Layout、Paint环节,通过切换类名就已经是一种非常好的方法

translate属性值来替换top/left/right/bottom的切换,scale属性值替换width/height,opacity属性替换display/visibility等等

image-20210902171036752.png

动画优化

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

CSS中的一下几个属性能触发硬件加速:

  1. transform
  2. opacity
  3. filter
  4. Will-change

如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速

算法优化

-线性函数代替真实计算

-几何模型优化

-碰撞检测优化

内存/缓存优化

离屏绘制