Skip to content

Latest commit

 

History

History
307 lines (247 loc) · 10.1 KB

简单实践,让动画信手拈来.md

File metadata and controls

307 lines (247 loc) · 10.1 KB

transition 与 animation 的选用

元素一旦绑定 transition 之后,会对下个 Event Loop 中变更的样式(如异步 hover 或通过 js 变更 style/class),给予过度效果。要想更细颗粒度定制动画,可为元素添加诸如 animation: keyframeName 1s 的样式,animation 在绑定时立即执行动画。

直接在 style 里修改动画不如绑定/解绑 class 方便

<button class='btn'> 按钮1 </button>
<div class='car'></div>
<style>
/* transition动画 */
.btn {
    transition: all .5s; background: yellow
}
.btn:hover {
    background: red
}
/* animation动画 */ 
.car {
    height: 100px;
    width: 100px;
    background: green;
    position: relative;
    animation: carMove 1s;
    animation-fill-mode: forwards;
}
@keyframes carMove {
    0%  { top: 10px }
    20% { top: 200px }
    100% { top: 250px }
}
</style>

两者有一些异同之处:

  • transition 需在绑定后的下一个 Event Loop 中实现动画,而 animation 在当时立即执行,因此,transition 常用于反复执行的场合,如 hover、toggle 等
  • animation 在绑定时候立即执行,执行完会默认恢复到原态,所以在 0% 帧和 100% 帧处可能有跳变,如果加上 animation-fill-mode: forwards 属性,会保持 100% 帧的,像 transition 动画那样。因此在串联执行多个 animation 动画时,常用 forwards 来保持连贯,避免每次都要“重头再来”。
  • 两者都可在后续的 Event Loop 中修改样式,触发动画

动画在执行的时候,会真实的改动到 DOM 中的元素上,比如我们可以通过以下案例监测到 clientHeight 的变化

animation 中省略样式的帧

animation 在时间粒度上的操控更灵活,可以通过 % 来定义特定时间帧。我们先来看一个案例,其中 color 样式并未在 0% 帧定义,看看是如何过渡的

@keyframes test {
    0% {
        width: 50%
    }
    50% {
        color: red
        <!--从color:black(继承自元素样式) 开始过渡,过渡时间0-50%-->
    }
    100% {
        width: 80%
        <!--从width:50%开始过渡,过渡时间0%-100% -->
        color: yellow
        <!--从color:black->red->yellow-->
    }
}
<!--综上:-->
<!--对于 width 属性,只有两帧过渡效果-->
<!--对于 color 属性,有三帧过渡效果(初始帧继承自元素样式)-->

所以,对于帧中样式的省略,满足下面的规则:

  • 0% 帧会继承元素本身属性,这些继承的属性优先级低,可能被新定义的覆盖
  • 如果用户未定义 0% 帧,则默认创建,其属性完全继承自元素
  • 0% 帧以后的所有帧,如果没有某个特定样式,则跳过该帧计算过渡曲线。例如上述案例中,50% 的帧中没有 width 样式,则 width 的过渡曲线从 0% 到 100% 按照 ease 曲线过渡

animation-fill-mode 属性要定义在 animation 之后才有效。(或具有更高样式优先级)

动画的重播

我们在执行了一次 animation 动画之后,有时候想再重复执行一次,直接想到的做法是:

  1. 移除已执行的 animation
  2. 再插入该 animation

但是需要注意的是,以上 1、2 两个步骤不能在同一个 Event Loop 中完成,通常两步之间需要一小段间隔(如 setTimeout 或在 Vue 中的 nextTick 里执行)

<div class='container'>
    <div class='car car-move'></div>
</div>
<style>
.car-move {
	animation: moves 2s;
}
.car {
	background:green;
	height:100px;
	width: 100px;
	animation-fill-mode: forwards; /* 需在 animation 后 */
}
@keyframes moves {
    /* 略 */
}
</style>
<script>
// DOMContentLoad 回调中执行
var car = document.querySelector('.car')
// 移除动画
setTimeout(()=>{
	car.className = 'car'
}, 1)
// 1毫秒之后再添加
setTimeout(()=>{
	car.className = 'car car-move'
}, 2)
</script>

这种方式可以实现,但更好的实践是操作DOM:

  1. 移除元素
  2. 重新插入

通过不超过两行代码即可完成,实现动画的重播:

var container = document.querySelector('.container')
// appendChild 操作会隐含做“摘除”的操作,相当于一行语句完成移除+插入
container.appendChild(container.children[0]) 

Vue 中,如果要执行DOM中元素的移除和插入,更方便快捷,直接用 v-if 控制即可。需要注意的是,无法在一个 Event Loop 中完成,可利用 nextTick 函数辅助完成

<div class='car car-move' v-if='switch'></div>
<script>
// ...
this.switch = false // 移除
this.$nextTick(() => {this.switch=true}) // 再插入
// ...
</script>

老虎机式数字动画

先看看效果:

这种动画效果应用场景广泛:

  • 数据展示大屏
  • 网页抽奖游戏
  • 提升用户行为的反馈性

全部代码如下:

<div class='tiger'>
    <div> 1 </div>
    <div> 2 </div>
