展转许久,终于决定拿起笔杆,记录下本身是如何克服所谓的恐惧,爱上写动画。本文面向JS基础不错的掘友,若是你的JS掌握的还不够好,记得必定要先打牢基础哦。html
动画真的没有你想的那么复杂,这是我最近亲历亲为得出的结论。曾几什么时候,我觉得动画须要很是扎实的数学、物理基础(基础好固然不错,没基础也不要怕),但其实不是这样。它须要的基础更多还是JS,是面向对象思想,是你的编程功底。数学物理基础当然要有,但这些主要影响的是你作出来的动画的复杂程度和精密程度。因此不要怕,万事开头难,等你找到了规律和诀窍,便会一发不可收拾地爱上它。git
本文采用一个雪花飞舞的动画实例,带你走进JS动画的大门。 点击预览es6
动画其实都是由无数的画面连续播放产生的效果,每一幅画面称做一帧,作动画其实就是画好每一帧,而后连续播放便可。咱们主要关注的是如何画出每一帧,以及如何播放。github
该实例的项目移步GitHub仓库,项目下的snow.js即为核心代码,以为动画不错的掘友能够clone下来研究一下,也可直接将snow.js引入使用,经过一些配置快速实现特效哦。具体参见GitHub仓库。编程
主要使用 canvas
绘图,经过window.requestAnimationFrame
实现刷新。经过浏览器的window.requestAnimationFrame方法发起动画帧请求,并传入一个回调函数,浏览器会在恰当的时机执行这个回调函数(也就是咱们的重绘函数)。canvas
漫天飞舞的雪花,如何才能用程序的方式表现出来?利用面向对象的编程思想很容易分析出,每一片雪花都是一个对象,这个对象有大小,形状,速度,等等信息,天然而然的,咱们能够抽象出一个Snowflake类,用于建立雪花对象,描述他的属性,用于动画中。雪花不可能彻底相同,所以建立的时候,须要用到随机数。这里建立两个用来获取随机元素的函数便于开发:数组
// 生成随机数
const random = function (min, max, floor = false) {
// 是否取整?
if (floor) {
return Math.floor(min + Math.random() * (max - min))
} else {
return min + Math.random() * (max - min)
}
};
// 从数组中获取随机元素
// 因为Math.floor是向下取整,max = arr.length,而非arr.length - 1
const randomIn = arr => arr[random(0, arr.length, true)];
复制代码
雪花类(Snowflake)其实是一个实体类,建立出来的个体虽然包含了雪花的各类属性,可是它并不能本身运动,咱们须要一个控制器来操做雪花,让他运动起来,因而咱们有了一个控制类——Snow。Snow主要负责利用canvas绘制每一帧,并连续播放帧,造成动画。浏览器
先放出总体代码,特意加了详细的注释:bash
class Snowflake {
constructor(config, image = null) {
this.config = config;
this.image = image;
this.load()
}
center () {
let x = this.x + this.radius / 2;
let y = this.y + this.radius / 2;
return {x, y} // ES6对象简写,实际是{x: x, y: y}
}
load () {
// 初始化绘图属性
this.x = random(0, window.innerWidth); // x轴起始位置
this.y = random(-window.innerHeight, 0); // y轴起始位置,在屏幕外
this.alpha = random(...this.config.alpha); // 透明度
this.radius = random(...this.config.radius); // 大小
this.color = randomIn(this.config.color); // 颜色
this.angle = 0; // 起始旋转角度
this.flip = 0; // 起始翻转参数,翻转是利用缩放和旋转模拟出来的
// 初始化变换属性
this.va = Math.PI / random(...this.config.va); // 旋转速度
this.va = Math.random() >= 0.5 ? this.va : -this.va; // 旋转方向
this.vx = random(...this.config.vx); // x轴移动速度
this.vy = random(...this.config.vy); // y轴移动速度
// 翻转速度,默认不翻转,这里作一下判断,!!转换布尔值
// vf 表明翻转速度,也就是缩放速度
!!this.config.vFlip && (this.vf = random(0, this.config.vFlip))
}
update(range) {
// 每调用一次都会致使相应的属性变化,变化速度取决于相应速度
this.x += this.vx;
this.y += this.vy;
this.angle += this.va;
this.flip += this.vf;
// 防止无限放大,缩放比例维持在0-1之间
if (this.flip > 1 || this.flip < 0) {
this.vf = -this.vf
}
// 当元素飞出范围则重置属性,复用元素
if (this.y >= range + this.radius ) {
this.load()
}
}
}
复制代码
一样,先放出代码。代码较长,主要是由于增长了注释,耐心看完必定会有所收获:网络
class Snow {
constructor(container, config = {}) {
let {num} = config;
// 雪花数量,通常无需改动
this.num = num || window.innerWidth / 2;
delete config['num'];
// 这里是默认配置,经过Object.assign()使传入的配置覆盖默认配置
this.config = Object.assign({
image: [], // 可选的图片(网络或本地)
vx: [-3, 3], // 水平速度
vy: [2, 5], // 垂直速度
va: [45, 180], // 角速度范围,传入图片才会生效
vFlip: 0, //翻转速度,推荐:慢0.05/正常0.1/快0.2
radius: [5, 15], // 半径范围,传入图片需调整此项
color: ['white'], // 可选颜色,传入图片时会忽略该项
alpha: [0.1, 0.9] // 透明度范围
}, config);
this.init(container)
}
init (container) {
// 初始化基本配置
this.container = document.querySelector(container); // 获取dom
this.canvas = document.createElement('canvas');
// 获取dom元素实际宽高,并让canvas充满dom。视状况添加CSS代码
this.canvas.width = this.container.offsetWidth;
this.canvas.height = this.container.offsetHeight;
// 获取上下文
this.ctx = this.canvas.getContext('2d');
// 插入文档后才能显示出来
this.container.appendChild(this.canvas);
this.snowflakes = new Set(); // 用来存放建立的雪花
// 根据传入的配置肯定画图仍是画圆
// 两种模式,默认用大小形状不一的白色圆形代替雪花
// 若传入图片则绘制图片,例如自定义雪花图片
if (!!this.config.image.length) {
// 加载传入的图片地址,等待图片彻底加载完成后开始建立雪花
this.loadImage(this.config.image).then(images => {
this.createSnowflakes(images);
// 发起动画请求,drawPicture返回一个绑定了了this的帧动画函数
requestAnimationFrame(this.drawPicture())
}).catch(e => console.error(e))
} else {
this.createSnowflakes();
requestAnimationFrame(this.drawCircle())
}
}
loadImage (images) {
// 定义一个加载图片的函数,使用Promise封装
let load = (src) => new Promise(resolve => {
let image = new Image();
image.src = src;
image.onload = () => resolve(image);
image.onerror = e => console.error('图片加载失败:' + e.path[0].src)
});
// 使用Promise.all()等待全部异步操做执行完毕
return Promise.all(images.map(src => load(src)))
}
createSnowflakes (image) {
if (image) {
for (let i = 0; i < this.num; i++) {
let img = randomIn(image);
let flake = new Snowflake(this.config, img);
this.snowflakes.add(flake)
}
} else {
for (let i = 0; i < this.num; i++) {
let flake = new Snowflake(this.config);
this.snowflakes.add(flake)
}
}
}
// 提取公共变换代码
transform (flake) {
// 所谓变化就是按照雪花的属性绘制,属性变了,绘制的东西天然就变了
// 串起来就造成了动画,因此这里须要调用update()更新属性
// 更新完属性后再进行绘制,串连起来就能够产生动画效果
flake.update(this.canvas.height);
let {x, y} = flake.center();
// 注意,canvas中旋转的是画布,移动的也是画布
// 先将画布移动到雪花中心再进行变换
this.ctx.translate(x, y);
this.ctx.rotate(flake.angle);
// 判断是否须要翻转(缩放)
!!flake.vFlip && this.ctx.scale(1, flake.flip);
// 变换结束记得移回原位
this.ctx.translate(-x, -y)
}
// 返回一个帧动画函数
drawCircle () {
// 由于window.requestAnimationFrame执行时this指向window
// 此函数里又使用了大量this,因此须要确保this指向不能变
// 这里使用箭头函数的话,this就会指向Snow,能够正常使用this了
let frame = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let flake of this.snowflakes) {
// 每次变化以前必定要调用save()保存状态,最后使用restore()恢复
// 不然画布的属性将会乱套
this.ctx.save();
this.transform(flake); // 公共变化
// 下来是画圆的过程,具体参考Canvas API
this.ctx.beginPath();
this.ctx.arc(flake.x, flake.y, flake.radius,0,2*Math.PI);
this.ctx.closePath();
this.ctx.globalAlpha = flake.alpha;
this.ctx.fillStyle = flake.color;
this.ctx.fill();
this.ctx.restore() // 恢复canvas上下文属性
}
// requestAnimationFrame 函数最后再讲
requestAnimationFrame(frame)
};
return frame
}
drawPicture() {
// 同上
let frame = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let flake of this.snowflakes) {
this.ctx.save();
this.transform(flake);
this.ctx.globalAlpha = flake.alpha;
this.ctx.drawImage(flake.image, flake.x, flake.y, flake.radius, flake.radius);
this.ctx.restore()
}
requestAnimationFrame(frame)
};
return frame
}
}
复制代码
想了解箭头函数和更多ES6新特性,推荐阮一峰《ES6标准入门》
前面提到,传入一个回调函数,浏览器会在恰当的时机执行,可是只会执行一次。所以咱们须要在回调函数里面再次使用requestAnimationFrame调用自身,如此,便能造成动画循环。
其实相似setInterval
,甚至更像链式setTimeout
,循环调用自身。浏览器判断的执行时机间隔大概是十几毫秒,这样的刷新速率,肉眼是不可能辨别的,因而就实现了动画效果。
那咱们为何要使用requestAnimationFrame
呢?由于浏览器判断时机的意思就是,若是你切换到了后台,动画就会中止渲染,直到你切换回来,以及相似的状况。而前者则会一直运行,无疑对性能是一种损耗。但损耗归损耗,老版本浏览器若是不支持的话,仍是得用这些古老的方法,但你们要知道原理奥。
更多请参考MDN——window.requestAnimationFrame
Canvas绘制时必定要先保存状态,每个对象绘制完毕后恢复状态,避免画布配置错乱。
示例代码已完善,可投入使用,简单配置便可实现飞舞效果,可用做背景画面或玻璃窗效果。具体参考GitHub。
// 传入id,默认配置下,雪花为大小、透明度不一的白色圆点
new Snow('#snow')
复制代码
// 配置相应项便可,不配置则应用默认配置
new Snow('#snow', {
image: [], // 可选的图片(网络或本地)
vx: [-3, 3], // 水平速度
vy: [2, 5], // 垂直速度
va: [45, 180], // 角速度范围,传入图片才会生效
vFlip: 0, // 翻转速度,推荐:慢0.05/正常0.1/快0.2
radius: [5, 15], // 半径范围,传入图片需调整此项
color: ['white'], // 可选颜色,传入图片时会忽略该项
alpha: [0.1, 0.9] // 透明度范围
num: window.innerWidth / 2, // 雪花数量,通常无需改动
})
复制代码
示例代码:
new Snow('#snow', {
image: ['./snow.png'],
radius: [10, 80]
})
复制代码
效果以下: