该篇文章的例子同时也算是我CSS动画的启蒙之做,思路来源于掘友的一篇文章,文末有连接,但只提供思路,因而我打算实现一波,顺便分享一下动画制做过程当中的心得体会。也算是应证了,当你打开思路之后,动画,真的很简单。css
接上篇文章JS动画?其实没你想的那么难。150行代码,带你走进新世界,本文继续带你们领略动画之美。html
实例基于CSS3,暂未考虑浏览器兼容问题,更可能是帮助你们打开思路,实际作得很差的地方你们能够本身修正,本身DIY。项目中的CSS是经过LESS
编译的。如何在普通项目中使用LESS?,答案在LESS中文官网。只须要引入一个less.js
的CDN便可,是否是很方便?它会让你爱上CSS。git
这是效果图:github
下面咱们进入正题,教你们如何画这两种电池。canvas
想要玩好CSS动画,障眼法是少不了的。何为障眼法? api
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的层叠关系,只有恰当的层叠才能显现出合适的效果。浏览器
下面是动画:bash
// 波浪的效果是主要是上下移动的过程当中一边旋转
@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
等字眼时,都会本能的放弃思考,不要紧,我也是。老是以为这些东西极其复杂。但实际上,仍是那句话,万事开头难,当你真的了解到它的强大以后,定会爱不释手。app
动画作完后须要添加到须要应用的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,不难想到,咱们须要提早绘制好若干大小不一的圆点,在经过设置不一样的时间,循环执行便可。此种方法须要绘制较多的圆形,须要许多DIV容器,DOM结构可能比较庞大,并且细心观察是能够看出动画的规律的。为了呼应上篇JS动画的文章中提到的面向对象思想分解动画,我决定采用JS实现。无规律的动画才有成就感,你说呢?
纯CSS也能够实现,有兴趣的掘友能够尝试呀,两种方法各有优劣。若是有结果的话,欢迎评论区留言,让我看看嘛(手动卖萌)。
在上篇文章中提到,如何分解一个动画?面向对象的思想分析可得,小球就是一个对象,咱们能够抽象出一个类,用来绘制小球并动态插入。使用Math.random()计算随机位置和半径大小。经过Circle类描述圆点的属性,经过Run类控制圆点的建立,插入,删除,运动等。
这里,顺便推荐一个好用的,不依赖jQuery的动画库Velocity.js, 我比较喜欢他的一个特色,每个动画均可以返回一个Promise对象,在某些状况下涉及异步流程控制的时候,确实很好用,并且很轻量。
下面看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>
复制代码
为了防止大圆的显示受到对比度滤镜的影响,单独提出来写,再定位到圆点容器上便可。因为单独提出来写,致使大圆没有融合效果,因此在圆点容器中增长一个隐藏层,定位到大圆下面,帮助它实现融合效果。这叫啥?没错,障眼法。
@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: 12px;
}
// 底座
.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 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也同样:
我最初的想法是:
.decoration
,在下面加一个圆可是很快我想起了CSS3新特性:box-shadow
,阴影效果能够有多个,意思就是我能够同时变换内外阴影,阴影就代替了我本来要加的这个隐藏圆,真的是方便。
当你准备在帧动画中运用的时候,必定要画个草图研究一下参数哦。参数规划的很差可能形成动画每一个阶段速度不一致,很奇怪的。拿最基础的前两个参数,也就是X轴和Y轴的偏移量来讲。首先你要明白坐标系是,向下向右为正的。看似是旋转动画,实际上是分别变换左右和上下的阴影实现的障眼法,没错,又是障眼法。
经过浏览器中坐标的规律咱们能够推算出当内部阴影处在四个角落时候的坐标以下图所示,外阴影同理:
按照0% ~ 100%
中间四个帧,依次填充以上四个坐标便可,无所谓前后,看你想要什么样子的效果了。我选的效果是内阴影顺时针旋转,因此个人坐标填充顺序是左上角开始,顺时针。外阴影和内阴影相反,对调一组对角坐标。
这就是我上面的坐标由来,也是由于本身都快绕晕了,才索性找来纸笔好好规划了一番。接下来说一讲如何绘制这些小球。
负责描述圆点属性,对应上篇文章中的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 = 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' // 保证插入容器后是以中心点计算位置的,是否是很相似canvas
c.classList.add('circles') // 添加一个自定义类以便外部添加样式
return c
}
}
复制代码
为何须要init()
方法来初始化属性,直接写在constructor中不是更好?
不是的,动画涉及的对象属性是须要按期更新的,有了这个方法,类的实例才能经过调用原型中的init()方法重置全部属性,这样才能达到随机效果哦。
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)
// 引入velocity.js以后,dom元素将会挂载上velocity方法,直接使用便可,api同jQuery.animate()
return circle.dom.velocity({
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中的代码拷贝下来研究一下的话,相信你会学到更多。有兴趣的掘友能够为仓库贡献一些代码,或者留言区燥起来,都是很是欢迎的奥。
终于讲完了,撰文一下午,腰酸背痛。若是你看完以为有收获,记得给个赞慰劳慰劳呀(卡姿兰大眼睛),很是感谢。