「疫情」反复发生,我在校园里待了两个月没有走出校园,我觉得很郁闷,那同理来说或许大家都很郁闷,那是否可以用某种方式来疏解一下郁闷?以弹幕形式的「线上许愿墙」便应运而生,或许你可以在这里查看线上的效果 线上 DEMO (内含音频⚠️,推荐使用微信打开)。
动画/动效
这个项目在动画和动效上画的时间会稍微多一点,同时很早之前就想尝试 Lottie (动画跨平台的简易方案) 于 Anyway.FM 播客的一期「欸!UI 设计师不可不了解(違う)的文档输出格式 」。
现如今动画方案已经非常多且成熟,包括但不限于 GSAP (我首页的动画便是基于他),Framer Motion ,React Spring ;同时一些框架如 Vue.js 和 Svelte 都有对动画的原生支持。
而这其中最核心的便是 CSS 动画,无论是你是刚入门或者是饱经风霜的前端, Transition /Animation 都是绕不开的,我很早之前还追寻过各种 CSS 动画的优化方法并翻译过一篇「如何使阴影的动画表现得流畅光滑? 」
CSS 动画
首先,我们来简单介绍一下比较两个依托 CSS 完成动画的方案。
CSS Transition
(过渡)
最早的 CSS 是没有任何时间概念的,你给予的变化都是立即响应的。但是现实中的物体运动都是变化的,状态是连续的,于是 Transition
带来的「时间」的概念。按照时间关系在两个状态中「过渡」,这便是 CSS Transition
的作用,他的好处想必不用说,让动画更自然、生动、更富有交互感。
我们用一个最简单的例子来阐述他的作用:
<div class="demo-block">
<div id="transition-demo">
Hover me(w/o transition).
</div>
<div id="transition-demo" class="with-transition">
Hover me(w/ transition).
</div>
</div>
<style>
#transition-demo {
width: 150px;
height: 150px;
background: pink;
display: flex;
align-items: center;
text-align: center;
}
#transition-demo:hover {
background: red;
}
#transition-demo.with-transition {
transition: .3s ease;
}
</style>
事实上,transition
是几个属性的联合简写,在使用 transition
时你至少需要保证 duration
即「过渡」动画的时长的定义:
{
transition: property | duration | easing function | delay
}
这里将用表格的形式来简要阐述 transition
及其属性:
属性(Property) | 描述 | 可行值 | 🌰 |
---|---|---|---|
transition-delay | 过渡延迟 | 1s .6s 600ms |
transition-delay: 600ms |
transition-duration | 过渡时间 | 同上 | transition-duration: 600ms |
transition-property | 需要「过渡」的属性 | all(所有属性), 其他值类的属性 | transition-property: width, height, color |
transition-timing-function | 缓动函数 | linear, ease-in, ease-out, ease-in-out, cubic-bezier |
transition-timing-function: ease |
缓动函数(Timing function)1
当我们告诉浏览器「过渡」的始末后,浏览器需要生成动画的中间状态,而控制中间状态的函数便是「缓动函数」。
像上表总结的,他默认提供了 linear
ease-in
ease-out
ease-in-out
四种默认的缓动函数,让我们来大概看一下,总的来说这是四条各异的「贝塞尔曲线」,如果你对这些曲线有兴趣,可以在 cubic-bezier playground
可视化的调试查看效果。
下面是一个不同缓动函数的例子:
更多关于 Timing Function 的例子或者有趣的应用可以在这里查看:Transition Timing Functions < CSS | The Art of Web , 另外除了我们上述所提到的「连续」变化的动画,其实 CSS 还提供了一种离散的缓动变化,具体介绍可以查看张鑫旭的 CSS3 animation属性中的steps功能符深入介绍 。
Transition
的优劣2
其优点主要在于,「简单」、「易于使用」。 但是,它的缺点也很明显:
- 只能完成一次,不能多次触发
- 无法对动画进行「暂停」、「恢复」、「取消」等操作;
- 无法在网页加载时触发(但是可以通过 JavaScript 控制 class 来进行动画)
- 一条 transition 只能规定一个「相同」的变化,不能同时多个不同的属性变化。
动画性能
动画的性能主要由 「FPS」(Frames Per Second,每秒帧数)来衡量,「帧」大概可以理解为「一个画面」,而动画由一个个静态画面组成,故 FPS 越高则是动画性能越好。
浏览器与渲染有关的两个流程是「重绘」、「重排」,简单来说 「重排」是对文档流进行重新布局,而「重绘」是对文档流中的元素进行重新渲染(不影响文档流),关于这两个概念你可以在 Understanding Reflow and Repaint in the browser 中尝试获取更深刻的理解。文章 如何使阴影的动画表现得流畅光滑? 便是从重排重绘的角度对动画性能进行优化。
其次,大部分时候浏览器使用 CPU 进行渲染,但是为了动画效果,我们可能需要让浏览器启用「GPU 渲染」。
或许你对 3d transform
会启用 GPU 加速有所耳闻(实际在 Rakunr
也用了这种 hack 加速法)。但实际上我们不需要进行 z-axis
变换,于是这个时候 will-change
应运而生,直白点来讲这个属性就是告诉浏览器 “我要变形了!”。
will-change3,是 CSS 3 新增的属性,通过 will-change
告知浏览器元素要对什么属性进行优化。
{
will-change: margin-left;
}
但是请注意,will-change
虽好,但在大量的 will-change
任务排队时,那渲染的性能还是非常不理想的,所以我们仍然需要遵循最小化影响原则。
CSS Animation
(动画)
而 Animation 便是为了解决 Transition
所遇到的困窘,使 Animation
获得 「多次触发」、「管理状态」、「多种变化」 的能力可以归功于 「始终态」 与 「关键帧」自定义功能。
跟 transition
一样,animation
也是多个属性的简写。
<div class="bg-animation"></div>
<style>
.bg-animation {
width: 120px;
height: 120px;
animation: pulse 5s infinite;
}
@keyframes pulse {
0% {
background-color: #001F3F;
}
100% {
background-color: #FF4136;
}
}
</style>
可以从上面的例子看出,animation
相对于 transition
多了一个 @keyframes [[animationName]]
,而这是 Animation
中最为生命力的地方,你可以在任意时刻插入你的「关键帧」,从而更灵活的控制你的动画。
完整定义一个 Animation
如下所示:
.element {
// 动画名称
animation-name: stretch;
// 动画持续时间
animation-duration: 1.5s;
// 缓动函数
animation-timing-function: ease-out;
// 动画播放延迟
animation-delay: 0s;
// 动画播放的方向
animation-direction: alternate;
// 循环播放次数
animation-iteration-count: infinite;
// 动画播放后结束状态
animation-fill-mode: none;
// 动画播放状态
animation-play-state: running;
}
对于前四个属性与 transition
大致相同,而后面四个属性是予我们「控制」动画得能力。
属性(Property) | 描述 | 可行值 |
---|---|---|
animation-direction | 播放方向 | normal alternate reverse alternate-reverse |
animation-play-state | 播放状态 | running paused |
animation-fill-mode | 结束状态 | none :回到动画没开始backwards :回到第一帧 both :根据 animation-direction 轮流应用 forwards 和 backwards规则 |
animation-iteration-count | 播放次数 | 数字, infinite |
继续阅读:
SVG 动画4 与 Canvas 动画
SVG(Scalable Vector Graphics) 是现在 Web 流行的「矢量图像格式」,除了绘图能力,他还提供了一些高级的动画 API,主要通过
<set>
, <animate>
, <animateColor>
, <animateTransform>
, <animateMotion>
几个方法进行动画。在这里不进行过多介绍,详细的可以查看4。
<svg width="500" height="100">
<circle id="orange-circle" r="30" cx="50" cy="50" fill="orange" />
<animate
xlink:href="#orange-circle"
attributeName="cx"
from="50"
to="450"
dur="1s"
begin="click"
fill="freeze" />
</svg>
Canvas5 是浏览器提供的绘图 API,它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。如果通过 Canvas 实现动画,则需要自己管理每一个状态与绘制图形,是最麻烦但是自由度最高的动画方式。
Lottie
Lottie
是由 Airbnb 开发的实时渲染 After Effects 动画的库,他可以工作在 iOS, Android 和 Web 上。
我们使用 Lottie 的时候仅需要,浏览器中的动画后端可以使用 html
, svg
与 canvas
。
Lottie 的动画文件主要使用 JSON 格式,你可以在 Lottie files 寻找和自定义你喜欢的动画,然后导出 JSON 文件。
import lottie from 'lottie-web';
import animationData from './animation.json';
let anim = lottie.loadAnimation({
container: document.queryElementById('lottie'),
renderer: 'canvas',
loop: true,
autoplay: true,
animationData: animationData,
})
这样,我们就完成了 Lottie 的动画的创建,并且可以使用 anim
对象对动画进行操作。
这个项目在最开始使用 Lottie 的时候,出现了非常严重的 内存泄漏 的情况,于是我收到了求助,并在查阅 issues 时 Memory leak using goToAndStop and destroy
并得知,Lottie 的动画是直接对 animationData
对象进行操作,所以如果使用上述方法,则会导致内存泄漏。
为了解决这个问题,对 animationData
进行一个 深拷贝 就能解决,即 _.cloneDeep(animationData)
。
除此之外,Vue.js 特别在文档中提示 Avoid Memory Leaks ,所以你还需要在组建卸载的时候对 lottie 实例进行「销毁」,最终的代码大致长这样:
mounted: {
this.animation = lottie.loadAnimation({
container: document.queryElementById('lottie'),
renderer: 'canvas',
loop: true,
autoplay: true,
animationData: _.cloneDeep(animationData),
})
},
destroyed: {
this.animation.destory()
this.animation = null
}
弹幕的实现6
弹幕的滚动
在实现现在这个版本的弹幕之前,学弟做了两个小 DEMO,都是基于两个 CSS 方案的,解决起来也比较粗糙,但是很适合教学。
transition
<div class="danmaku-block">
<div class="danmaku">12312321</div>
<div class="danmaku">12312321</div>
<div class="danmaku">12312321</div>
</div>
<style>
.danmaku {
transform: translateX(100%);
transition: 0s;
}
.demo-block:hover .danmaku {
transform: translateX(-100%);
transition: 3s linear;
}
</style>
<script>
document.querySelectorAll('.danmaku').forEach(el => {
el.addEventListener('transitionend', () => {
console.log('end!')
})
})
</script>
animation
<div class="demo-block">
<div class="animation-danmaku is-paused">12312321</div>
<div class="animation-danmaku is-paused">12312321</div>
<div class="animation-danmaku is-paused">12312321</div>
<button id="danmaku-control">播放</button>
</div>
<script>
const danmakuCtrl = document.querySelector('#danmaku-control')
const danmakuLists = document.querySelectorAll('.animation-danmaku')
danmakuLists.forEach((el) => {
el.addEventListener('mouseenter', (e) => {
e.target.classList.add('is-paused')
})
el.addEventListener('mouseleave', (e) => {
e.target.classList.remove('is-paused')
})
let iterCnt = 0
el.addEventListener('animationiteration', (e) => {
e.target.innerHTML = `12312321 ${++iterCnt}`
})
})
danmakuCtrl.addEventListener('click', () => {
danmakuLists.forEach((el) => el.classList.remove('is-paused'))
})
</script>
<style>
.animation-danmaku {
animation: 6s danmaku-move infinite linear;
transform: translateX(-100%);
}
.animation-danmaku.is-paused {
animation-play-state: paused;
}
@keyframes danmaku-move {
0% {
transform: translateX(150%);
}
100% {
transform: translateX(-50%);
}
}
</style>
弹道+完整实现
由于本人特别懒,所以直接参考了弹幕实现原理 ,这篇文章考虑的细节还是比较好的,代码逻辑也清晰,所以大家可以直接上。
不过他使用的是 transition
的方案,我这边由于需要实现 悬浮暂停 则切换到了 animation
能够帮助我控制弹幕的进度。
端表现不一致性
万恶的 Safari
在 Safari 下我遇到了两个非常头疼的问题,
- var() 失效
- flexbox 的 transform 失效
.item {
--height: 127px;
display: flex;
}
.item.next-state {
transform: translateX(-var(--height));
}
上述代码可是把这两个坑踩了个遍,当时在 iOS 设备上,弹幕不正常工作,我还以为是「高度太小」导致程序判断不够塞弹幕,结果实际 debug 的时候发现“弹幕都插入了”,但是没有他的影子。
如果想要在 Safari 中正常使用 var
,那么你需要这么做:
:root {
--height: 127px;
}
.item {
height: var(--height);
}
万恶的移动端
Android 软键盘改变布局问题
在 iOS 与 Android 下软键盘的表现是不一致的,iOS 的软件是单独的一层,而 Android 的软键盘是会占用底部的,故在两者的软键盘弹出时,Android 的 Webview 的布局高度会变坍塌,而 iOS 则不会。
为了解决问题我们有两种解决思路,核心都是「固定高度」:
- 固定高度
<div :style='{{ height: document.body.clientHeight + 'px' }}' />
- 监听变换
let height = document.body.clientHeight
window.onresize = () => {
if (height > document.body.clientHeight) {
document.body.style.height = height + "px"
}
}
onClick
与 onMouse*
事件
移动端的「点击」事件与 PC 端表现的不太一致,其主要是为了判断「是否为长按」而对所有的 onClick
进行了 200ms 的延迟;所以你想即想「快速反应」又不想重复触发事件,则需要一点 trick,主要是以下两个方法:
- 使用冒泡阻止
node.addEventListener('click', () => {
console.log('handle click')
})
node.addEventListener('ontouchstart', (e) => {
e.preventDefault()
console.log('handle touch')
})
- 判断是否支持 「touch」
const clickEvent = (function () {
return 'ontouchstart' in document.documentElement === true ? 'touchstart' : 'click'
})();
node.addEventListener(clickEvent, (e) => {})
感谢看到这里,实际上这个项目已经以 Apache License 的形式开源在了 Wish-Danmaku-Wall ,如果有什么好的建议或者想法可以随时在 Issues 或者评论区留言。