Correctly handle groups as clipping masks.

Closes #370.
This commit is contained in:
Jürg Lehni 2014-03-17 14:51:47 +01:00
parent 0fe52a7d39
commit 438de7013a
6 changed files with 64 additions and 44 deletions

View file

@ -106,18 +106,22 @@ var Group = Item.extend(/** @lends Group# */{
}, },
_getClipItem: function() { _getClipItem: function() {
// Allow us to set _clipItem to null when none is found and still return // NOTE: _clipItem is the child that has _clipMask set to true.
// it as a defined value without searching again var clipItem = this._clipItem;
if (this._clipItem !== undefined) // Distinguish null (no clipItem set) and undefined (clipItem was not
return this._clipItem; // looked for yet).
for (var i = 0, l = this._children.length; i < l; i++) { if (clipItem === undefined) {
var child = this._children[i]; clipItem = null;
if (child._clipMask) for (var i = 0, l = this._children.length; i < l; i++) {
return this._clipItem = child; 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 return clipItem;
// nex time.
return this._clipItem = null;
}, },
/** /**
@ -164,14 +168,33 @@ var Group = Item.extend(/** @lends Group# */{
}, },
_draw: function(ctx, param) { _draw: function(ctx, param) {
var clipItem = param.clipItem = this._getClipItem(); var clip = param.clip,
if (clipItem) 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 })); 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;
}
} }
}); });

View file

