剖析 createjs.Graphics

在長期使用 createjs 的過程當中,我一直有這樣一個經驗:「beginFill 必須在 drawXXX 以前調用,不然 beginFill 會被忽略(是的不報錯)」。
可是爲何會這樣,其實並無去深究它。今天很想知道 Graphics 是怎麼工做的。html

緣由

createjs.Graphics上的繪製圖形API最終都會轉換成原生canvas語句。因此先看一下原生 canvas 的表現。canvas

代碼一
var ctx = canvas.getContext("2d");數組

ctx.rect(0, 0, 100, 100);
ctx.fillStyle = "#ff0000"; 
ctx.fill();

最終的樣子是:
圖示app

代碼二
var ctx = canvas.getContext("2d");函數

ctx.fillStyle = "#ff0000"; 
ctx.fill();
ctx.rect(0, 0, 100, 100);

最終的樣子是:
圖示ui

爲何代碼二看不到紅色的矩形?
在 photoshop 上畫一個既沒有填充顏色又沒有描邊顏色的圖形,畫完以後這個圖形是看不到的,這個道理也一樣適用於 canvas。fill 這個API就是用於填充顏色。this

對於 canvas 來講,要先繪製圖形再進行填充(fill)或描邊(stroke),圖形最終纔會被渲染到畫布上。若是先填充或描邊後再繪製圖形,那麼圖形不會被渲染。spa

打個比喻:prototype

先挖坑再倒水 == 一坑水;
先倒水再挖坑 == 一個坑code

原生 canvas 的填充或描邊方法共有4個,以下:

  • fill

  • stroke

  • fillRect

  • strokeRect

分析 createjs.Graphics 的源碼

createjs.Graphics 的源碼地址:http://www.createjs.com/docs/...

我在以前的描述中說過「全部的Graphics上的繪製圖形API最終都會轉換成原生canvas語句」,看 createjs 的源碼也確實如此。因爲 API 太多,只能先從 Graphics.prototype.drawRect 切入。

首先研究的對源碼實際上是 Graphics 構造函數,以下圖:
圖示

其次, G 表示 Graphics 構造函數自己,p 表明 Graphics.prototype,以下:
圖示

第三,Graphics.prototype.drawRect 指向了 Graphics.prototype.rect,以下:
圖示

第四,Graphics.prototype.rect直接返回了 this.append 並同時調用了 G.Rect 方法,以下 :
圖示

第五,先看一下 G.Rect 作了什麼,以下:
圖示

這裏出現了 exec 不知道是作什麼的,不過能夠看到 execdrawRect 轉換成原生的 canvas 代碼了!!!

第六,回頭看 Graphics.prototype.append ,以下:
圖示

這裏獲得的信息就是把 new G.Rect(x, y, w, h) push 到數組 _activeInstructions中。彷佛沒有我想要的東西,不過,我往上看它的註釋以下:

// TODO: deprecated.
/**
 * Removed in favour of using custom command objects with {{#crossLink "Graphics/append"}}{{/crossLink}}.
 * @method inject
 * @deprecated
 **/

/**
 * Appends a graphics command object to the graphics queue. Command objects expose an "exec" method
 * that accepts two parameters: the Context2D to operate on, and an arbitrary data object passed into
 * {{#crossLink "Graphics/draw"}}{{/crossLink}}. The latter will usually be the Shape instance that called draw.
 *
 * This method is used internally by Graphics methods, such as drawCircle, but can also be used directly to insert
 * built-in or custom graphics commands. For example:
 *
 *         // attach data to our shape, so we can access it during the draw:
 *         myShape.color = "red";
 *
 *         // append a Circle command object:
 *         myShape.graphics.append(new createjs.Graphics.Circle(50, 50, 30));
 *
 *         // append a custom command object with an exec method that sets the fill style
 *         // based on the shape's data, and then fills the circle.
 *         myShape.graphics.append({exec:function(ctx, shape) {
 *             ctx.fillStyle = shape.color;
 *             ctx.fill();
 *         }});
 *
 * @method append
 * @param {Object} command A graphics command object exposing an "exec" method.
 * @param {boolean} clean The clean param is primarily for internal use. A value of true indicates that a command does not generate a path that should be stroked or filled.
 * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.)
 * @chainable
 **/

這裏的信息過重要了:p.append 的做用是把「命令對象(command object)」推到「圖像隊列(graphics queue)」中。當「圖形實例(Shape instance)」調用 draw 方法時,會從「圖像隊列(graphics queue)」取出「命令對象」,並把繪製出這個實例的樣子。

第七步,查閱 p.draw,以下:
圖示

這裏一目瞭然,在執行 draw 時是對數組 _instructions 作出隊列操做。可是,第六步提到的數組是_activeInstructions,那麼 _instructions_activeInstructions 是什麼關係呢?上圖有一個叫 this._updateInstructions() 或許能夠給我答案。

第八步,查閱 _updateInstructions 方法:
圖示

從上圖代碼可知:_activeInstructions_instructions 的一部分。再深刻分析能夠看到上圖代碼接下來的 this._fillthis._stroke

在我看來 createjs 把Graphics 的方法分紅兩類三種。

第一類 繪製圖形 繪製方法;如: rect/moveTo/lineTo 等
第二類 渲染圖形 填充方法(fill);
描邊方法(stroke);

當前分析的 drawRect 就是第一類。須要分析第二類。

第九步,分析 beginFill
圖示

能夠發現,beginFill 不調用 this.append 而是 this._setFill 了。

第十步,查閱 p._setFill,以下:
圖示

第二類方法直接就調用了 this._updateInstructions(true) 了,並且第二類方法生成的命令也再也不是存入 _activeInstructions數組中了(其實 _activeInstructions 數組就是第一類方法生成的命令的數組)。

1615 行的 this.command = this._fill = fill,其實很重要。回頭看第八步的 _updateInstructions ,第二類方法內部調用 _updateInstructions 並傳入 boolean 值true,它的做用是清空 _activeInstructions 數組(見 1602 行)。
分析「1577行~1606行」的代碼能夠知道這20行的代碼的做用是把第二類方法生成的命令追加到 _instructions 數組上。這裏有一個邏輯陷井:把當前的第二命令追加到 instructions 數組上。

爲何是個陷井呢?
回頭看 1614~1615 行,this._fill 在 調用 _updateInstructions 後被賦值。這意味着第二類方法生成的命令會在下次調用 _updateInstructions 是被追加到 _instructions 數組上。

哪些操做會調用到 _updateInstructions?
第二類方法 與 p.draw。

這意味着:第二類方法生成的命令位於隊列的位置是下一個第二類方法所在的鏈式位置(若是隻有一個第二類方法則在鏈式最後)
可是上面的結論並不能解決本文本拋出的知識點:「beginFill必須放在 drawXXX 以前,不然beginFill會被忽略」。

回到1577行的判斷語句:if (this._dirty && active.length) 。上面其實提到了第二類方法調用 _updateInstructions 方法後會把 _activeInstructions 數組清空( 即active.length === 0)。另外 p.draw 不會清空 _activeInstructions 數組,卻會把 this.dirty 置爲 false(見行:1599)。
這意味着:Graphics的鏈式末尾都是第二類方法,那麼這些方法生成的命令不會被追加到 _instructions 數組上(即不會被執行)。以下:
var rect = new createjs.Shape();
rect.graphics.drawRect(0, 0, 100, 100).beginFill("#ff0000").setStrokeStyle("#000000").beginStroke(4);
stage.addChild(rect);
上面的代碼執行後是空白。

PS:第二類命令全部的方法 ---- beginFill, beginStroke, setStrokeStyle, setStrokeDash。
功能上 beginFill 在形式上徹底同樣,因此只須要分析 beginFill 便可。以下:
圖示

多圖形實例

createjs.Graphics 是能夠建立一個多圖形實例的,以下:
var instance = new createjs.Shape();
instance.graphics

.beginFill("#ff0000").drawRect(0, 0, 100, 100) // 矩形
.beginFill("#ffff00").drawCircle(150, 150, 50) // 圓形

圖示

其實我想象中的樣子是一個 Shape 實例只能建立一個圖形,但事實是一個 Shape 實例是能夠建立多個圖形的。從原生 canvas 說一下多圖形是怎麼繪製的:

var ctx = canvas.getContext("2d");

ctx.beginPath(); 
ctx.rect(0, 0, 100, 100); 
ctx.fillStyle = "#ff0000"; 
ctx.fill(); 
ctx.closePath(); 
ctx.beginPath(); 
ctx.arc(150, 150, 50, 150, 100 * Math.PI * 2); 
ctx.fillStyle = "#ffff00"; 
ctx.fill(); 
ctx.closePath();

圖示

嚴格上說,原生 canvas 的一個圖形的繪製與渲染由 beginPath() 開始,再由 closePath() 結束。
實際上,beginPath() 表明上一個圖形的結束和下一個圖形的開始。

因此代碼能夠簡單爲:
var ctx = canvas.getContext("2d");

ctx.rect(0, 0, 100, 100); 
ctx.fillStyle = "#ff0000"; 
ctx.fill();
ctx.beginPath(); 
ctx.arc(150, 150, 50, 150, 100 * Math.PI * 2); 
ctx.fillStyle = "#ffff00"; 
ctx.fill();

若是矩形與圓形中間的 beginPath() 沒有了,會怎麼樣?
var ctx = canvas.getContext("2d");

ctx.rect(0, 0, 100, 100); 
ctx.fillStyle = "#ff0000"; 
ctx.fill(); 
ctx.arc(150, 150, 50, 150, 100 * Math.PI * 2); 
ctx.fillStyle = "#ffff00"; 
ctx.fill();

圖示

這種狀況,矩形和圓形同屬於一個圖形,因此 fill 填充取的是最後一次的顏色。

回頭看 createjs.Graphicsp._updateInstructions
圖示

圖示

圖示

很容易得出另外一個結論:第二類方法會在其鏈式上所在位置插入beginPath的命令以標記上一個圖形結束和下一個圖形開始
若是以一個圖形爲研究對象不可貴出Graphics 繪製渲染一個圖形的語式:第二類方法().[.第二類方法()...].第一類方法()[.第一類方法()...]

上面的語式能夠簡單地寫成: 第二類方法組.第一類方法組
而後若是把方法轉化爲對應的原生命令,那麼這些命令的執行順序是:第一類方法生成的命令 -> 第二類方法生成的命令。正好與語式左右互換。

總結

本文對 Graphics 源碼解析後,給出的結論以下:

  • 第二類方法生成的命令位於隊列的位置是下一個第二類方法所在的鏈式位置(若是隻有一個第二類方法則在鏈式最後)

  • Graphics的鏈式末尾都是第二類方法,那麼這些方法生成的命令不會被追加到 _instructions 數組上(即不會被執行)

  • 第二類方法在鏈式的位置標誌上一個圖形的結束和下一個圖形的開始

雖然有三個結論,不過不便被記憶。

更有價值的應該是繪製圖形的語式:「第二類方法組.第一類方法組
但論實用價值仍是開頭的那句話:beginFill 必須在 drawXXX 以前調用,不然 beginFill 會被忽略(是的不報錯)

相關文章
相關標籤/搜索