From 438de7013a6dc2102bcc73d519dee2615c69417a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 17 Mar 2014 14:51:47 +0100 Subject: [PATCH] Correctly handle groups as clipping masks. Closes #370. --- src/item/Group.js | 59 ++++++++++++++++++++++++++++------------ src/item/Item.js | 9 ++++-- src/item/Shape.js | 9 +++--- src/path/CompoundPath.js | 9 +----- src/path/Path.js | 18 ++++++------ src/path/PathItem.js | 4 +-- 6 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/item/Group.js b/src/item/Group.js index 695d218c..19eeb337 100644 --- a/src/item/Group.js +++ b/src/item/Group.js @@ -106,18 +106,22 @@ var Group = Item.extend(/** @lends Group# */{ }, _getClipItem: function() { - // Allow us to set _clipItem to null when none is found and still return - // it as a defined value without searching again - if (this._clipItem !== undefined) - return this._clipItem; - for (var i = 0, l = this._children.length; i < l; i++) { - var child = this._children[i]; - if (child._clipMask) - return this._clipItem = child; + // NOTE: _clipItem is the child that has _clipMask set to true. + var clipItem = this._clipItem; + // Distinguish null (no clipItem set) and undefined (clipItem was not + // looked for yet). + if (clipItem === undefined) { + clipItem = null; + for (var i = 0, l = this._children.length; i < l; i++) { + var child = this._children[i]; + if (child._clipMask) { + clipItem = child; + break; + } + } + this._clipItem = clipItem; } - // Make sure we're setting _clipItem to null so it won't be searched for - // nex time. - return this._clipItem = null; + return clipItem; }, /** @@ -164,14 +168,33 @@ var Group = Item.extend(/** @lends Group# */{ }, _draw: function(ctx, param) { - var clipItem = param.clipItem = this._getClipItem(); - if (clipItem) + var clip = param.clip, + clipItem = !clip && this._getClipItem(), + draw = true; + param = param.extend({ clipItem: clipItem, clip: false }); + if (clip) { + // If told to clip with a group, we start our own path and draw each + // child just like in a compound-path. We also cache the resulting + // path in _currentPath. + if (this._currentPath) { + ctx.currentPath = this._currentPath; + draw = false; + } else { + ctx.beginPath(); + param.dontStart = param.dontFinish = true; + } + } else if (clipItem) { clipItem.draw(ctx, param.extend({ clip: true })); - for (var i = 0, l = this._children.length; i < l; i++) { - var item = this._children[i]; - if (item !== clipItem) - item.draw(ctx, param); } - param.clipItem = null; + if (draw) { + for (var i = 0, l = this._children.length; i < l; i++) { + var item = this._children[i]; + if (item !== clipItem) + item.draw(ctx, param); + } + } + if (clip) { + this._currentPath = ctx.currentPath; + } } }); diff --git a/src/item/Item.js b/src/item/Item.js index 1c172542..9de5223c 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -238,9 +238,11 @@ var Item = Base.extend(Callback, /** @lends Item# */{ cacheParent = this._parent || symbol, project = this._project; if (flags & /*#=*/ ChangeFlag.GEOMETRY) { - // Clear cached bounds and position whenever geometry changes + // Clear cached bounds, position and decomposed matrix whenever + // geometry changes. Also clear _currentPath since it can be used + // both on compound-paths and clipping groups. this._bounds = this._position = this._decomposed = - this._globalMatrix = undefined; + this._globalMatrix = this._currentPath = undefined; } if (cacheParent && (flags & (/*#=*/ ChangeFlag.GEOMETRY | /*#=*/ ChangeFlag.STROKE))) { @@ -3509,6 +3511,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ // Determine if we can draw directly, or if we need to draw into a // separate canvas and then composite onto the main canvas. direct = normalBlend && opacity === 1 + || param.clip // If native blending is possible, see if the item allows it || (nativeBlend || normalBlend && opacity < 1) && this._canComposite(), @@ -3554,7 +3557,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ ctx.restore(); if (trackTransforms) transforms.pop(); - if (param.clip) + if (param.clip && !param.dontFinish) ctx.clip(); // If a temporary canvas was created, composite it onto the main canvas: if (!direct) { diff --git a/src/item/Shape.js b/src/item/Shape.js index fa352958..8a888210 100644 --- a/src/item/Shape.js +++ b/src/item/Shape.js @@ -169,11 +169,12 @@ var Shape = Item.extend(/** @lends Shape# */{ var style = this._style, hasFill = style.hasFill(), hasStroke = style.hasStroke(), - clip = param.clip; - if (hasFill || hasStroke || clip) { + dontPaint = param.dontFinish || param.clip; + if (hasFill || hasStroke || dontPaint) { var radius = this._radius, shape = this._shape; - ctx.beginPath(); + if (!param.dontStart) + ctx.beginPath(); if (shape === 'circle') { ctx.arc(0, 0, radius, 0, Math.PI * 2, true); } else { @@ -217,7 +218,7 @@ var Shape = Item.extend(/** @lends Shape# */{ } ctx.closePath(); } - if (!clip && (hasFill || hasStroke)) { + if (!dontPaint && (hasFill || hasStroke)) { this._setStyles(ctx); if (hasFill) { ctx.fill(style.getWindingRule()); diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 26580d34..5fe48188 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -101,13 +101,6 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ } }, - _changed: function _changed(flags) { - _changed.base.call(this, flags); - // Clear cached native Path - if (flags & (/*#=*/ ChangeFlag.HIERARCHY | /*#=*/ ChangeFlag.GEOMETRY)) - this._currentPath = undefined; - }, - insertChildren: function insertChildren(index, items, _preserve) { // Pass on 'path' for _type, to make sure that only paths are added as // children. @@ -254,8 +247,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ if (this._currentPath) { ctx.currentPath = this._currentPath; } else { + param = param.extend({ dontStart: true, dontFinish: true }); ctx.beginPath(); - param = param.extend({ compound: true }); for (var i = 0, l = children.length; i < l; i++) children[i].draw(ctx, param); this._currentPath = ctx.currentPath; diff --git a/src/path/Path.js b/src/path/Path.js index 3382b1fa..2454380a 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2008,10 +2008,9 @@ var Path = PathItem.extend(/** @lends Path# */{ return { _draw: function(ctx, param) { - var clip = param.clip, - // Also mark this Path as _compound so _changed() knows about it - compound = this._compound = param.compound; - if (!compound) + var dontStart = param.dontStart, + dontPaint = param.dontFinish || param.clip; + if (!dontStart) ctx.beginPath(); var style = this.getStyle(), @@ -2028,20 +2027,20 @@ var Path = PathItem.extend(/** @lends Path# */{ return dashArray[((i % dashLength) + dashLength) % dashLength]; } - if (this._currentPath) { + if (!dontStart && this._currentPath) { ctx.currentPath = this._currentPath; - } else if (hasFill || hasStroke && !dashLength || compound || clip){ + } else if (hasFill || hasStroke && !dashLength || dontPaint) { // Prepare the canvas path if we have any situation that // requires it to be defined. drawSegments(ctx, this); if (this._closed) ctx.closePath(); // CompoundPath collects its own _currentPath - if (!compound) + if (!dontStart) this._currentPath = ctx.currentPath; } - if (!clip && !compound && (hasFill || hasStroke)) { + if (!dontPaint && (hasFill || hasStroke)) { // If the path is part of a compound path or doesn't have a fill // or stroke, there is no need to continue. this._setStyles(ctx); @@ -2059,7 +2058,8 @@ var Path = PathItem.extend(/** @lends Path# */{ // NOTE: We don't cache this path in another currentPath // since browsers that support currentPath also support // native dashes. - ctx.beginPath(); + if (!dontStart) + ctx.beginPath(); var flattener = new PathFlattener(this), length = flattener.length, from = -style.getDashOffset(), to, diff --git a/src/path/PathItem.js b/src/path/PathItem.js index f274247a..bb76d103 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -309,8 +309,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{ /*#*/ if (__options.nativeContains || !__options.booleanOperations) { // To compare with native canvas approach: var ctx = CanvasProvider.getContext(1, 1); - // Abuse clip = true to get a shape for ctx.isPointInPath(). - this._draw(ctx, new Base({ clip: true })); + // Use dontFinish to tell _draw to only produce geometries for hit-test. + this._draw(ctx, new Base({ dontFinish: true })); var res = ctx.isPointInPath(point.x, point.y, this.getWindingRule()); CanvasProvider.release(ctx); return res;