大前端动画

3,949 阅读7分钟

大前端开发中经常会遇到动画的开发,那么什么是动画?在物理学中运动就是研究物体在时间维度和空间维度上改变的现象,所以动画也一样,动画主要研究2个因素,发生运动物体的时间空间

Web前端开发中的动画

在 Web 前端开发中实现动画有2种方式。要么依靠 CSS 实现动画,要么依靠 JS 控制实现动画。

CSS 实现动画

首先要说 CSS 中的4个概念:animation、transition、transform、translate

属性 含义
transition(过度动画) 用于设置元素的样式过渡效果,和 animation 有类似的效果,但存在使用场合有着较大差别
transform(变形) 用于设置元素的旋转、位移、缩放。和设置元素的动画并没直接关系,就跟写 css 属性一样
translate(移动) 用于设置元素的位置,就是 transform 的一个属性
animation(动画) 用于设置怨毒的动画属性,它是一个简写,有6个属性值

transition 字面意思,过渡是指元素从属性a的某个值过渡到属性a的另一个值,这就是一个状态的改变,但是需要一个条件来触发从而发生这种转变,比如 &:hover,&:checked,&:focus、媒体查询或者 JS

#box {
    height: 100px;
    width: 100px;
    background: green;
    transition: transform 1s ease-in 1s;
}
#box:hover{
    transform: rotate(180deg) scale(0.5,0.5);
    transform: translateX(100px);
    transform: translateY(100px) translateX(100px) scale(0.5, 0.5);
}
<div id="box"></div>

分析:给 div 添加了一个过渡动画,动画指定了 transform 动画,触发时机为当鼠标移上去的时候。因此当鼠标移入的时候元素的 transform 属性发生变化,那么这个时候触发了 transition 动画,当鼠标移除的时候也产生了 transform 的变化,因此还是会触发 transition,产生动画。 上面设置了3个 transform 只有最后一个生效 因此 transition 产生动画的条件是设置的 property 发生变化,这种动画的特点是需要一个驱动力去触发。因此就存在一些缺点:

  1. 需要事件触发,没法在网页加载时自动发生
  2. 是一次性的,不能重复发生,除非再次触发
  3. 只可以定义开始状态和结束状态,不能定义中间状态,因此没有丰富的动画空间
  4. 一条 transition 规则,只能定义一个属性的变化,不能涉及多个属性

语法:transition:property duration timing-function delay

属性 含义
transition-property 规定设置过渡效果的 css 属性名称
transition-duration 规定完成过渡效果需要时间
transition-timing-function 规定速度效果的速度曲线
transition-delay 规定动画效果何时开始

animation

animation 总体来说是对 transition 的增强,不再受限于触发时机和动画的属性值。

.box {
    height: 100px;
    width: 100px;
    border: 15px solid black;
    animation: changebox 4s ease-in-out 1s 1 alternate running forwards;
}
.box:hover {
    animation-play-state: paused;
}
@keyframes changebox {
    10% {
        background: red;
    }
    50% {
        width: 80px;
    }
    70% {
        border: 15px solid yellow;
    }
    100% {
        width: 180px;
        height: 180px;
    }
}
<div class="box"></div>

animation: animation-name, animation-duration, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, animation-fill-mode

属性 含义
animation-name 用来调用@keyframes定义好的动画,与@keyframes定义的动画名称一致
animation-duration 指定元素播放动画所持续的时间
animation-timing-function 指定速度效果的速度曲线,是针对每一个小动画所在时间范围内的变化频率
animation-delay 定义在执行动画之前的等待时间
animation-iteration-count 定义动画的播放次数,可选具体次数或者无限次(infinite)
animation-direction 设置动画播放方向:normal(按时间轴顺序)、alternate(轮流,即来回往复进行)
animation-play-state 控制元素的播放状态:running(继续)、paused(暂停)
animation-fill-mode 控制动画结束后元素的样式,有4个值: none(回到动画之前的状态)、forwards(元素停留在动画结束后的状态)、backwords(动画回到第一帧的状态)、both(根据 animation-direction 轮流应用 forwards 和 backwords 规则)。注意与 iteration-count 不要冲突

总结:单个动画效果、简单的由 transtion 实现,复杂的用 animation 实现。animation 出现后市面上出现了很多这种 css 动画库,其中我在使用 animate.css 推荐小伙伴们使用下

JS 实现动画