</div>
<style>
.tiger {
    height: 3rem;
    line-height: 3rem;
    overflow: hidden;
}
.tiger div {
    font-size: 2rem; font-weight: bold;
    animation: scroll 1s;
    animation-fill-mode: forwards
}
@keyframes scroll {
    100% { transform: translateY(-100%) }
}
</style>
<script>
let tiger = document.querySelector('.tiger')
let numbers = tiger.children
tiger.onclick = function(){
	let lastNumber = numbers[1].innerHTML;
	numbers[0].innerHTML = lastNumber;
	numbers[1].innerHTML = parseInt(lastNumber) + 1;
	tiger.appendChild(numbers[0]) // 两个数字,所以要插入两次(隐式移除)
	tiger.appendChild(numbers[0])
}
</script>

通过代码可以发现,数字“更新”的本质是:

  • 两个卡片同时向上位移,溢出 hidden
  • 数字有更新时候,需重复动画,重复的方式是从DOM中移除,再添加
  • 再添加的DOM中,两个卡片内的数字已经更新过了,添加的瞬间触发新动画

手动挡的动画

requestAnimationFrame 可补充 CSS 样式无法触及的渐变,如渐变 scrollTop 实现页面的平滑滚动。本质上讲:

  • 实现 fps:60(帧时隙1000ms/60 = 16.7ms)的动画,以保证重绘时间和浏览器刷新频率一致(过频繁浪费算力,过缓慢则不流畅)。所以从它的兼容性定义看,很像是一个 16.7ms 的 setTimeout 函数,需要递归的执行下去。
  • 通俗来讲,每 16.7ms,我们修改一次数值,如果把数值的修改反应到DOM中去,就能像幻灯片一样播放起来
// 兼容性的函数定义
function requestAnimationFrame(callback) {
    setTimeout(callback, 16.7)
}

动画的实现是通过递归调用,串联起来,形成连续帧的效果

function animate(){
    // ... 跳出条件 + 每一帧的动作
    requestAnimationFrame(animate) // 递归
}
animate() // 执行动画

案例:数字递增动效

这个效果在钱包类 APP 中常见

非常简洁的实现:

// DOM中获取元素
let account = document.querySelector('.account')
let number = parseInt(account.innerHTML)
// 在动画中操作元素内容
animate();
function animate() {
    // 递归跳出条件
	if(that.num > 300) return
	// 数字递增显示
	number += 1 
	account.innerHTML = number
	// 递归
	requestAnimationFrame(animate);
}

案例:平滑返回顶端

scrollTop 属性无法用 CSS3 动画来操控,我们用 requestAnimationFrame 来一帧一帧的实现:

document.querySelector('.getBack').onclick = function(){
	if (document.documentElement.scrollTop > 0) {
		document.documentElement.scrollTop -= 20 
		requestAnimationFrame (scrollBack)
	}
}

数字的过渡的插件-Tween.js

在平滑滚回顶端的案例中,每帧的步长是20px,是一个一次线性函数。而 css3 动画默认的 ease 曲线效果不错。Tween.js 可以辅助我们计算过渡曲线在每一帧的采样值,让 requestAnimationFrame 也可以用到丰富的过渡时间曲线(不限于简单粗暴的线性递增)

我们看看用纯 JS 实现动画的案例,了解一下其使用:

<!--<script src="//cdn.bootcss.com/tween.js/r14/Tween.min.js"></script>-->
<div class='box' style='height:50px;width:50px;background:red'></div>
<script>
let box = document.querySelector('.box')
let coord = { x: 50, y: 50 }
// 创建 tween 对象来操控 coord 对象,coord 将在一段时间内被高频更新
let tween = new TWEEN.Tween(coord)
tween.to({ x: 150, y:150 }, 2000)
tween.onUpdate(function() {
	box.style.height = this.x + 'px'
	box.style.width = this.y + 'px'
})
tween.easing(TWEEN.Easing.Quadratic.Out) // 采用 ease 渐变,可选
tween.start()
// 周期性触发 onUpdate 中的回调(修改元素长宽)
function animate() {
    if(TWEEN.update()) // 触发更新 + 返回值判断跳出递归
	requestAnimationFrame(animate);
}
animate();
</script>

Tween 有以下的特点(以上面案例讲解):

  • 在渐变过程中,直接操作到 coord 的地址,其中的数据会被高频更新
  • onUpdate 传入的回调函数,会被强制绑定执行者为 coord,执行时 this === coord,通过 this.x 和 coord.x 访问的是同一地址
  • tween 成员函数 return this,因此可以串联,如 tween.to().onUpdate().start()
  • 执行 start() 之后,coord 对象的数据就开始持续更新了,在对应的时刻调用 TWEEN.update() 才能触发 onUpdate 回调中的动作,比如去调整 DOM

可能有这样的疑问:案例中的效果可以用 transition 很方便实现,何苦写这么多代码? 没错,尽量用 css 动画,在很多 css 动画无法涉足的领域中可以灵活的用 js 操作数据做补充

在 vue 中更方便的使用

Vue 的官方教程中也有 Tween.js 过渡的 案例。其根本原理在是,在 onUpdate 的回调中,把渐变的数据赋值给响应式变量,以触发 DOM 更新。

不要试图直接将 Vue 的响应式的对象作为 TWEEN 的参数