展轉許久,終於決定拿起筆桿,記錄下本身是如何克服所謂的恐懼,愛上寫動畫。本文面向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]
})
複製代碼
效果以下: