剖析 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 会被忽略(是的不报错)

相关文章
相关标签/搜索