Canvas基础-粒子动画Part4

Posted on 2016-10-05 in Canvas by yucongchen

在之前的文章 Canvas基础-粒子动画Part2Canvas基础-粒子动画Part3 中分别讲了用图片和文字做粒子动画,今天我们来把代码简单整理一下,封装成一个类,能同时支持用图片和文字做粒子动画,而且有更好的灵活性。

封装类

HTML结构和上一篇的一样,这里从外部引入一个js文件,我们的类就写这里面。

    <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 ,然后把我们需要的一些参数啊什么的给定义进去。

"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 更多的缓动函数也按这个形式添加就可以了;
  • 把之前的一些变量抽出来作为参数,并添加默认值。

这步比较简单,看过之前文章的比较好理解。添加完类,我们再把之前用到的几个函数给弄过来。

    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 是新定义的,其它三个函数是我们之前用过的,主要根据类的参数对之前的变量做一些改动。

比如 _handleCanvas 中的循环 for(var x=0; x<imgData.width; x+=mass) mass 代表取样的密度,之前是写死的6,这里改成可以通过参数调整的,这个值越小,点越密,关于这个参数的更多信息可以参考第一篇文章 Canvas基础-粒子动画Part1

另外不要吐槽我的命名,下划线开头表示私有函数,Python你懂的。

之后我们需要一个 render 方法,用来把那些经过 _handleCanvas 处理之后的点,给渲染出来。

    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 参数的值传进去。

这里的图片用的是这样的:

http://ww2.sinaimg.cn/large/0060lm7Tgw1f7j6l617wuj305g05cq2r.jpg

效果:

http://ww3.sinaimg.cn/large/0060lm7Tgw1f8hqbgormlg30cs0fuh4t.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地址: https://github.com/bob-chen/ParticleMaker

Demo的源码地址: https://github.com/bob-chen/canvas-demo/blob/master/basic/particle-part4.html

碎碎念

最近总想记录一些所思所想,写写科技与人文,写写生活状态,写写读书感悟,发在微信公众平台上,主要是扯淡和感悟,欢迎关注,交流。

微信公众号:程序员的诗和远方

公众号ID : MonkeyCoder-Life

http://www.imbeta.cn/images/wx-qrcode.jpg