Canvas 基礎 - 粒子動畫 Part4

在以前的文章 Canvas基礎-粒子動畫Part2Canvas基礎-粒子動畫Part3 中分別講了用圖片和文字作粒子動畫,今天咱們來把代碼簡單整理一下,封裝成一個類,能同時支持用圖片和文字作粒子動畫,並且有更好的靈活性。javascript

封裝類

HTML結構和上一篇的同樣,這裏從外部引入一個js文件,咱們的類就寫這裏面。css

<body>
        <div class="input-wrap">
            <input id="txt" type="text" name="" value="" placeholder="輸入發射文字...">
            <button id="btn" class="btn">發射</button>
        </div>
        <canvas id="canvas" width="300" height="300" ></canvas>
        <script type="text/javascript" src="./particle-maker.js"></script>
    </body>複製代碼

以後在 particle-maker.js 文件中,寫咱們的類,取名叫 ParticleMaker ,而後把咱們須要的一些參數啊什麼的給定義進去。html

 "use strict";

var gRafId = null; //requestAnimationFrame id, new ParticleMaker() 的時候要能把前一次的動畫取消
function ParticleMaker(conf) {
    var me = this,
        canvas = null,  // canvas element
        ctx = null,   // canvas contex
        dotList = [], // dot object list
        // rafId = gRafId, // rafid, 不能放在此處,由於 new 對象的時候會覆蓋,沒法取消前一次的動畫
        finishCount = 0; // finish dot count

    var fontSize = conf["fontSize"] || 500,
        fontFamily = conf["fontFamily"] || "Helvetica Neue, Helvetica, Arial, sans-serif",
        mass = conf["mass"] || 6, // 取樣密度
        dotRadius = conf["dotRadius"] || 2, // 點半徑
        startX = conf["startX"] || 400, // 開始位置X
        startY = conf["startY"] || 400, // 開始位置Y
        endX = conf["endX"] || 0, // 結束位置X
        endY = conf["endY"] || 0, // 結束位置Y
        effect = conf["effect"] || "easeInOutCubic", // 緩動函數
        fillColor = conf["fillColor"] || "#000", // 填充顏色
        content = conf["content"] || "Beta"; // 要畫的東西,若是是圖片須要 new Image() 傳進來

    // 緩動函數
    // t 當前時間
    // b 初始值
    // c 總位移
    // d 總時間
    var effectFunc = {
        easeInOutCubic: function (t, b, c, d) {
            if ((t/=d/2) < 1) return c/2*t*t*t + b;
            return c/2*((t-=2)*t*t + 2) + b;
        },
        easeInCirc: function (t, b, c, d) {
            return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
        },
        easeOutQuad: function (t, b, c, d) {
            return -c *(t/=d)*(t-2) + b;
        }
    }

    if (typeof effectFunc[effect] !== "function") {
        console.log("effect lost, use easeInOutCubic");
        effect = "easeInOutCubic";
    }

    function Dot(centerX, centerY, radius) {
        this.x = centerX;
        this.y = centerY;
        this.radius = radius;
        this.frameNum = 0;
        this.frameCount =  Math.ceil(3000 / 16.66);
        this.sx = startX;
        this.sy = startY;
        this.delay = this.frameCount*Math.random();
        this.delayCount = 0;
    }
}複製代碼
  • 這裏把以前用到的 rafId 給放到全局了,由於若是放到 ParticleMaker 類裏面,下次 new 的時候會覆蓋,這樣就無法取消掉以前的動畫了;
  • 又另外添加了兩個緩動函數,而且緩動函數默認爲 easeInOutCubic 更多的緩動函數也按這個形式添加就能夠了;
  • 把以前的一些變量抽出來做爲參數,並添加默認值。

這步比較簡單,看過以前文章的比較好理解。添加完類,咱們再把以前用到的幾個函數給弄過來。java

this._setFontSize = function(s) {
        ctx.font = s + 'px ' + fontFamily;
    }
    this._isNumber = function(n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
    }
    this._cleanCanvas = function() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }
    this._handleCanvas = function() {

        var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        // console.log(imgData);

        for(var x=0; x<imgData.width; x+=mass) {
            for(var y=0; y<imgData.height; y+=mass) {
                var i = (y*imgData.width + x) * 4;
                if(imgData.data[i+3] > 128 && imgData.data[i] < 100){
                    var dot = new Dot(x, y, dotRadius);
                    dotList.push(dot);
                }
            }
        }

    }複製代碼

除了用來清除畫布的 _cleanCanvas 是新定義的,其它三個函數是咱們以前用過的,主要根據類的參數對以前的變量作一些改動。git

