Canvas基础-粒子动画Part4
在之前的文章 Canvas基础-粒子动画Part2 和 Canvas基础-粒子动画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; } }
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
方法和支持画文字和画图片的 drawText
和 drawImage
方法。
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
方法,做的事情比较简单:
canvasId
, 没有就不搞了;_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
参数的值传进去。
这里的图片用的是这样的: