mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-06-01 15:54:42 -04:00
Include new, improved point in path algorithm based on winding number.
It's also possible to switch to using the canvas's native isPointInPath() through options.nativeContains
This commit is contained in:
parent
eae526f38c
commit
4f27be8f12
7 changed files with 226 additions and 126 deletions
examples/Scripts
src
test/tests
|
@ -286,8 +286,19 @@
|
|||
strokeWidth: 1
|
||||
};
|
||||
|
||||
function convertToPath(item) {
|
||||
if (item instanceof Shape) {
|
||||
var path = item.toPath();
|
||||
item.remove();
|
||||
return path;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
// Better if path1 and path2 fit nicely inside a 200x200 pixels rect
|
||||
function testBooleanStatic(path1, path2) {
|
||||
path1 = convertToPath(path1);
|
||||
path2 = convertToPath(path2);
|
||||
path1.style = path2.style = pathStyleNormal;
|
||||
|
||||
if (operations.union) {
|
||||
|
|
|
@ -227,7 +227,7 @@ var BlendMode = new function() {
|
|||
|
||||
// Now test for the new blend modes. Just seeing if globalCompositeOperation
|
||||
// is sticky is not enough, as Chome 27 pretends for blend-modes to work,
|
||||
// but does not actually apply them.
|
||||
// but does not actually apply them.
|
||||
var ctx = CanvasProvider.getContext(1, 1);
|
||||
Base.each(modes, function(func, mode) {
|
||||
// Blend #330000 (51) and #aa0000 (170):
|
||||
|
|
|
@ -22,5 +22,6 @@ var options = {
|
|||
svg: true,
|
||||
fatline: true,
|
||||
paperscript: true,
|
||||
nativeContains: false,
|
||||
debug: false
|
||||
};
|
||||
|
|
|
@ -192,19 +192,42 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
|
|||
* Item#contains() is prepared for such a result.
|
||||
*/
|
||||
_contains: function(point) {
|
||||
/*#*/ if (options.nativeContains) {
|
||||
// To compare with native canvas approach:
|
||||
var ctx = CanvasProvider.getContext(1, 1),
|
||||
children = this._children,
|
||||
param = Base.merge({ compound: true });
|
||||
// Return early if the compound path doesn't have any children:
|
||||
if (children.length === 0)
|
||||
return [];
|
||||
ctx.beginPath();
|
||||
for (var i = 0, l = children.length; i < l; i++)
|
||||
children[i]._draw(ctx, param);
|
||||
var res = ctx.isPointInPath(point.x, point.y);
|
||||
CanvasProvider.release(ctx);
|
||||
return res && children;
|
||||
/*#*/ } // options.nativeContains
|
||||
|
||||
// Compound paths are a little complex: In order to determine whether a
|
||||
// point is inside a path or not due to the even-odd rule, we need to
|
||||
// check all the children and count how many intersect. If it's an odd
|
||||
// number, the point is inside the path. Once we know it's inside the
|
||||
// path, _hitTest also needs access to the first intersecting element,
|
||||
// for the HitResult, so we collect and return a list here.
|
||||
var children = [];
|
||||
var total = 0,
|
||||
children = [];
|
||||
for (var i = 0, l = this._children.length; i < l; i++) {
|
||||
var child = this._children[i];
|
||||
if (child.contains(point))
|
||||
var child = this._children[i],
|
||||
winding = child._getWinding(point);
|
||||
total += winding;
|
||||
/*
|
||||
if (winding & 1)
|
||||
children.push(child);
|
||||
*/
|
||||
if (winding)
|
||||
children.push(child);
|
||||
}
|
||||
return (children.length & 1) == 1 && children;
|
||||
return total && children; // <- non-zero // even-odd: (total & 1) && children;
|
||||
},
|
||||
|
||||
_hitTest: function _hitTest(point, options) {
|
||||
|
|
|
@ -535,7 +535,7 @@ statics: {
|
|||
|
||||
// Converts from the point coordinates (p1, c1, c2, p2) for one axis to
|
||||
// the polynomial coefficients and solves the polynomial for val
|
||||
solveCubic: function (v, coord, val, roots) {
|
||||
solveCubic: function (v, coord, val, roots, min, max) {
|
||||
var p1 = v[coord],
|
||||
c1 = v[coord + 2],
|
||||
c2 = v[coord + 4],
|
||||
|
@ -543,7 +543,7 @@ statics: {
|
|||
c = 3 * (c1 - p1),
|
||||
b = 3 * (c2 - c1) - c,
|
||||
a = p2 - p1 - c - b;
|
||||
return Numerical.solveCubic(a, b, c, p1 - val, roots);
|
||||
return Numerical.solveCubic(a, b, c, p1 - val, roots, min, max);
|
||||
},
|
||||
|
||||
getParameterOf: function(v, x, y) {
|
||||
|
@ -642,68 +642,8 @@ statics: {
|
|||
return new Rectangle(min[0], min[1], max[0] - min[0], max[1] - min[1]);
|
||||
},
|
||||
|
||||
_getCrossings: function(v, prev, x, y, roots) {
|
||||
// Implementation of the crossing number algorithm:
|
||||
// http://en.wikipedia.org/wiki/Point_in_polygon
|
||||
// Solve the y-axis cubic polynomial for y and count all solutions
|
||||
// to the right of x as crossings.
|
||||
var count = Curve.solveCubic(v, 1, y, roots),
|
||||
crossings = 0,
|
||||
tolerance = /*#=*/ Numerical.TOLERANCE,
|
||||
abs = Math.abs;
|
||||
|
||||
// Checks the y-slope between the current curve and the previous for a
|
||||
// change of orientation, when a solution is found at t == 0
|
||||
function changesOrientation(tangent) {
|
||||
return Curve.evaluate(prev, 1, 1).y
|
||||
* tangent.y > 0;
|
||||
}
|
||||
|
||||
if (count === -1) {
|
||||
// Infinite solutions, so we have a horizontal curve.
|
||||
// Find parameter through getParameterOf()
|
||||
roots[0] = Curve.getParameterOf(v, x, y);
|
||||
count = roots[0] !== null ? 1 : 0;
|
||||
}
|
||||
for (var i = 0; i < count; i++) {
|
||||
var t = roots[i];
|
||||
if (t > -tolerance && t < 1 - tolerance) {
|
||||
var pt = Curve.evaluate(v, t, 0);
|
||||
if (x < pt.x + tolerance) {
|
||||
// Pass 1 for Curve.evaluate() type to calculate tangent
|
||||
var tan = Curve.evaluate(v, t, 1);
|
||||
// Handle all kind of edge cases when points are on
|
||||
// contours or rays are touching countours, to termine
|
||||
// whether the crossing counts or not.
|
||||
// See if the actual point is on the countour:
|
||||
if (abs(pt.x - x) < tolerance) {
|
||||
// Do not count the crossing if it is on the left hand
|
||||
// side of the shape (tangent pointing upwards), since
|
||||
// the ray will go out the other end, count as crossing
|
||||
// there, and the point is on the contour, so to be
|
||||
// considered inside.
|
||||
var angle = tan.getAngle();
|
||||
if (angle > -180 && angle < 0
|
||||
// Handle special case where point is on a corner,
|
||||
// in which case this crossing is skipped if both
|
||||
// tangents have the same orientation.
|
||||
&& (t > tolerance || changesOrientation(tan)))
|
||||
continue;
|
||||
} else {
|
||||
// Skip touching stationary points:
|
||||
if (abs(tan.y) < tolerance
|
||||
// Check derivate for stationary points. If root is
|
||||
// close to 0 and not changing vertical orientation
|
||||
// from the previous curve, do not count this root,
|
||||
// as it's touching a corner.
|
||||
|| t < tolerance && !changesOrientation(tan))
|
||||
continue;
|
||||
}
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crossings;
|
||||
isClockwise: function(v) {
|
||||
return Curve._getEdgeSum(v) > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -750,6 +690,131 @@ statics: {
|
|||
+ t * t * t * v3,
|
||||
padding);
|
||||
}
|
||||
},
|
||||
|
||||
_getWinding: function(v, x, y, roots1, roots2) {
|
||||
var tolerance = /*#=*/ Numerical.TOLERANCE,
|
||||
abs = Math.abs;
|
||||
|
||||
// Implementation of the crossing number algorithm:
|
||||
// http://en.wikipedia.org/wiki/Point_in_polygon
|
||||
// Solve the y-axis cubic polynomial for y and count all solutions
|
||||
// to the right of x as crossings.
|
||||
if (Curve.isLinear(v)) {
|
||||
// Special case for handling lines.
|
||||
var y0 = v[1],
|
||||
y1 = v[7],
|
||||
dir = 1;
|
||||
if (y0 > y1) {
|
||||
var tmp = y0;
|
||||
y0 = y1;
|
||||
y1 = tmp;
|
||||
dir = -1;
|
||||
}
|
||||
if (y < y0 || y > y1)
|
||||
return 0;
|
||||
var cross = (v[6] - v[0]) * (y - v[1]) - (v[7] - v[1]) * (x - v[0]);
|
||||
if ((cross < -tolerance ? -1 : 1) == dir)
|
||||
dir = 0;
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Handle bezier curves. We need to chop them into smaller curves with
|
||||
// defined orientation, by solving the derrivative curve for Y extrema.
|
||||
var y0 = v[1],
|
||||
y1 = v[3],
|
||||
y2 = v[5],
|
||||
y3 = v[7];
|
||||
// Split the curve at Y extremas, to get mono bezier curves
|
||||
var a = 3 * (y1 - y2) - y0 + y3,
|
||||
b = 2 * (y0 + y2) - 4 * y1,
|
||||
c = y1 - y0,
|
||||
// Keep then range to 0 .. 1 (excluding) in the search for y extrema
|
||||
count = Numerical.solveQuadratic(a, b, c, roots1, tolerance,
|
||||
1 - tolerance);
|
||||
|
||||
var winding = 0,
|
||||
left,
|
||||
right = v;
|
||||
var t = roots1[0];
|
||||
for (var i = 0; i <= count; i++) {
|
||||
if (i === count) {
|
||||
left = right;
|
||||
} else {
|
||||
// Divide the curve at t.
|
||||
var curves = Curve.subdivide(right, t);
|
||||
left = curves[0];
|
||||
right = curves[1];
|
||||
t = roots1[i];
|
||||
// TODO: Watch for divide by 0
|
||||
// Now renormalize t to the range of the next iteration.
|
||||
t = (roots1[i + 1] - t) / (1 - t);
|
||||
}
|
||||
// Make sure that the connecting y extrema are flat
|
||||
if (i > 0)
|
||||
left[3] = left[1]; // curve2.handle1.y = curve2.point1.y;
|
||||
if (i < count)
|
||||
left[5] = right[1]; // curve1.handle2.y = curve2.point1.y;
|
||||
var dir = 1;
|
||||
if (left[1] > left[7]) {
|
||||
left = [
|
||||
left[6], left[7],
|
||||
left[4], left[5],
|
||||
left[2], left[3],
|
||||
left[0], left[1]
|
||||
];
|
||||
dir = -1;
|
||||
}
|
||||
if (y < left[1] || y > left[7])
|
||||
continue;
|
||||
// Adjust start and end range depending on if curve was flipped.
|
||||
// In normal orientation we exclude the end point since it's also
|
||||
// the start point of the next curve. If flipped, we have to exclude
|
||||
// the end point instead.
|
||||
var min = -tolerance * dir,
|
||||
root,
|
||||
xt;
|
||||
if (Curve.solveCubic(left, 1, y, roots2, min, 1 + min) === 1) {
|
||||
root = roots2[0];
|
||||
xt = Curve.evaluate(left, root, 0).x;
|
||||
} else {
|
||||
var mid = (left[1] + left[7]) / 2;
|
||||
xt = y < mid ? left[0] : left[6];
|
||||
root = y < mid ? 0 : 1;
|
||||
// Filter out end points based on direction.
|
||||
if (dir < 0 && root === 0 && abs(y - left[1]) < tolerance ||
|
||||
dir > 0 && root === 1 && abs(y - left[7]) < tolerance)
|
||||
continue;
|
||||
}
|
||||
// See if we're touching a horizontal stationary point.
|
||||
var flat = abs(Curve.evaluate(left, root, 1).y) < tolerance;
|
||||
// Calculate compare tolerance based on curve orientation (dir), to
|
||||
// add a bit of tolerance when considering points lying on the curve
|
||||
// as inside. But if we're touching a horizontal stationary point,
|
||||
// set compare tolerance to -tolerance, since we don't want to step
|
||||
// side-ways in tolerance based on orientation. This is needed e.g.
|
||||
// when touching the bottom tip of a circle.
|
||||
// Pass 1 for Curve.evaluate() type to calculate tangent
|
||||
if (x >= xt + (flat ? -tolerance : tolerance * dir)) {
|
||||
// If this is a horizontal stationary point, and we're at the
|
||||
// end of the curve, flip the orientation of dir.
|
||||
winding += flat && abs(root - 1) < tolerance ? -dir : dir;
|
||||
}
|
||||
}
|
||||
return winding;
|
||||
},
|
||||
|
||||
_getEdgeSum: function(v) {
|
||||
// Method derived from:
|
||||
// http://stackoverflow.com/questions/1165647
|
||||
// We treat the curve points and handles as the outline of a polygon of
|
||||
// which we determine the orientation using the method of calculating
|
||||
// the sum over the edges. This will work even with non-convex polygons,
|
||||
// telling you whether it's mostly clockwise
|
||||
var sum = 0;
|
||||
for (var j = 2; j < 8; j += 2)
|
||||
sum += (v[j - 2] - v[j]) * (v[j + 1] + v[j - 1]);
|
||||
return sum;
|
||||
}
|
||||
}}, Base.each(['getBounds', 'getStrokeBounds', 'getHandleBounds', 'getRoughBounds'],
|
||||
// Note: Although Curve.getBounds() exists, we are using Path.getBounds() to
|
||||
|
|
100
src/path/Path.js
100
src/path/Path.js
|
@ -1684,7 +1684,7 @@ var Path = PathItem.extend(/** @lends Path# */{
|
|||
return null;
|
||||
},
|
||||
|
||||
_contains: function(point) {
|
||||
_getWinding: function(point) {
|
||||
var closed = this._closed;
|
||||
// If the path is not closed, we should not bail out in case it has a
|
||||
// fill color!
|
||||
|
@ -1692,44 +1692,55 @@ var Path = PathItem.extend(/** @lends Path# */{
|
|||
// We need to call the internal _getBounds, to get non-
|
||||
// transformed bounds.
|
||||
|| !this._getBounds('getRoughBounds')._containsPoint(point))
|
||||
return false;
|
||||
// Note: This only works correctly with even-odd fill rule, or paths
|
||||
// that do not overlap with themselves.
|
||||
// TODO: Find out how to implement the "Point In Polygon" problem for
|
||||
// non-zero fill rule.
|
||||
return 0;
|
||||
// Use the crossing number algorithm, by counting the crossings of the
|
||||
// beam in right y-direction with the shape, and see if it's an odd
|
||||
// number, meaning the starting point is inside the shape.
|
||||
// http://en.wikipedia.org/wiki/Point_in_polygon
|
||||
var curves = this.getCurves(),
|
||||
segments = this._segments,
|
||||
crossings = 0,
|
||||
// Reuse one array for root-finding, give garbage collector a break
|
||||
roots = new Array(3),
|
||||
winding = 0,
|
||||
// Reuse arrays for root-finding, give garbage collector a break
|
||||
roots1 = [],
|
||||
roots2 = [],
|
||||
last = (closed
|
||||
? curves[curves.length - 1]
|
||||
// Create a straight closing line for open paths, just like
|
||||
// how filling open paths works.
|
||||
: new Curve(segments[segments.length - 1]._point,
|
||||
segments[0]._point)).getValues(),
|
||||
previous = last;
|
||||
segments[0]._point)).getValues();
|
||||
for (var i = 0, l = curves.length; i < l; i++) {
|
||||
var vals = curves[i].getValues(),
|
||||
x = vals[0],
|
||||
y = vals[1];
|
||||
// Filter out curves with 0-lenght (all 4 points in the same place):
|
||||
// Filter out curves with 0-length (all 4 points in the same place):
|
||||
if (!(x === vals[2] && y === vals[3] && x === vals[4]
|
||||
&& y === vals[5] && x === vals[6] && y === vals[7])) {
|
||||
crossings += Curve._getCrossings(vals, previous,
|
||||
point.x, point.y, roots);
|
||||
previous = vals;
|
||||
winding += Curve._getWinding(vals, point.x, point.y,
|
||||
roots1, roots2);
|
||||
}
|
||||
}
|
||||
if (!closed) {
|
||||
crossings += Curve._getCrossings(last, previous, point.x, point.y,
|
||||
roots);
|
||||
winding += Curve._getWinding(last, point.x, point.y,
|
||||
roots1, roots2);
|
||||
}
|
||||
return (crossings & 1) === 1;
|
||||
return winding;
|
||||
},
|
||||
|
||||
_contains: function(point) {
|
||||
/*#*/ if (options.nativeContains) {
|
||||
// To compare with native canvas approach:
|
||||
var ctx = CanvasProvider.getContext(1, 1);
|
||||
this._draw(ctx, Base.merge({ clip: true }));
|
||||
var res = ctx.isPointInPath(point.x, point.y);
|
||||
CanvasProvider.release(ctx);
|
||||
return res;
|
||||
/*#*/ } // options.nativeContains
|
||||
|
||||
// even-odd:
|
||||
// return !!(this._getWinding(point) & 1);
|
||||
// non-zero:
|
||||
return !!this._getWinding(point);
|
||||
},
|
||||
|
||||
_hitTest: function(point, options) {
|
||||
|
@ -1779,16 +1790,13 @@ var Path = PathItem.extend(/** @lends Path# */{
|
|||
|
||||
function isInArea(point) {
|
||||
var length = area.length,
|
||||
previous = getAreaCurve(length - 1),
|
||||
roots = new Array(3),
|
||||
crossings = 0;
|
||||
for (var i = 0; i < length; i++) {
|
||||
var curve = getAreaCurve(i);
|
||||
crossings += Curve._getCrossings(curve, previous,
|
||||
point.x, point.y, roots);
|
||||
previous = curve;
|
||||
}
|
||||
return (crossings & 1) === 1;
|
||||
roots1 = [],
|
||||
roots2 = [],
|
||||
winding = 0;
|
||||
for (var i = 0; i < length; i++)
|
||||
winding += Curve._getWinding(getAreaCurve(i), point.x, point.y,
|
||||
roots1, roots2);
|
||||
return !!winding;
|
||||
}
|
||||
|
||||
function checkSegmentStroke(segment) {
|
||||
|
@ -2410,35 +2418,11 @@ statics: {
|
|||
* @private
|
||||
*/
|
||||
isClockwise: function(segments) {
|
||||
var sum = 0,
|
||||
xPre, yPre,
|
||||
add = false;
|
||||
function edge(x, y) {
|
||||
if (add)
|
||||
sum += (xPre - x) * (y + yPre);
|
||||
xPre = x;
|
||||
yPre = y;
|
||||
add = true;
|
||||
}
|
||||
// Method derived from:
|
||||
// http://stackoverflow.com/questions/1165647
|
||||
// We treat the curve points and handles as the outline of a polygon of
|
||||
// which we determine the orientation using the method of calculating
|
||||
// the sum over the edges. This will work even with non-convex polygons,
|
||||
// telling you whether it's mostly clockwise
|
||||
var sum = 0;
|
||||
// TODO: Check if this works correctly for all open paths.
|
||||
for (var i = 0, l = segments.length; i < l; i++) {
|
||||
var seg1 = segments[i],
|
||||
seg2 = segments[i + 1 < l ? i + 1 : 0],
|
||||
point1 = seg1._point,
|
||||
handle1 = seg1._handleOut,
|
||||
handle2 = seg2._handleIn,
|
||||
point2 = seg2._point;
|
||||
edge(point1._x, point1._y);
|
||||
edge(point1._x + handle1._x, point1._y + handle1._y);
|
||||
edge(point2._x + handle2._x, point2._y + handle2._y);
|
||||
edge(point2._x, point2._y);
|
||||
}
|
||||
for (var i = 0, l = segments.length; i < l; i++)
|
||||
sum += Curve._getEdgeSum(Curve.getValues(segments[i],
|
||||
segments[i + 1 < l ? i + 1 : 0]));
|
||||
return sum > 0;
|
||||
},
|
||||
|
||||
|
@ -2690,7 +2674,9 @@ statics: {
|
|||
// possible.
|
||||
var strokeWidth = style.getStrokeColor() ? style.getStrokeWidth() : 0,
|
||||
joinWidth = strokeWidth;
|
||||
if (strokeWidth > 0) {
|
||||
if (strokeWidth === 0) {
|
||||
strokeWidth = /*#=*/ Numerical.TOLERANCE;
|
||||
} else {
|
||||
if (style.getStrokeJoin() === 'miter')
|
||||
joinWidth = strokeWidth * style.getMiterLimit();
|
||||
if (style.getStrokeCap() === 'square')
|
||||
|
|
|
@ -153,3 +153,17 @@ test('Path#contains() (Rotated Rectangle Contours)', function() {
|
|||
testPoint(path, curves[i].getPoint(0.5), true);
|
||||
}
|
||||
});
|
||||
|
||||
test('Path#contains() (touching stationary point with changing orientation)', function() {
|
||||
var path = new Path({
|
||||
segments: [
|
||||
new Segment([100, 100]),
|
||||
new Segment([200, 200], [-50, 0], [50, 0]),
|
||||
new Segment([300, 300]),
|
||||
new Segment([300, 100])
|
||||
],
|
||||
closed: true
|
||||
});
|
||||
|
||||
testPoint(path, new Point(200, 200), true);
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue