在 第一篇 中我們大致分析了從: 創建舞臺 -> 添加顯示對象-> 更新顯示對象 的源碼實現
這一篇將主要分析幾個常用顯示對象自各 draw 方法的實現
讓我們看向例子 examples/Text_simple.html
這個例子中使用了三個顯示對象類 Bitmap 、Text 、 Shape
Bitmap draw
以下例子中添加了一個 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 並沒有,但卻又能正常工作?,先擱置,繼續往下看就會明白
Text draw
繪製文本
創建一個文本 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 上了還不是絕對座標能行嗎?
不科學啊
是時候探究了!
updateContext
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 做了兩件事
- 如果顯示對象有明確指定的 matrix 則應用 matrix
- 如果沒有明確指定,則將顯示對象的,x,y,scaleX, scaleY, rotation, skewW, skewY, regX, regY 合到 matrix上
// 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 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
矢量圖形類 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/