写在前面
该篇文章的例子同时也算是我CSS动画的启蒙之作,思路来源于掘友的一篇文章,文末有链接,但只提供思路,于是我打算实现一波,顺便分享一下动画制作过程中的心得体会。也算是应证了,当你打开思路以后,动画,真的很简单。
接上篇文章JS动画?其实没你想的那么难。150行代码,带你走进新世界,本文继续带大家领略动画之美。
实例基于CSS3,暂未考虑浏览器兼容问题,更多是帮助大家打开思路,实际做得不好的地方大家可以自己修正,自己DIY。项目中的CSS是通过LESS
编译的。如何在普通项目中使用LESS?,答案在LESS中文官网。只需要引入一个less.js
的CDN即可,是不是很方便?它会让你爱上CSS。
这是效果图:
下面我们进入正题,教大家如何画这两种电池。
波浪电池
想要玩好CSS动画,障眼法是少不了的。何为障眼法?
这就是障眼法,这里取消overflow:hidden;
即可看到原理,到这份上,也不用我多说了吧?下面讲解关键代码:
// 先画个电池
<div id="battery1">
<div class="battery-anode"></div>
<div class="battery-body">
<div class="wave"></div>
<div class="wave" style="transform: rotate(90deg); opacity: 0.7;"></div>
<div class="wave" style="transform: rotate(45deg); opacity: 0.9;"></div>
<div class="charge"></div>
</div>
</div>
这里,battery-anode
是正极,battery-body
是电池体,wave
是波浪,charge
是充电用的方形色块。从图中也能看出来,波浪是通过好几块方形旋转制造出来的障眼法。这里需要注意的是这些div的层叠关系,只有恰当的层叠才能显现出合适的效果。
下面是动画:
// 波浪的效果是主要是上下移动的过程中一边旋转
@keyframes rotateAndUp {
0% {
margin-top: -50px;
}
100% {
margin-top: -215px;
transform: rotate(360deg);
}
}
// 充电动画,设置颜色渐变,红色到橙色到绿色,模拟充电状态
@keyframes charge {
0% {
margin-top: 100px;
background-color: red;
}
50% {
margin-top: 25px;
background-color: orange;
}
100% {
margin-top: -50px;
background-color: limegreen;
}
}
相信大家在写CSS的时候,看到@include,@keyframes,@media
等字眼时,都会本能的放弃思考,没关系,我也是。总是觉得这些东西极其复杂。但实际上,还是那句话,万事开头难,当你真的了解到它的强大之后,定会爱不释手。
动画做完后需要添加到需要应用的dom元素上,使用css的animation
属性即可。
animation: rotateAndUp 8s linear infinite;
设置infinite
动画才能一直运动下去哦。
注意
这里有一点需要注意,@keyframes中,0%是动画的起始状态,其实就是动画开始时元素将会立即转变为的状态,如果帧动画外定义了相同属性则会被覆盖。在上面的rotateAndUp
动画中,大家可能想到:
为什么我要在DOM结构里面单独定义CSS?为什么帧动画的0%里面没有加入transfrom: rotate()
?
首先,加入transform:rotate()
定义不同的初始状态是为了波浪错位,这样才错落有致;其次,如果写在0%里面,那么动画只要一开始,相应的元素立即变为0%中定义的状态,相同属性被覆盖,同样达不到效果,所有波浪将会保持一致。所以如果某些属性是需要变换的,那么直接写在0%中即可,最常见的就是transform属性了。另外这里提到的是复杂动画,常规动画例如hover
效果,直接transform + transition
即可完美演绎。
细节
细心的小伙伴可以发现,CSS动画虽然是循环执行的,但是每次结束时都是跳回原位重头播放。有的时候我们可能需要的效果并不是这样,也不是简单的倒回去执行(reverse),而是平滑的过渡。如果你写过@keyframes,不妨看看我说的对不对?
举个例子,一个圆形360°旋转并上下移动的动画,一般来说很容易,但自己在控制帧动画@keyframes的时候,总是达不到预期的效果,动画末尾总是会跳帧,或者抖动,可想你的无奈。如何防止跳帧?这其实是有技巧的,笔者多次试验总结发现并不难,很简单,想知道不?继续往下看吧,就是接下来的,安卓充电动画里的上方大圆球。
安卓充电动画
这个动画,第一眼看上去,大家最感兴趣的应该就是这些小圆点如何移动,以及为什么他们可以产生这样粘稠的效果
思路
首先,小圆球之间的粘稠效果是通过强大的CSS滤镜:filter
,利用filter的contrast + blur
实现。使用blur模糊元素边缘,再使用contrast加强对比度,即可实现。其中blur加在元素上,contrast加在背景上,背景必须有颜色,最好是能够和元素的颜色区分开,这样才能更好的对比,实例中背景色为白色。这里举个例子:如何看清一根头发?放到白纸上。
效果如下:
小球如何运动?
CSS实现
采用CSS,不难想到,我们需要提前绘制好若干大小不一的圆点,在通过设置不同的时间,循环执行即可。此种方法需要绘制较多的圆形,需要许多DIV容器,DOM结构可能比较庞大,而且细心观察是可以看出动画的规律的。为了呼应上篇JS动画的文章中提到的面向对象思想分解动画,我决定采用JS实现。无规律的动画才有成就感,你说呢?
纯CSS也可以实现,有兴趣的掘友可以尝试呀,两种方法各有优劣。如果有结果的话,欢迎评论区留言,让我看看嘛(手动卖萌)。
CSS + JS实现
在上篇文章中提到,如何分解一个动画?面向对象的思想分析可得,小球就是一个对象,我们可以抽象出一个类,用来绘制小球并动态插入。使用Math.random()计算随机位置和半径大小。通过Circle类描述圆点的属性,通过Run类控制圆点的创建,插入,删除,运动等。
这里,顺便推荐一个好用的,不依赖jQuery的动画库Velocity.js, 我比较喜欢他的一个特点,每一个动画都可以返回一个Promise对象,在某些情况下涉及异步流程控制的时候,确实很好用,而且很轻量。本文仍旧借用jQuery.animate(),感兴趣的小伙伴可以去了解下哦
下面看DOM结构:
<!--电池容器-->
<div class="battery-container">
<!--上方旋转的大圆-->
<div class="big-circle">98.7%</div>
<!--盛放小圆点的容器-->
<div class="circle-container">
// 隐藏层,主要帮助大圆实现融合滤镜效果
<div class="decoration"></div>
<!--电池底座-->
<div class="bottom"></div>
</div>
</div>
为了防止大圆的显示受到对比度滤镜的影响,单独提出来写,再定位到圆点容器上即可。由于单独提出来写,导致大圆没有融合效果,所以在圆点容器中增加一个隐藏层,定位到大圆下面,帮助它实现融合效果。这叫啥?没错,障眼法。
LESS代码
@radius等等是预先定义的变量,这就是LESS的方便之处,使用变量定义,修改时只需修改变量就可以全局修改到所有引用了该变量的地方,拓展性非常好。
.big-circle {
width: @radius;
height: @radius;
line-height: @radius;
border-radius: @radius;
top: -@radius / 2;
font-size: 30px;
position: absolute;
z-index: 10; // 关键代码,处于层叠上方
text-align: center;
background-color: white;
animation: breathe 3s linear infinite; // 关键代码,呼吸动画就是赋予圆球活力的感觉
}
.circle-container {
width: 100%;
height: 100%;
position: relative;
background-color: white; // 关键代码
filter: contrast(20); // 关键代码,加强对比度
// decoration作用是帮助大圆实现融合效果,藏在大圆下面
.decoration {
width: @radius;
height: @radius;
border-radius: @radius;
position: absolute;
background-color: @limegreen;
filter: blur(6px);
animation: upAndDown 2s ease infinite; // 关键代码
}
// 小圆球的样式
.circles {
filter: blur(6px); // 关键代码,模糊边缘
background-color: @limegreen;
position: absolute;
}
// 底座
.bottom {
width: @radius;
height: @radius / 3;
border-radius: 50% e('/') 100% 100% 0 0; // 画椭圆的时候防止斜杠被LESS编译成除法运算,所以这样写
position: absolute;
bottom: -1px;
background-color: @limegreen;
filter: blur(6px); // 关键代码
}
}
@keyframes帧动画
上面卖的帧动画防跳帧的关子现在告诉大家:
// 通过内外阴影变化模拟旋转呼吸效果
@keyframes breathe {
from, to {
box-shadow: 3px 3px 20px @limegreen inset, -3px -3px @limegreen;
}
25% {
box-shadow: -3px 3px 20px @limegreen inset, 3px -3px @limegreen;
}
50% {
box-shadow: -3px -3px 20px @limegreen inset, 3px 3px @limegreen;
}
75% {
box-shadow: 3px -3px 20px @limegreen inset, 3px -3px @limegreen;
}
}
// .decoration的上下位移动画
@keyframes upAndDown {
from, to {
top: -@scale/2 + 3px;
}
50% {
top: -@scale/2 + 8px;
}
}
其实很简单,我们只需要保证动画首尾状态相同,就可以防止跳帧,平滑过渡。就是 from, to {} (或 0%, 100% {})
连在一起写。原理其实很简单,动画经过中间的变化最终又变了回去,既然变回去了,自然就是平滑的过渡了。
这里我们去掉from看看效果,去掉to也一样:
很显然,动画进行到最后回到起点时的过渡非常生硬,这不是我们想要的效果。 我需要强调,千万不要小看了CSS,它就是这么强,强的可怕,只要你能想到,就能做出来,问题是,大多时候我们想不到,就拿障眼法来说,我也是刚学会。下面这个关于大圆我最初的想法,也从一定程度上也说明了CSS的强大。我最初的想法是:
- 仿照
.decoration
,在下面加一个圆 - 加入动画使隐藏圆旋转和缩放,模拟呼吸效果
- 变换阴影,更生动
但是很快我想起了CSS3新特性:box-shadow
,阴影效果可以有多个,意思就是我可以同时变换内外阴影,阴影就代替了我原本要加的这个隐藏圆,真的是方便。
关于阴影
当你准备在帧动画中运用的时候,一定要画个草图研究一下参数哦。参数规划的不好可能造成动画每个阶段速度不一致,很奇怪的。拿最基础的前两个参数,也就是X轴和Y轴的偏移量来说。首先你要明白坐标系是,向下向右为正的。看似是旋转动画,其实是分别变换左右和上下的阴影实现的障眼法,没错,又是障眼法。
通过浏览器中坐标的规律我们可以推算出当内部阴影处在四个角落时候的坐标如下图所示,外阴影同理:
所以呀,好的效果还是要多尝试,多打磨。但打磨也要讲究方法,坐标是规划出来的,越复杂,越需要规划,不然学数学干嘛,不是随便填两个试试得来的。按照0% ~ 100%
中间四个帧,依次填充以上四个坐标即可,无所谓先后,看你想要什么样子的效果了。我选的效果是内阴影顺时针旋转,所以我的坐标填充顺序是左上角开始,顺时针。外阴影和内阴影相反,对调一组对角坐标。
这就是我上面的坐标由来,也是因为自己都快绕晕了,才索性找来纸笔好好规划了一番。接下来讲一讲如何绘制这些小球。
Circle类
负责描述圆点属性,对应上篇文章中的Snowflake类。这个类还是很简单的。
class Circle {
constructor () {
this.init()
}
init () {
this.radius = random(15, 30)
this.vy = random(1500, 3000, true) // 使用时间描述速度,时间越长速度越慢
// x初始横坐标位置,需要控制在这个范围内,可以使得小球更加集中
this.x = random(Circle.range, Circle.width - Circle.range) // 这里的
// 这就是主角啦
this.dom = this.create()
}
create () {
let c = this.dom || document.createElement('div') // 复用已有元素
c.style.width = this.radius + 'px'
c.style.height = this.radius + 'px'
c.style.borderRadius = this.radius + 'px'
c.style.left = this.x - this.radius / 2 + 'px' // 中心点
c.style.bottom = '12px' // 底部留一点空隙,动画更自然
c.classList.add('circles')
return c
}
}
思考
为什么需要init()
方法来初始化属性,直接写在constructor中不是更好?
不是的,动画涉及的对象属性是需要定期更新的,有了这个方法,类的实例才能通过调用原型中的init()方法重置所有属性,这样才能达到随机效果哦。
Run类
class Run {
constructor() {
this.container = document.querySelector('.circle-container')
// 设置静态量,描述小球画布范围
Circle.width = this.container.offsetWidth // 画布宽度
Circle.height = this.container.offsetHeight // 小球移动的最大高度
Circle.range = Circle.width / 4 // 左右留下的间距
this.circles = this.createCircle(10) // 造圆,数量可调整
this.run()
}
createCircle (num) {
let circles = new Set()
for (let i=0; i<num; i++) {
circles.add(new Circle())
}
return circles
}
animation (circle) {
this.container.appendChild(circle.dom)
// 这里借用jQuery.animate()方法
return $(circle.dom).animate({
bottom: Circle.height + 'px'
}, {
duration: circle.vy,
// 动画完成后循环调用自身即可,挺像上篇文章里的requestAnimationFrame的是不是?
complete: () => {
circle.init() // 刷新属性,删除完成使命的小球,防止无限插入
this.animation(circle)
}
})
}
run () { // 启动动画
this.circles.forEach(item => {
this.animation(item)
})
}
}
一样的思路,一样的面向对象,我们又完成了一个动画效果。也许日后我们真的会用到自己做的这个动画效果。有兴趣的话可以把它造成一个轮子,像上篇文章里的snow.js一样,封装成一个小插件,留出可配置的接口,简单配置即可完成动画。
最后
CSS中,很多看似高级的动画,都是通过障眼法打造的。如果你仔细看完这篇文章,一定会有所收获,就算没看懂代码,也能打开一些思路。如果可以把GitHub中的代码拷贝下来研究一下的话,相信你会学到更多。有兴趣的掘友可以为仓库贡献一些代码,或者留言区燥起来,都是非常欢迎的奥。
终于讲完了,撰文一下午,腰酸背痛。如果你看完觉得有收获,记得给个赞慰劳慰劳呀(卡姿兰大眼睛),非常感谢。