在之前的文章 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;
}
}複製程式碼
- 這裡把之前用到的 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
另外不要吐槽我的命名,下劃線開頭表示私有函式,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
, 沒有就不搞了; - 如果有動畫已經在執行,則取消掉之前的;
- 設定一些初始值,獲取 Canvas 元素及其 Context,並清除畫布;
- 判斷配置中要畫的東西是文字還是圖片,分別呼叫相應的函式。
_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
引數的值傳進去。
這裡的圖片用的是這樣的:
效果:
控制檯也可以看到 onFinish
回撥的輸出:
onFinish
<img src="images.jpeg">
onFinish
掘金複製程式碼
支援 AMD&CMD
最後我們再來折騰一下,讓我們的類不僅可以普通呼叫,還可以支援 seajs
和 requirejs
。
在類的外面,加入以下程式碼就搞定了:
// 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();
});複製程式碼
總結
到這裡就基本搞完了,程式碼比較多,推薦跑一下原始碼,對照著看,有不清楚的也可以翻翻之前的文章,或者留言交流哈。
ParticleMaker的GitHub地址: github.com/bob-chen/Pa…
Demo的原始碼地址: github.com/bob-chen/ca…
碎碎念
最近總想記錄一些所思所想,寫寫科技與人文,寫寫生活狀態,寫寫讀書感悟,發在微信公眾平臺上,主要是扯淡和感悟,歡迎關注,交流。
微信公眾號:程式設計師的詩和遠方
公眾號ID : MonkeyCoder-Life