大家用 JS 写动画立马想到的是 setTimeout 和 setInterval,但是较好的动画体验是保持在 60fps 最好,上面的2个 api 由于会受到 runloop 的影响,并不会特别准时,JS 有个 requestAnimationFrame api 可以保持动画在 60fps, JS 实现动画的本质就是控制元素在时间和空间上的变化的研究。

假如要实现一个匀速直线动画,让一个 div 在 3秒内在水平方向上从向由右移动500px。那么如何实现,先从物理问题上解决吧。

总时间: 3s 总位移: 500px 那么每秒移动多少 500px/3s 我们设计一个 JS 函数。4个参数, 属性开始值,属性结束值,动画执行时间,回调函数

大体思路是:外界传入上面4个参数,我们可以记录函数调用刚开始的时刻也就是开始时间(start),然后通过 performance.now() 拿到当前时间(now),然后 period = (now-start) 就是经过的时间。然后通过 period/time 就是时间的进度百分比,拿这个百分比再去乘以总的属性值差就是当前的属性值,然后将计算结果实时调用回调函数(这个回调函数就是指定这个属性值如何应用到动画元素上)

/**
* 执行补间动画方法
*
* @param {Number} start 开始数值
* @param {Number} end 结束数值
* @param {Number} time 补间时间
* @param {Function} callback 每帧回调
* @param {Function} timing 速度曲线,默认匀速
*/
function animate(start, end, time, callback, timing = t => t) {
  let startTime = performance.now() // 设置开始的时间戳
  let period = end - start // 拿到数值差值
  // 创建每帧之前要执行的函数
  function loop() {
    liveAnimationFunction = requestAnimationFrame(loop) // 下一调用每帧之前要执行的函数
    const passTime = performance.now() - startTime // 获取当前时间和开始时间差
    let per = passTime / time // 计算当前已过百分比
    if (per >= 1) { // 判读如果已经执行
      per = 1 // 设置为最后的状态
      cancelAnimationFrame(raf) // 停掉动画
    }
    const pass = period * timing(per) // 通过已过时间百分比*开始结束数值差得出当前的数值
    callback(pass)
  }
  let liveAnimationFunction = requestAnimationFrame(loop) // 下一阵调用每帧之前要执行的函数
}

function doMove(easing) {
  animate(0, 100, 1000, move.bind(null, document.querySelector(`#${easing}Box`)), EasingFunctions[easing])
}

function move(box, value) {
  box.style.transform = `translateX(${value}px)`
}

Native 端动画(iOS为例)

其实动画的本质就是元素时间和空间上发生变化的研究。在 web 前端如此,在 native 端也是如此,不过就是换了一些 api 如此。

举个例子,就拿上面所说的水平位移为例,下面给 iOS 的原生代码

//CALayer 层动画
CABasicAnimation *positionAnimation = [CABasicAnimation animation];
//指定动画路径是水平方向x轴
positionAnimation.keyPath = @"position.x";
//指定位移距离
positionAnimation.toValue = @1000;
//下面2行代码让动画停留在动画结束的位置
positionAnimation.fillMode = kCAFillModeForwards;//(效果完全等同于 css 中的 animation-fill-mode属性)
positionAnimation.removedOnCompletion = NO;
[self.animationView.layer addAnimation:positionAnimation forKey:nil];

css 中的 animation-fill-mode(控制动画结束后元素的样式,有4个值: none(回到动画之前的状态)、forwards(元素停留在动画结束后的状态)、backwords(动画回到第一帧的状态)、both(根据 animation-direction 轮流应用 forwards 和 backwords 规则))和 native(iOS)端的 fillMode 属性一致。

下面提出 iOS 端的 fillMode 取值选项

/* `fillMode' options. */
CA_EXTERN CAMediaTimingFillMode const kCAFillModeForwards
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CAMediaTimingFillMode const kCAFillModeBackwards
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CAMediaTimingFillMode const kCAFillModeBoth
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
CA_EXTERN CAMediaTimingFillMode const kCAFillModeRemoved
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

看得出来 fillMode web 端和 native 端是一模一样的。

再举个例子,平时我们有可能画一根横线,在 web 端 和 native 端都存在 view 这样的概念,在 web 端可能会是一个 div(高度设置为1)或者是 canvas 实现(canvas 拿到当前上下文、绘制路径、关闭路径、填充颜色)。在 native 端也一样,开启绘图上下文、拿到上下对象、绘制路径、上色、关闭上下文。

所以其他具体的例子也就不举了,本质上大前端的所有动画干的事情都一样,所以我们需要处理好时间和位置的关系。