JS動畫?其實沒你想的那麼難。150行代碼,帶你走進新世界

寫在前面

展轉許久,終於決定拿起筆桿,記錄下本身是如何克服所謂的恐懼,愛上寫動畫。本文面向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繪製每一幀,並連續播放幀,造成動畫。瀏覽器

代碼組織

Snowflake類

先放出總體代碼,特意加了詳細的註釋: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()
        }
    }
}
複製代碼
  1. 構造函數:傳入雪花配置和可選的圖片,無圖片的話,則默認繪製圓形。
  2. load:用來初始化以及重置全部屬性。
  3. center:返回元素當前的中點。
  4. update:繪製每一幀的時候都要調用一次,更新屬性這樣就至關於運動了。

Snow類

一樣,先放出代碼。代碼較長,主要是由於增長了註釋,耐心看完必定會有所收穫:網絡

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標準入門》

  1. constructor:構造函數主要處理默認參數的問題
  2. init:初始化控制類相關屬性
  3. loadImage:使用Promise加載傳入的圖片
  4. createSnowflakes:根據是否傳圖片肯定建立什麼樣的雪花
  5. transform:提取了兩種繪圖方式的公共變換代碼,加以複用
  6. drawCircle:繪製圓形雪花。
  7. drawPicture:繪製圖片,即自定義雪花

關於requestAnimationFrame

前面提到,傳入一個回調函數,瀏覽器會在恰當的時機執行,可是隻會執行一次。所以咱們須要在回調函數裏面再次使用requestAnimationFrame調用自身,如此,便能造成動畫循環。

其實相似setInterval,甚至更像鏈式setTimeout,循環調用自身。瀏覽器判斷的執行時機間隔大概是十幾毫秒,這樣的刷新速率,肉眼是不可能辨別的,因而就實現了動畫效果。

那咱們爲何要使用requestAnimationFrame呢?由於瀏覽器判斷時機的意思就是,若是你切換到了後臺,動畫就會中止渲染,直到你切換回來,以及相似的狀況。而前者則會一直運行,無疑對性能是一種損耗。但損耗歸損耗,老版本瀏覽器若是不支持的話,仍是得用這些古老的方法,但你們要知道原理奧。

更多請參考MDN——window.requestAnimationFrame

總結

使用Canvas繪製動畫的通常步驟以下:

  1. 分析動畫元素,利用面向對象思想將動畫元素變成對象,抽象出相應的類。
  2. 編寫幀動畫函數,以幀爲單位繪製畫面。
  3. 調用API刷新。

注意:

Canvas繪製時必定要先保存狀態,每個對象繪製完畢後恢復狀態,避免畫布配置錯亂。

參考

  1. 阮一峯《ES6標準入門》
  2. MDN——window.requestAnimationFrame

引入使用

示例代碼已完善,可投入使用,簡單配置便可實現飛舞效果,可用做背景畫面或玻璃窗效果。具體參考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,   // 雪花數量,通常無需改動
})
複製代碼

GIF示例

示例代碼:

new Snow('#snow', {
    image: ['./snow.png'],
    radius: [10, 80]
})
複製代碼

效果以下:

GIF示例
相關文章
相關標籤/搜索