在 第一篇 中我們大致分析了從: 建立舞臺 -> 新增顯示物件-> 更新顯示物件 的原始碼實現
這一篇將主要分析幾個常用顯示物件自各 draw 方法的實現
讓我們看向例子 examples/Text_simple.html
這個例子中使用了三個顯示物件類 Bitmap 、Text 、 Shape
以下例子中新增了一個 image
var image = new createjs.Bitmap("imagePath.png");
stage.addChild(image);
當呼叫 stage.update 後,會呼叫顯示物件的 draw 方法,如果是 Container 類,則繼續遞迴呼叫其 draw 方法
這樣所有 stage 舞臺上的顯示物件的 draw 方法都會被呼叫到,注意 canvas 的上下文物件 ctx 引數都會被傳入
分兩步:
如果 DisplayObject 類內有快取,則繪製快取
如果沒有快取則迴圈顯示列表呼叫每個 child 的 draw 方法 child 都為 DisplayObject 範例,還判斷了 DisplayObject 範例的 isVisible 如果不可見則不繪製
// Container 類 原始碼 160 - 176 行
p.draw = function(ctx, ignoreCache) {
if (this.DisplayObject_draw(ctx, ignoreCache)) { return true; }
// 用 slice 的原因是防止繪製過程中 children 發生變更導致出錯
var list = this.children.slice();
for (var i=0,l=list.length; i<l; i++) {
var child = list[i];
if (!child.isVisible()) { continue; }
// draw the child:
ctx.save();
child.updateContext(ctx);
child.draw(ctx);
ctx.restore();
}
return true;
};
child 就為一個 Bitmap 物件
直接看 Bitmap 類實現的 draw 方法如下:
// Bitmap 類 原始碼 142-159
p.draw = function(ctx, ignoreCache) {
if (this.DisplayObject_draw(ctx, ignoreCache)) { return true; }
var img = this.image, rect = this.sourceRect;
if (img.getImage) { img = img.getImage(); }
if (!img) { return true; }
if (rect) {
// some browsers choke on out of bound values, so we'll fix them:
var x1 = rect.x, y1 = rect.y, x2 = x1 + rect.width, y2 = y1 + rect.height, x = 0, y = 0, w = img.width, h = img.height;
if (x1 < 0) { x -= x1; x1 = 0; }
if (x2 > w) { x2 = w; }
if (y1 < 0) { y -= y1; y1 = 0; }
if (y2 > h) { y2 = h; }
ctx.drawImage(img, x1, y1, x2-x1, y2-y1, x, y, x2-x1, y2-y1);
} else {
ctx.drawImage(img, 0, 0);
}
return true;
};
就三步:
有快取則繪製快取
如果有 rect 限制,有目標尺寸 rect 限制,則繪製成 rect 尺寸,呼叫 canvas 的 drawImage 原生方法並傳入目標尺寸
如果沒有 rect 限制,則直接呼叫 canvas 的 drawImage 原生方法
先不管 ctx.drawImage(img, x1, y1, x2-x1, y2-y1, x, y, x2-x1, y2-y1);
這一句,
具體語法可以查詢 https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/drawImage
注意: ctx.drawImage(img, 0, 0);
後兩個引數值是圖象的 x, y 座標,
都傳了 0 好傢伙直接 "hardcode" 了,繪製時不用考慮影象的位置嗎?
都畫在了 0, 0 位置畫在左上角?
這不科學,如果使用者指定了影象位置比如 x = 100, y = 80 那怎麼辦?
如果我來實現,直覺上就會想要把此處改為 ctx.drawImage(img, 0 + x, 0 + y);
但 EaselJS 並沒有,但卻又能正常工作?,先擱置,繼續往下看就會明白
繪製文字
建立一個文字 txt
txt = new createjs.Text("text on the canvas... 0!", "36px Arial", "#FFF");
直接看向 Text 的 draw 實體方法:
// Text 類 原始碼 208 - 217 行
p.draw = function(ctx, ignoreCache) {
if (this.DisplayObject_draw(ctx, ignoreCache)) { return true; }
var col = this.color || "#000";
if (this.outline) { ctx.strokeStyle = col; ctx.lineWidth = this.outline*1; }
else { ctx.fillStyle = col; }
this._drawText(this._prepContext(ctx));
return true;
};
依然先判斷快取
文字預設為黑色
如果有 outline 則 lineWidth 被設定限制寬度,用顯示文字周邊的框
呼叫 this._drawText(this._prepContext(ctx));
_prepContext
存著的上下文中預設的預設樣式
Text 的 _drawText
方法是真正執行繪製文字的的邏輯(支援換行)
// Text 類 原始碼 339 - 390 行
p._drawText = function(ctx, o, lines) {
var paint = !!ctx;
if (!paint) {
ctx = Text._workingContext;
ctx.save();
this._prepContext(ctx);
}
var lineHeight = this.lineHeight||this.getMeasuredLineHeight();
var maxW = 0, count = 0;
var hardLines = String(this.text).split(/(?:\r\n|\r|\n)/);
for (var i=0, l=hardLines.length; i<l; i++) {
var str = hardLines[i];
var w = null;
if (this.lineWidth != null && (w = ctx.measureText(str).width) > this.lineWidth) {
// text wrapping:
var words = str.split(/(\s)/);
str = words[0];
w = ctx.measureText(str).width;
for (var j=1, jl=words.length; j<jl; j+=2) {
// Line needs to wrap:
var wordW = ctx.measureText(words[j] + words[j+1]).width;
if (w + wordW > this.lineWidth) {
if (paint) { this._drawTextLine(ctx, str, count*lineHeight); }
if (lines) { lines.push(str); }
if (w > maxW) { maxW = w; }
str = words[j+1];
w = ctx.measureText(str).width;
count++;
} else {
str += words[j] + words[j+1];
w += wordW;
}
}
}
if (paint) { this._drawTextLine(ctx, str, count*lineHeight); }
if (lines) { lines.push(str); }
if (o && w == null) { w = ctx.measureText(str).width; }
if (w > maxW) { maxW = w; }
count++;
}
if (o) {
o.width = maxW;
o.height = count*lineHeight;
}
if (!paint) { ctx.restore(); }
return o;
};
步驟:
paint
為 false 即沒有傳 ctx 時 僅用於測量文字的尺寸,並不實際繪製到舞臺上
通過 String(this.text).split(/(?:\r\n|\r|\n)/);
這一句將通過回車與換行符得到多行文字
迴圈分解出的文字陣列,ctx.measureText 測量文字寬度後判斷是否大於 lineWidth 如果加上後面一斷文字大於,則需新啟一行
呼叫 _drawTextLine()
方法繪製文字
呼叫 canvas 真實 api ctx.fillText 繪製文字
// Text 類 原始碼 399 - 403 行
p._drawTextLine = function(ctx, text, y) {
// Chrome 17 will fail to draw the text if the last param is included but null, so we feed it a large value instead:
if (this.outline) { ctx.strokeText(text, 0, y, this.maxWidth||0xFFFF); }
else { ctx.fillText(text, 0, y, this.maxWidth||0xFFFF); }
};
發現沒有 ctx.fillText 處傳的 x 還是 hardcode 寫死 0 而座標 y 還是相對座標,都繪製到 canvas 上了還不是絕對座標能行嗎?
不科學啊
是時候探究了!
draw 方法內使用的座標都是寫死或相對座標,但又可以如期繪製正確的絕對座標位置
是時候看一下之前遺留的 updateContext 方法了
還記得第一篇中 stage.update 內 draw 方法前的一句 this.updateContext(ctx);
嗎
實際上最終呼叫的是 DisplayObject 類的 updateContext 實體方法如下:
// DisplayObject.js 原始碼 787-810 行
p.updateContext = function(ctx) {
var o=this, mask=o.mask, mtx= o._props.matrix;
if (mask && mask.graphics && !mask.graphics.isEmpty()) {
mask.getMatrix(mtx);
ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, mtx.tx, mtx.ty);
mask.graphics.drawAsPath(ctx);
ctx.clip();
mtx.invert();
ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, mtx.tx, mtx.ty);
}
this.getMatrix(mtx);
var tx = mtx.tx, ty = mtx.ty;
if (DisplayObject._snapToPixelEnabled && o.snapToPixel) {
tx = tx + (tx < 0 ? -0.5 : 0.5) | 0;
ty = ty + (ty < 0 ? -0.5 : 0.5) | 0;
}
ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, tx, ty);
ctx.globalAlpha *= o.alpha;
if (o.compositeOperation) { ctx.globalCompositeOperation = o.compositeOperation; }
if (o.shadow) { this._applyShadow(ctx, o.shadow); }
};
o._props
是 src/easeljs/geom/DisplayProps.js
DisplayProps 類的範例
DisplayProps 主要負責了顯示物件的以下屬性操作
visible、alpha、shadow、compositeOperation、matrix
mtx= o._props.matrix
在 DisplayObject 範例屬性 _props
物件中得到 matrix
updateContext 就是在上下文中應用不同的 matrix 實現上下文中的「變幻」
首先就是對 mask 遮罩的處理,遮罩是通過繪製 Graphics 後對上下文進行 ctx.clip 實現的
如果存在 mask 當前顯示物件有遮罩,通過 mask.getMatrix 把遮罩的 matrix
ctx.transform 將上下文變化至遮罩所在的「狀態」
繪製遮罩 mask.graphics.drawAsPath(ctx)
還原矩陣 mtx.invert(); 回到當前顯示物件的「狀態」
至此 mask 部分處理完畢
回到當前顯示物件的 getMatrix 獲取矩陣後應用矩陣變化
getMatrix 做了兩件事
// DisplayObject.js 原始碼 1020-1024 行
p.getMatrix = function(matrix) {
var o = this, mtx = matrix || new createjs.Matrix2D();
return o.transformMatrix ? mtx.copy(o.transformMatrix) :
(mtx.identity() && mtx.appendTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation, o.skewX, o.skewY, o.regX, o.regY));
};
注意這一句 mtx.appendTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation, o.skewX, o.skewY, o.regX, o.regY))
就是將當前顯示物件的變幻屬性合到矩陣中
得到新的 matrix 後呼叫 ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, tx, ty); 實現一系列變化
至於 src/easeljs/geom/Matrix2D.js
矩陣類
平時在 css 中的使用的變化 scale, rotate, translateX, translateY 最後都是矩陣變幻實現的
矩陣變幻的好處一次可以實現多種變化,只是不那麼直觀
至於矩陣為什麼可以實現變幻,我這小學數學水平可講不清楚,推薦 3blue1brown 的視訊看完肯定會醍醐灌頂
我的總結是矩陣實現的是對座標軸的線性變幻,直接將座標軸原點變幻到繪製點!!
所以在具體 draw 繪製時 x,y 座標可以寫死或使用相對座標,因為 draw 之前已經使用矩陣把整體座標軸變幻到位了
繪製完後又會重置回來開始新的物件的變幻
Shape 類程式碼非常少,實現繪製的是 Graphics 類
Shape 只是作為 Graphics 範例的載體
使用 shape.graphics 屬性即可存取
Shape.js 原始碼 106-110 行
p.draw = function(ctx, ignoreCache) {
if (this.DisplayObject_draw(ctx, ignoreCache)) { return true; }
this.graphics.draw(ctx, this);
return true;
};
向量圖形類 Graphics 在 src/easeljs/display/Graphics.js
通常 Graphics 用於繪製向量圖形
可單獨使用,也可以在 Shape 範例內呼叫
var g = new createjs.Graphics();
g.setStrokeStyle(1);
g.beginStroke("#000000");
g.beginFill("red");
g.drawCircle(0,0,30);
要實現 Grapihcs 繪製,就得組合一系列繪圖命令
一系列繪製命令被儲存在了 _instructions
陣列屬性內
這些命令被稱為 Command Objects 命令物件
原始碼 1653 行 - 2459 行都是命令物件
命令物件分別都暴露了一個 exec 方法
比如 MoveTo 命令
// Graphics.js 原始碼 1700 - 1702 行
(G.MoveTo = function(x, y) {
this.x = x; this.y = y;
}).prototype.exec = function(ctx) { ctx.moveTo(this.x, this.y); };
比如圓形繪製命令
// Graphics.js 原始碼 2292 - 2295 行
(G.Circle = function(x, y, radius) {
this.x = x; this.y = y;
this.radius = radius;
}).prototype.exec = function(ctx) { ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); };
下面是圓角矩形的也是在 Graphics 靜態方法
(G.RoundRect = function(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL) {
this.x = x; this.y = y;
this.w = w; this.h = h;
this.radiusTL = radiusTL; this.radiusTR = radiusTR;
this.radiusBR = radiusBR; this.radiusBL = radiusBL;
}).prototype.exec = function(ctx) {
var max = (this.w<this.h?this.w:this.h)/2;
var mTL=0, mTR=0, mBR=0, mBL=0;
var x = this.x, y = this.y, w = this.w, h = this.h;
var rTL = this.radiusTL, rTR = this.radiusTR, rBR = this.radiusBR, rBL = this.radiusBL;
if (rTL < 0) { rTL *= (mTL=-1); }
if (rTL > max) { rTL = max; }
if (rTR < 0) { rTR *= (mTR=-1); }
if (rTR > max) { rTR = max; }
if (rBR < 0) { rBR *= (mBR=-1); }
if (rBR > max) { rBR = max; }
if (rBL < 0) { rBL *= (mBL=-1); }
if (rBL > max) { rBL = max; }
ctx.moveTo(x+w-rTR, y);
ctx.arcTo(x+w+rTR*mTR, y-rTR*mTR, x+w, y+rTR, rTR);
ctx.lineTo(x+w, y+h-rBR);
ctx.arcTo(x+w+rBR*mBR, y+h+rBR*mBR, x+w-rBR, y+h, rBR);
ctx.lineTo(x+rBL, y+h);
ctx.arcTo(x-rBL*mBL, y+h+rBL*mBL, x, y+h-rBL, rBL);
ctx.lineTo(x, y+rTL);
ctx.arcTo(x-rTL*mTL, y-rTL*mTL, x+rTL, y, rTL);
ctx.closePath();
};
exec 方法才是真正呼叫 canvas context 繪製的地方
G 就是 Graphics 的簡寫,在 250 行 var G = Graphics;
這些單獨的繪圖命令其實就是 G 的一些靜態方法,只是這些靜態方法又擁有各自不同的 exec 實體方法實現具體的繪圖
而 Graphics 的實體方法又會將繪圖命令 append 一個 「靜態方法的範例」 儲存陣列內
比如 lineTo , 注意是 new G.MoveTo(x,y) 一個命令
// Graphics.js 原始碼 469 - 471 行
p.moveTo = function(x, y) {
return this.append(new G.MoveTo(x,y), true);
};
下面是 append 原始碼,命令都存在 _activeInstructions
陣列內
// Graphics.js 原始碼 1024 - 1029 行
p.append = function(command, clean) {
this._activeInstructions.push(command);
this.command = command;
if (!clean) { this._dirty = true; }
return this;
};
再看通用的 draw 方法
// Graphics.js 原始碼 434-440 行
p.draw = function(ctx, data) {
this._updateInstructions();
var instr = this._instructions;
for (var i=this._storeIndex, l=instr.length; i<l; i++) {
instr[i].exec(ctx, data);
}
};
draw 內的主要邏輯就是用迴圈呼叫 _instructions
儲存的「命令物件」執行命令物件的 exec 方法
_instructions
的命令是通過 _updateInstructions
方法從 _activeInstructions
陣列內複製的
// Graphics.js 原始碼 1593-1627 行
p._updateInstructions = function(commit) {
var instr = this._instructions, active = this._activeInstructions, commitIndex = this._commitIndex;
debugger
if (this._dirty && active.length) {
instr.length = commitIndex; // remove old, uncommitted commands
instr.push(Graphics.beginCmd);
var l = active.length, ll = instr.length;
instr.length = ll+l;
for (var i=0; i<l; i++) { instr[i+ll] = active[i]; }
if (this._fill) { instr.push(this._fill); }
if (this._stroke) {
// doesn't need to be re-applied if it hasn't changed.
if (this._strokeDash !== this._oldStrokeDash) {
instr.push(this._strokeDash);
}
if (this._strokeStyle !== this._oldStrokeStyle) {
instr.push(this._strokeStyle);
}
if (commit) {
this._oldStrokeStyle = this._strokeStyle;
this._oldStrokeDash = this._strokeDash;
}
instr.push(this._stroke);
}
this._dirty = false;
}
// 如果 commit 了,把 _activeInstructions 當前命令集合清空,且遊標指向 this._instructions 的最後位置
if (commit) {
active.length = 0;
this._commitIndex = instr.length;
}
};
還有一些方法(如:beginStroke beginFill) 內呼叫了 _updateInstructions(true)
注意傳的是 true
比如:
p.beginStroke = function(color) {
return this._setStroke(color ? new G.Stroke(color) : null);
};
p._setStroke = function(stroke) {
this._updateInstructions(true);
if (this.command = this._stroke = stroke) {
stroke.ignoreScale = this._strokeIgnoreScale;
}
return this;
};
注意: 這裡收集的命令暫時不會被放到 this._instructions
陣列內
直到有 append 方法執行過 dirty 為 true 了 才會把 stroke 命令新增到 this._instructions
陣列
因為沒有 append 任何實質的內容(圓,線,矩形等),則不需要執行 stroke ,beginFill 等命令,因為無意義
p._updateInstructions
到底幹了啥?
主要是在 draw 之前不斷收集命令, 在多處都有呼叫 _updateInstructions
這些操作 commit 均為 true 表明後面的繪製是新的開始,將之前的一系列繪製命令歸為一個路徑繪製,下一個得新啟一個路徑繪製
使用 this._commitIndex
遊標重新指示命令陣列內的位置
debugger 偵錯一下看看
在 easeljs-NEXT.js 的 5226 行加上 debugger
瀏覽器中開啟 examples/Graphics_simple.html 檔案,並開啟瀏覽器偵錯工具
Graphics_simple.html 檔案的 javascript 程式碼內有一個 drawSmiley 方法
function drawSmiley() {
var s = new createjs.Shape();
var g = s.graphics;
//Head
g.setStrokeStyle(10, 'round', 'round');
g.beginStroke("#000");
g.beginFill("#FC0");
g.drawCircle(0, 0, 100); //55,53
//Mouth
g.beginFill(); // no fill
g.arc(0, 0, 60, 0, Math.PI);
//Right eye
g.beginStroke(); // no stroke
g.beginFill("#000");
g.drawCircle(-30, -30, 15);
//Left eye
g.drawCircle(30, -30, 15);
return s;
}
很明顯通過 debugger 先呼叫了 setStrokeStyle
當呼叫 setStrokeStyle、beginStroke、beginFill 等都會執行 _updateInstructions
命令
當執行到 g.drawCircle(0, 0, 100);
此時命令才會被一起收集順序如下
發現沒有,與我們在呼叫順序不一樣
g.setStrokeStyle(10, 'round', 'round');
g.beginStroke("#000");
g.beginFill("#FC0");
g.drawCircle(0, 0, 100); //55,53
BeginPath -> Circle -> Fill -> StrokeStyle
這是 Canvas 真正正常執行的順序
BeginPath 也在每次 dirty (append 方法導至 dirty 為 true) 時新增,beginPath 當然是另啟一個新的路徑繪製了
這一篇分析的是,最常用的三個顯示物件
下一篇分析另三個稍顯高階的顯示物件
部落格園: http://cnblogs.com/willian/
github: https://github.com/willian12345/