@ -238,9 +238,11 @@ var Item = Base.extend(Callback, /** @lends Item# */{
cacheParent = this._parent || symbol, cacheParent = this._parent || symbol,
project = this._project; project = this._project;
if (flags & /*#=*/ ChangeFlag.GEOMETRY) { 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._bounds = this._position = this._decomposed =
this._globalMatrix = undefined; this._globalMatrix = this._currentPath = undefined;
} }
if (cacheParent && (flags if (cacheParent && (flags
& (/*#=*/ ChangeFlag.GEOMETRY | /*#=*/ ChangeFlag.STROKE))) { & (/*#=*/ 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 // Determine if we can draw directly, or if we need to draw into a
// separate canvas and then composite onto the main canvas. // separate canvas and then composite onto the main canvas.
direct = normalBlend && opacity === 1 direct = normalBlend && opacity === 1
|| param.clip
// If native blending is possible, see if the item allows it // If native blending is possible, see if the item allows it
|| (nativeBlend || normalBlend && opacity < 1) || (nativeBlend || normalBlend && opacity < 1)
&& this._canComposite(), && this._canComposite(),
@ -3554,7 +3557,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{
ctx.restore(); ctx.restore();
if (trackTransforms) if (trackTransforms)
transforms.pop(); transforms.pop();
if (param.clip) if (param.clip && !param.dontFinish)
ctx.clip(); ctx.clip();
// If a temporary canvas was created, composite it onto the main canvas: // If a temporary canvas was created, composite it onto the main canvas:
if (!direct) { if (!direct) {

View file

@ -169,11 +169,12 @@ var Shape = Item.extend(/** @lends Shape# */{
var style = this._style, var style = this._style,
hasFill = style.hasFill(), hasFill = style.hasFill(),
hasStroke = style.hasStroke(), hasStroke = style.hasStroke(),
clip = param.clip; dontPaint = param.dontFinish || param.clip;
if (hasFill || hasStroke || clip) { if (hasFill || hasStroke || dontPaint) {
var radius = this._radius, var radius = this._radius,
shape = this._shape; shape = this._shape;
ctx.beginPath(); if (!param.dontStart)
ctx.beginPath();
if (shape === 'circle') { if (shape === 'circle') {
ctx.arc(0, 0, radius, 0, Math.PI * 2, true); ctx.arc(0, 0, radius, 0, Math.PI * 2, true);
} else { } else {
@ -217,7 +218,7 @@ var Shape = Item.extend(/** @lends Shape# */{
} }
ctx.closePath(); ctx.closePath();
} }
if (!clip && (hasFill || hasStroke)) { if (!dontPaint && (hasFill || hasStroke)) {
this._setStyles(ctx); this._setStyles(ctx);
if (hasFill) { if (hasFill) {
ctx.fill(style.getWindingRule()); ctx.fill(style.getWindingRule());

View file

@ -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) { insertChildren: function insertChildren(index, items, _preserve) {
// Pass on 'path' for _type, to make sure that only paths are added as // Pass on 'path' for _type, to make sure that only paths are added as
// children. // children.
@ -254,8 +247,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
if (this._currentPath) { if (this._currentPath) {
ctx.currentPath = this._currentPath; ctx.currentPath = this._currentPath;
} else { } else {
param = param.extend({ dontStart: true, dontFinish: true });
ctx.beginPath(); ctx.beginPath();
param = param.extend({ compound: true });
for (var i = 0, l = children.length; i < l; i++) for (var i = 0, l = children.length; i < l; i++)
children[i].draw(ctx, param); children[i].draw(ctx, param);
this._currentPath = ctx.currentPath; this._currentPath = ctx.currentPath;

View file

@ -2008,10 +2008,9 @@ var Path = PathItem.extend(/** @lends Path# */{
return { return {
_draw: function(ctx, param) { _draw: function(ctx, param) {
var clip = param.clip, var dontStart = param.dontStart,
// Also mark this Path as _compound so _changed() knows about it dontPaint = param.dontFinish || param.clip;
compound = this._compound = param.compound; if (!dontStart)
if (!compound)
ctx.beginPath(); ctx.beginPath();
var style = this.getStyle(), var style = this.getStyle(),
@ -2028,20 +2027,20 @@ var Path = PathItem.extend(/** @lends Path# */{
return dashArray[((i % dashLength) + dashLength) % dashLength]; return dashArray[((i % dashLength) + dashLength) % dashLength];
} }
if (this._currentPath) { if (!dontStart && this._currentPath) {
ctx.currentPath = 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 // Prepare the canvas path if we have any situation that
// requires it to be defined. // requires it to be defined.
drawSegments(ctx, this); drawSegments(ctx, this);
if (this._closed) if (this._closed)
ctx.closePath(); ctx.closePath();
// CompoundPath collects its own _currentPath // CompoundPath collects its own _currentPath
if (!compound) if (!dontStart)
this._currentPath = ctx.currentPath; 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 // If the path is part of a compound path or doesn't have a fill
// or stroke, there is no need to continue. // or stroke, there is no need to continue.
this._setStyles(ctx); this._setStyles(ctx);
@ -2059,7 +2058,8 @@ var Path = PathItem.extend(/** @lends Path# */{
// NOTE: We don't cache this path in another currentPath // NOTE: We don't cache this path in another currentPath
// since browsers that support currentPath also support // since browsers that support currentPath also support
// native dashes. // native dashes.
ctx.beginPath(); if (!dontStart)
ctx.beginPath();
var flattener = new PathFlattener(this), var flattener = new PathFlattener(this),
length = flattener.length, length = flattener.length,
from = -style.getDashOffset(), to, from = -style.getDashOffset(), to,

View file

@ -309,8 +309,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{
/*#*/ if (__options.nativeContains || !__options.booleanOperations) { /*#*/ if (__options.nativeContains || !__options.booleanOperations) {
// To compare with native canvas approach: // To compare with native canvas approach:
var ctx = CanvasProvider.getContext(1, 1); var ctx = CanvasProvider.getContext(1, 1);
// Abuse clip = true to get a shape for ctx.isPointInPath(). // Use dontFinish to tell _draw to only produce geometries for hit-test.
this._draw(ctx, new Base({ clip: true })); this._draw(ctx, new Base({ dontFinish: true }));
var res = ctx.isPointInPath(point.x, point.y, this.getWindingRule()); var res = ctx.isPointInPath(point.x, point.y, this.getWindingRule());
CanvasProvider.release(ctx); CanvasProvider.release(ctx);
return res; return res;