前言
大家好,我是Fly哥感谢你抽空阅读这篇文章, 本篇文章大概花费10分钟阅读, 你可以学到下面几方面的内容
- 第一个是谈谈前端动画的一些技术思考和选型, 比如 到底是用 js 还是使用css 还是使用canvas 或者是使用一些其他的渲染引擎
- 第二个是手写一个简单的小型动画sdk, 十分的轻量, 无扩展。
关于前端动画的技术选型
CSS
实现动画的方式其实对于前端开发者来说,其实无非很简单, 第一种就是 CSS, 使用css 的 animate 和 transiton, 比如实现一个很简单的呼吸动画。 我们如何用css 只需要这么写 就可以实现
.div {
animation: breathe 0.8s infinite;
}
@keyframes breathe {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
我们定义 动画的 3个关键点 , 0 , 50% , 100% 不断地去改变元素的scale 属性, 然后定义动画的时间 animate-duration 在定义动画的重复次数 无限。 我们就可以很轻松的实现, 一个很快的呼吸动画。第二种就是 用js 其实也就是操作dom 在 单位时间内 ,不断地去改变 div 的样式, 也能实现css 的效果。 从性能上看,肯定是css 的性能更好, js 还需要操作dom, 在短时间内 不断的进行 重排重绘本身这就是一种开销了。所以这种的第一个优势就实现了
能用css3 实现的动画, 坚决不用js 去操作dom
但是使用 css3 实现的动画, 尽可能用 transform opactity 不会导致浏览器的重排重绘, 因为浏览器渲染 页面 是由两个线程 去进行操作的 , 一个 是主线程 一个是合成线程 , 而合成线程 主要用的 gpu 去进行渲染的 但是 有一些属性会进行 强制提升 合成层 比如 will-change 我觉得还是看情况使用, 如果动画不是很复杂的话,完全不需要用。 因为一个渲染层 有可能些会占据 大量 的 内存
一个渲染层,需要多少内存占用?为了便于理解,举一个简单的例子;一个宽、高都是300px的纯色图像需要多少内存?
300 300 4 = 360000字节,即360kb。这里乘以4是因为,每个像素需要四个字节计算机内存来描述, 也可以用rgba 去思考 4个通道。
假设我们做一个轮播图组件,轮播图有10张图片;为了实现图片间平滑过渡的交互;为每个图像添加了will-change:transform。这将提升图像为复合层,它将多需要19mb的空间。800 600 4 * 10 = 1920000。
仅仅是一个轮播图组件就需要19m的额外空间!这对于移动端的机型 可以说是灾难级别的 , 会带来手机发烫, 页面卡段一系列问题。 所以对于要使用硬件加速的元素, 可以使用 transform 的 scale 属性 ,这在 css 3 实现序列帧动画 将十分有用。
如图:
对于图片,你要怎么做呢?你可以将图片的尺寸减少5%——10%,然后使用scale将它们放大;用户不会看到什么区别,但是你可以减少大量的存储空间。 这就是减少 复合层的内存大小。
JS
难道css 动画 就无敌了吗, 但凡遇到一些需要一些 关键帧做的操作, 其实css 就很难 做的很好了
所以我对于css 动画的 理解 只适合做展示型动画, 什么意思了, 就是动画的中间状态我们无法控制 ,假设动画一共有100 帧 ,我想在 第 50 帧 , 做一些操作, 比如我做一下接口请求, 弹一个框啥的。 这边涉及到逻辑控制了。
谈到js 控制动画 就一定离不开requestAnimationFrame
requestAnimationFrame()最大的优势是由系统来决定回调函数的执行时机。 具体一点讲,如果屏幕刷新率是 60 帧,那么回调函数就每 16.7ms 被执行一次,如果刷新率是 75Hz,那么这个时间间隔就变成了 1000/75=13.3ms,换句话说就是,requestAnimationFrame()的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
除此之外,requestAnimationFrame()还有以下两个优势:
- CPU 节能:使用 setInterval()实现的动画,当页面被隐藏或最小化时,setInterval()仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费 CPU 资源。而 requestAnimationFrame()则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的 requestAnimationFrame()也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。
- 函数节流:在高频率事件(resize,scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,使用 requestAnimationFrame()可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来。
requestAnimationFrame()的工作原理:
先来看看 Chrome 源码:
int Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback> callback) {
if (!m_scriptedAnimationController) {
m_scriptedAnimationController=ScriptedAnimationController::create(this); // We need to make sure that we don't start up the animation controller on a background tab, for example.
if (!page())
m_scriptedAnimationController->suspend();
}
return m_scriptedAnimationController->registerCallback(callback);
}
rquestAnimationFrame 的实现原理就很明显了:
- 注册回调函数
- 浏览器按一定帧率更新时会触发 触发所有注册过的 callback
好了铺垫的差不多了, 开始如何去设计一款通用的动画sdk
设计篇
设想一个简单的 动画, 他有哪些生命周期。如图所示
我们假设变化一个元素 从 A 位置变换 到 B位置 经历了 duration 然后 在A 和B 之间的位置 应该都是属于中间态, 中间态可以拿到当前动画的进度, 去做一些更新。 设计东西, 一定要考虑实际的业务场景。 比如我们在开始这个动画之前, 会做一些操作, 比如让页面静止, 或者啥的一些操作, 有或者没有,但是设计的一定要暴露好hook 给使用者, 中间的进度callback 自然也不用说了。 最后就是动画结束了, 我们要做些什么。
所以我们就可以得到 3个hook
onStart 开始的之前回调
onProgress 过程中的回调
onFinsh 动画结束后的回调
这个其实最简单的思考, 其实还有一个动画 重复多少次, 以及动画重复每次间隔的时间,还有当前动画延迟的时间,不过你能想到的场景大部分都能想到了, 如果没想到, 需要加某些功能请联系我。
正如上面图示所表示 时间轴 表示的是动画一共持续的时间, 每一个动画任务 都有自己的时间, 也有自己所处的塞道, 如果你有5个动画 需要串行, 其实你只要依次按顺序排好, 然后就可以了。 如果需要并行, 其实就是需要2条以上赛道同时开始 就可以了, 确定好每条赛道的动画 在什么时候播放就可以了。
DEMO1
下面我以demo 的方式去展示下 视频下方动画, div1 动画结束后 div2 才开始
代码如下:
import { timeLine, TimeLineManager } from 'timeline'
import { useLayoutEffect, useRef, useState } from 'react'
export const Time = () => {
const [x1, setX1] = useState(0)
const [x2, setX2] = useState(0)
const timeLineRef = useRef()
useLayoutEffect(() => {
timeLineRef.current = timeLine()
.addAnimate({
duration: 1000,
onProgress: (value) => {
setX1(100 * value)
},
})
.addAnimate({
duration: 1000,
onProgress: (value) => {
setX2(100 * value)
},
})
}, [])
return (
<div>
<div
style={{
width: '100px',
height: '100px',
background: 'red',
transform: `translateX(${x1}px)`,
}}
onClick={() => {
timeLineRef.current?.start()
}}
>
第一个
</div>
<div
style={{
width: '100px',
height: '100px',
background: 'green',
transform: `translateX(${x2}px)`,
}}
>
第二个
</div>
</div>
)
}
DEMO2
我们加大些难度 第一个动画 重复几次 次 然后再等 等待几秒中 在进行播放动画2, 动画的缓动函数 内置了一些
如视频所示:
import { timeLine, TimeLine, Easing } from 'timeline'
useLayoutEffect(() => {
timeLineRef.current = timeLine()
.addAnimate({
duration: 1000,
repeat: 2,
easingFunc: Easing.Cubic.In,
onProgress: (value) => {
setX1(100 * value)
},
onFinsh: () => {
console.error('我走结束了')
},
})
.addAnimate({
delay: 1000,
duration: 1000,
onProgress: (value) => {
setX2(100 * value)
},
})
}, [])
给大家看下核心代码, 相对于之前 紧紧是增加了几个参数 一个重复的次数,然后引入了内置的缓动函数, 你也可以自己写, 是一个纯函数, 输入和输出 保持都是number类型就好了。 缓动函数的作用其实就是动画变化的时间, 在 单位时间内, 是匀速, 还是匀加速, 还是先加速后减速。
写到这里我发现了一个可以优化的点, 就是当做钟摆动画的, repat 是 从动画的结束的末尾 然后到 动画结束的开始, 这里应该也要提供一个 onRepeatCallback ,比如 有一个动画场景, 就是 到尽头 在敲一下,做一些其他东西, 这里的话 我们也应该给出hook ,让开发者做自己想要的东西。
DEMO3
比如有动画同时进行的,也就是做并发动画, 我加一个 div3 动画 让他和 12 一起开始, 各自做各自的事, 当用到并行 动画的 其实就是 多条timeLine, 我提供了一个 timeLine Manager 去做管理, 开始 以及整体动画 结束的回调, timeLine Manager都可以做到。
代码如下:
useLayoutEffect(() => {
timeLineManagerRef.current = new TimeLineManager([
timeLine()
.addAnimate({
duration: 1000,
repeat: 2,
easingFunc: Easing.Cubic.In,
onProgress: (value) => {
setX1(100 * value)
},
onFinsh: () => {
console.error('我走结束了')
},
})
.addAnimate({
delay: 1000,
duration: 1000,
onProgress: (value) => {
setX2(100 * value)
},
}),
timeLine().addAnimate({
duration: 500,
repeat: 3,
easingFunc: Easing.Cubic.In,
onProgress: (value) => {
setX3(100 * value)
},
}),
]).start()
}, [])
核心代码也很简单 参数传多个 timeLine 就可以了。 同样也提供开始 和结束的hook , 用起来是十分的方便。 后续还有暂停, 继续这些我就不一一演示了, 周末会将库开源, 在Readme 里 会详细的 api 讲解。
最后
很感谢大家看到最后, 你的点赞是我最大的支持, 开源的git 地址 周末会在朋友圈发布。 如果你有想法和我交流的 欢迎加我微信骚扰我!!!