好比 _handleCanvas 中的循環 for(var x=0; x mass 表明取樣的密度,以前是寫死的6,這裏改爲能夠經過參數調整的,這個值越小,點越密,關於這個參數的更多信息能夠參考第一篇文章 Canvas基礎-粒子動畫Part1 程序員

另外不要吐槽個人命名,下劃線開頭表示私有函數,Python你懂的。github

以後咱們須要一個 render 方法,用來把那些通過 _handleCanvas 處理以後的點,給渲染出來。canvas

this.render = function() {

        me._cleanCanvas();
        ctx.fillStyle = fillColor;

        var len = dotList.length,
            curDot = null,
            frameNum = 0,
            frameCount = 0,
            curX, curY;

        finishCount = 0;

        for(var i=0; i < len; i+=1) {
            // 當前粒子
            curDot = dotList[i];

            // 獲取當前的time和持續時間和延時
            frameNum = curDot.frameNum;
            frameCount = curDot.frameCount;

            if(curDot.delayCount < curDot.delay){
                curDot.delayCount += 1;
                continue;
            }

            ctx.save();
            ctx.beginPath();

            if(frameNum < frameCount) {
                curX = effectFunc[effect](frameNum, curDot.sx, curDot.x-curDot.sx, curDot.frameCount);
                curY = effectFunc[effect](frameNum, curDot.sy, curDot.y-curDot.sy, curDot.frameCount);

                ctx.arc(curX, curY, curDot.radius, 0, 2*Math.PI);
                curDot.frameNum += 1;

            } else {
                ctx.arc(curDot.x, curDot.y, curDot.radius, 0, 2*Math.PI);
                finishCount += 1;
            }
            ctx.fill();
            ctx.restore();

            if (finishCount >= len) {
                // console.log(gRafId);
                cancelAnimationFrame(gRafId);
                return conf["onFinish"] && conf["onFinish"]();
            }
        }

        // gRafId = requestAnimationFrame(arguments.callee);
        gRafId = requestAnimationFrame(me.render);
    }複製代碼

這個函數大致和 Canvas基礎-粒子動畫Part2 中的同樣,爲了閱讀連貫性,我把其中的解釋給拷貝過來了:瀏覽器

  • 動畫進行中的時候frameNum < frameCount,經過前面的緩動函數計算出當前應該到達的x,y值,而後畫到Canvas上並將這個點的幀數加一。
  • 最後一個幀的時候,也就是else條件,就不要畫計算出來的值了,畫實際應該在的位置。
  • 必定要注意ctx.beginPath()ctx.fill(),否則你的畫布上啥子都沒有。
  • 定義了一個finishCount,用來在每次畫粒子的時候統計有多少個是已經跑到相應位置了,因此每次循環開始前都要將其置爲0,當跑到位的粒子數量和總粒子數量相等的時候,就調用cancelAnimationFrame並退出,停掉相應的繪製,不要浪費資源。
  • 還有就是判斷是否停掉要放在ctx.fill()以後作,否則有會出現少了一個粒子的狀況。

這裏對其作了一些小改動:微信

  • effectFunc[effect] 緩動函數從配置中讀取;
  • conf["onFinish"] && conf["onFinish"]() 當初始化的配置中有設置完成的回調時,這裏調用一下。
  • requestAnimationFrame(arguments.callee) 這裏特別說明一下,原本調用函數自己這個是想用 arguments.callee 來作的,callee 表示正被執行的函數對象,也就是 render 函數自己,可是咱們在文件開頭聲明瞭使用嚴格模式 use strict ,嚴格模式下不給用arguments, caller, callee,因此換成了 gRafId = requestAnimationFrame(me.render)

最後咱們須要讓動畫跑起來的 run 方法和支持畫文字和畫圖片的 drawTextdrawImage 方法。

this.run = function() {
        if( !conf["canvasId"] ){
            console.log("No canvas Id");
            return;
        }

        // 有正在運行的動畫要取消掉
        if (gRafId) cancelAnimationFrame(gRafId);

        dotList = [];
        finishCount = 0;

        canvas = document.getElementById(conf["canvasId"]);
        ctx = canvas.getContext("2d");

        this._cleanCanvas();

        var drawFunc = this.drawText;
        if( typeof content === "object" && content.src && content.src != "" ){
            drawFunc = this.drawImage;
        }

        drawFunc(content);

        // Move to this._run();
        // this._handleCanvas();
        // this._cleanCanvas();
        // this.render();

    }

    this._run = function(){
        // ctx.save();

        this._handleCanvas();

        this._cleanCanvas();

        this.render();
    }

    this.drawText = function(l) {
        // init canvas
        ctx.textBaseline = "top";

        me._setFontSize(fontSize);
        var s = Math.min(fontSize,
                  (canvas.width / ctx.measureText(l).width) * 0.8 * fontSize, 
                  (canvas.height / fontSize) * (me._isNumber(l) ? 1 : 0.5) * fontSize);
        me._setFontSize(s);

        ctx.fillStyle = "#000";
        ctx.fillText(l, endX, endY); // 最後位置

        me._run();
    }

    this.drawImage = function(img) {

        if(img.complete){
            ctx.drawImage(img, endX, endY);
            me._run();
        } else {
            img.onload = function(){
                ctx.drawImage(img, endX, endY);
                me._run();
            }
        }
    }複製代碼

由於畫文字是很快的,能夠是順序同步的,而畫圖片可能有一個等待圖片 onload 的過程,這裏是可能有異步調用的狀況。下面來解釋一下:

首先是 run 方法,作的事情比較簡單:

  1. 檢查配置裏面是否有 canvasId, 沒有就不搞了;
  2. 若是有動畫已經在運行,則取消掉以前的;
  3. 設置一些初始值,獲取 Canvas 元素及其 Context,並清除畫布;
  4. 判斷配置中要畫的東西是文字仍是圖片,分別調用相應的函數。

_run 方法,這個是調用畫文字或者圖片以後要執行的步驟,由於有等待圖片異步調用的狀況,因此要單獨出來。

drawText 方法比較簡單,判斷 fontSize 是否合適,寫文字上去,而後當即調用 _run 方法。

drawImage 方法首先用 compelete 屬性判斷一下圖片是否加載完了,沒加載完則設個 onload 事件,等加載完再畫圖片以及調用 _run 方法。

到這裏整個類就基本OK了,爲了不 requestAnimationFrame 方法在部分瀏覽器沒有,能夠加個polyfill。

var requestAnimationFrame = window.requestAnimationFrame ||
                    function(callback) {
                        return window.setTimeout(callback, 1000 / 60);
                    };  

    var cancelAnimationFrame = window.cancelAnimationFrame ||
                    function(id) {
                        window.clearTimeout(id);
                    }複製代碼

調用方法

簡單寫下調用方法:

var canvas = document.getElementById("canvas"),
        ctx = canvas.getContext('2d'),
        winWidth = document.documentElement.clientWidth,
        winHeight = document.documentElement.clientHeight;

    canvas.width = winWidth;
    canvas.height = winHeight;


    document.querySelector("#btn").addEventListener("click", function(){
        init();
    })

    function init() {

        var s = 0;
        input = document.querySelector("#txt");

        // var l = input.value ? input.value : "Beta";

        var l = input.value;
        if( !input.value ) {
            l = new Image();
            l.src = "images.jpeg";
        }

        input.value = "";

        // normal useage
        var particleMaker = new ParticleMaker({
            canvasId: "canvas",
            startX: 200,
            startY: 400,
            endX: 10,
            endY: 40,
            // mess: 10,
            // dotRadius: 3,
            content: l,
            fillColor: "#ff4444",
            effect: "easeOutQuad",
            onFinish: function(){
                console.log("onFinish");
                console.log(l);
            }
        });

        particleMaker.run();
    }複製代碼

代碼比較簡單,一開始給 Canvas 設置各類屬性,而後當點擊按鈕的時候,調用 init 方法, init 方法中判斷下輸入框有沒有輸入過東西,沒輸入東西就拿個圖片作,輸入過東西就把輸入的東西做爲 content 參數的值傳進去。

這裏的圖片用的是這樣的:

https://user-gold-cdn.xitu.io/2016/11/29/294750e6a92a0a929f7ac79a367fb2dd.jpg

效果:

https://user-gold-cdn.xitu.io/2016/11/29/9bed01f9ed70762e610ce94a76ad143d.gif

控制檯也能夠看到 onFinish 回調的輸出:

onFinish
<img src="images.jpeg">​
onFinish
掘金複製代碼

支持 AMD&CMD

最後咱們再來折騰一下,讓咱們的類不只能夠普通調用,還能夠支持 seajsrequirejs

在類的外面,加入如下代碼就搞定了:

// AMD & CMD Support
window.ParticleMaker = ParticleMaker;
if (typeof define === "function") {
    define(function(require, exports, module) {
        module.exports = ParticleMaker;
    })
}複製代碼

調用:

先從CDN上搞個 seajs 來:

<!--<script type="text/javascript" src="./particle-maker.js"></script>-->
    <script src="//cdn.bootcss.com/seajs/3.0.2/sea.js"></script>複製代碼

而後修改下 init 函數裏面的調用:

// seajs useage
        seajs.use("./particle-maker", function(ParticleMaker) {
            var particleMaker = new ParticleMaker({
                canvasId: "canvas",
                startX: 200,
                startY: 400,
                endX: 10,
                endY: 40,
                // mess: 10,
                // dotRadius: 3,
                content: l,
                fillColor: "#ff4444",
                effect: "easeOutQuad",
                onFinish: function() {
                    console.log("onFinish");
                    console.log(l);
                }
            });

            particleMaker.run();
        });複製代碼

總結

到這裏就基本搞完了,代碼比較多,推薦跑一下源碼,對照着看,有不清楚的也能夠翻翻以前的文章,或者留言交流哈。

Canvas基礎-粒子動畫Part1

Canvas基礎-粒子動畫Part2

Canvas基礎-粒子動畫Part3


ParticleMaker的GitHub地址: github.com/bob-chen/Pa…

Demo的源碼地址: github.com/bob-chen/ca…

碎碎念

最近總想記錄一些所思所想,寫寫科技與人文,寫寫生活狀態,寫寫讀書感悟,發在微信公衆平臺上,主要是扯淡和感悟,歡迎關注,交流。

微信公衆號:程序員的詩和遠方

公衆號ID : MonkeyCoder-Life

https://user-gold-cdn.xitu.io/2016/11/29/92d9e667c10e7d56e05b76c62ddb110d

相關文章
相關標籤/搜索