1

在长期使用 createjs 的过程中,我一直有这样一个经验:「beginFill 必须在 drawXXX 之前调用,否则 beginFill 会被忽略(是的不报错)」。
但是为什么会这样,其实并没有去深究它。今天很想知道 Graphics 是怎么工作的。

原因

createjs.Graphics上的绘制图形API最终都会转换成原生canvas语句。所以先看一下原生 canvas 的表现。

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

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

最终的样子是:
图示

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

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

最终的样子是:
图示

为什么代码二看不到红色的矩形?
在 photoshop 上画一个既没有填充颜色又没有描边颜色的图形,画完之后这个图形是看不到的,这个道理也同样适用于 canvas。fill 这个API就是用于填充颜色。

对于 canvas 来说,要先绘制图形再进行填充(fill)或描边(stroke),图形最终才会被渲染到画布上。如果先填充或描边后再绘制图形,那么图形不会被渲染。

打个比喻:

先挖坑再倒水 == 一坑水;
先倒水再挖坑 == 一个坑

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


leeenx
614 声望8 粉丝

JD.COM & 凹凸实验室