paper.js/src/path/PathItem.js

1120 lines
35 KiB
JavaScript
Raw Normal View History

2013-04-07 12:49:34 -04:00
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
2011-03-07 20:41:50 -05:00
* http://paperjs.org/
*
* Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
2011-03-06 19:50:44 -05:00
* http://lehni.org/ & http://jonathanpuckey.com/
*
2011-07-01 06:17:45 -04:00
* Distributed under the MIT license. See LICENSE file for details.
*
2011-03-07 20:41:50 -05:00
* All rights reserved.
2011-03-06 19:50:44 -05:00
*/
/**
* @name PathItem
2011-07-01 05:22:33 -04:00
*
* @class The PathItem class is the base for any items that describe paths
* and offer standardised methods for drawing and path manipulation, such as
* {@link Path} and {@link CompoundPath}.
*
* @extends Item
*/
var PathItem = Item.extend(/** @lends PathItem# */{
_class: 'PathItem',
initialize: function PathItem() {
// Do nothing.
},
/**
* Returns all intersections between two {@link PathItem} items as an array
* of {@link CurveLocation} objects. {@link CompoundPath} items are also
* supported.
*
* @param {PathItem} path the other item to find the intersections to.
* @return {CurveLocation[]} the locations of all intersection between the
* paths
* @example {@paperscript}
* // Create a rectangular path with its top-left point at
* // {x: 30, y: 25} and a size of {width: 50, height: 50}:
* var path = new Path.Rectangle(new Point(30, 25), new Size(50, 50));
* path.strokeColor = 'black';
*
* var secondPath = path.clone();
* var intersectionGroup = new Group();
*
* function onFrame(event) {
* secondPath.rotate(3);
*
* var intersections = path.getIntersections(secondPath);
* intersectionGroup.removeChildren();
*
* for (var i = 0; i < intersections.length; i++) {
* var intersectionPath = new Path.Circle({
* center: intersections[i].point,
* radius: 4,
* fillColor: 'red'
* });
* intersectionGroup.addChild(intersectionPath);
* }
* }
*/
getIntersections: function(path, expand) {
// First check the bounds of the two paths. If they don't intersect,
2012-12-27 14:19:23 -05:00
// we don't need to iterate through their curves.
if (!this.getBounds().touches(path.getBounds()))
return [];
var locations = [],
curves1 = this.getCurves(),
curves2 = path.getCurves(),
2013-12-08 16:12:36 -05:00
matrix1 = this._matrix.orNullIfIdentity(),
matrix2 = path._matrix.orNullIfIdentity(),
2013-12-17 09:23:07 -05:00
length1 = curves1.length,
length2 = curves2.length,
values2 = [];
for (var i = 0; i < length2; i++)
values2[i] = curves2[i].getValues(matrix2);
2013-12-17 09:23:07 -05:00
for (var i = 0; i < length1; i++) {
var curve1 = curves1[i],
values1 = curve1.getValues(matrix1);
for (var j = 0; j < length2; j++)
Curve.getIntersections(values1, values2[j], curve1, curves2[j],
locations);
}
return PathItem._conditionIntersections(locations, expand);
},
getSelfIntersections: function(expand){
var locations = [],
locs = [],
curves = this.getCurves(),
length = curves.length - 1,
matrix = this._matrix.orNullIfIdentity(),
values = [],
curve1, values1, parts, i, j, k, ix, from, to, param, v1, v2,
EPSILON = /*#=*/ Numerical.EPSILON,
EPSILON1s = 1-EPSILON;
for (i = 0; i <= length; i++)
values[i] = curves[i].getValues(matrix);
for (i = 0; i <= length; i++) {
curve1 = curves[i];
values1 = values[i];
// First check for self-intersections within the same curve
from = curve1.getSegment1();
to = curve1.getSegment2();
v1 = from._handleOut;
v2 = to._handleIn;
// Check if extended handles of endpoints of this curve intersects
// each other. We cannot have a self intersection within this curve
// if they don't intersect due to convex-hull property.
ix = new paper.Line(from._point.subtract(v1), v1.multiply(2), true)
.intersect(new paper.Line(to._point.subtract(v2),
v2.multiply(2), true), false);
if (ix) {
parts = paper.Curve.subdivide(values1);
locs.length = 0;
Curve.getIntersections(parts[0], parts[1], curve1, curve1, locs);
for (j = locs.length - 1; j >= 0; j--) {
ix = locs[j];
if (ix._parameter <= EPSILON1s) {
ix._parameter = ix._parameter * 0.5;
ix._parameter2 = 0.5 + ix._parameter2 * 0.5;
break;
}
}
if (j >= 0)
locations.push(ix);
}
// Check for intersections with other curves
for (j = i + 1; j <= length; j++){
// Avoid end point intersections on consecutive curves
if (j === i + 1 || (j === length && i === 0)) {
locs.length = 0;
Curve.getIntersections(values1, values[j], curve1,
curves[j], locs);
for (k = locs.length - 1; k >= 0; k--) {
param = locs[k].getParameter();
if (param < EPSILON1s && param > EPSILON)
locations.push(locs[k]);
}
} else {
paper.Curve.getIntersections(values1, values[j], curve1,
curves[j], locations);
}
}
}
return PathItem._conditionIntersections(locations, expand);
},
setPathData: function(data) {
// This is a very compact SVG Path Data parser that works both for Path
// and CompoundPath.
2013-11-02 04:30:40 -04:00
// First split the path data into parts of command-coordinates pairs
// Commands are any of these characters: mzlhvcsqta
var parts = data.match(/[mlhvcsqtaz][^mlhvcsqtaz]*/ig),
coords,
relative = false,
control,
current = new Point(); // the current position
function getCoord(index, coord, isCurrent) {
2013-11-02 04:30:40 -04:00
var val = parseFloat(coords[index]);
if (relative)
val += current[coord];
if (isCurrent)
current[coord] = val;
return val;
}
function getPoint(index, isCurrent) {
return new Point(
getCoord(index, 'x', isCurrent),
getCoord(index + 1, 'y', isCurrent)
);
}
// First clear the previous content
this.clear();
for (var i = 0, l = parts.length; i < l; i++) {
2013-06-12 20:27:20 -04:00
var part = parts[i],
cmd = part[0],
lower = cmd.toLowerCase();
2013-11-02 04:30:40 -04:00
// Match all coordinate values
coords = part.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g);
2013-11-02 04:30:40 -04:00
var length = coords && coords.length;
relative = cmd === lower;
switch (lower) {
case 'm':
case 'l':
2013-11-02 04:30:40 -04:00
for (var j = 0; j < length; j += 2)
this[j === 0 && lower === 'm' ? 'moveTo' : 'lineTo'](
getPoint(j, true));
control = current;
break;
case 'h':
case 'v':
var coord = lower == 'h' ? 'x' : 'y';
2013-11-02 04:30:40 -04:00
for (var j = 0; j < length; j++) {
getCoord(j, coord, true);
this.lineTo(current);
}
control = current;
break;
case 'c':
2013-11-02 04:30:40 -04:00
for (var j = 0; j < length; j += 6) {
this.cubicCurveTo(
getPoint(j),
control = getPoint(j + 2),
getPoint(j + 4, true));
}
break;
2013-02-28 22:21:46 -05:00
case 's':
// Smooth cubicCurveTo
2013-11-02 04:30:40 -04:00
for (var j = 0; j < length; j += 4) {
this.cubicCurveTo(
// Calculate reflection of previous control points
current.multiply(2).subtract(control),
control = getPoint(j),
getPoint(j + 2, true));
}
break;
case 'q':
2013-11-02 04:30:40 -04:00
for (var j = 0; j < length; j += 4) {
this.quadraticCurveTo(
control = getPoint(j),
getPoint(j + 2, true));
}
break;
case 't':
// Smooth quadraticCurveTo
2013-11-02 04:30:40 -04:00
for (var j = 0; j < length; j += 2) {
2013-03-01 04:18:42 -05:00
this.quadraticCurveTo(
// Calculate reflection of previous control points
control = current.multiply(2).subtract(control),
getPoint(j, true));
}
break;
case 'a':
// TODO: Implement Arcs!
break;
case 'z':
this.closePath();
break;
}
}
},
_canComposite: function() {
// A path with only a fill or a stroke can be directly blended, but if
// it has both, it needs to be drawn into a separate canvas first.
return !(this.hasFill() && this.hasStroke());
},
/**
* Returns the winding contribution of the given point with respect to this
* PathItem.
*
* @param {Object} point Point to determine the winding direction
* about.
* @param {Boolean} horizontal Boolean value indicating if we need to
* consider this point as part of a
* horizontal curve.
* @return {Number} Winding number.
*/
_getWinding: function(point, horizontal) {
var curves = this._getMonotoneCurves();
return PathItem._getWindingNumber(point, curves, horizontal);
},
_contains: function(point) {
// NOTE: point is reverse transformed by _matrix, so we don't need to
// apply here.
/*#*/ if (__options.nativeContains) {
// 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 }));
var res = ctx.isPointInPath(point.x, point.y, this.getWindingRule());
CanvasProvider.release(ctx);
return res;
/*#*/ } else { // !__options.nativeContains
var winding = this._getWinding(point);
return !!(this.getWindingRule() === 'evenodd' ? winding & 1 : winding);
/*#*/ } // !__options.nativeContains
2013-12-29 07:25:48 -05:00
},
statics: {
/**
* Private method for splitting a PathItem at the given intersections.
* The routine works for both self intersections and intersections
* between PathItems.
* @param {Array} intersections Array of CurveLocation objects
*/
_splitPath: function(intersections) {
var loc, i, j, node1, node2, t, segment,
path1, isLinear, crv, crvNew,
newSegments = [],
tolerance = /*#=*/ Numerical.EPSILON;
for (i = intersections.length - 1; i >= 0; i--) {
node1 = intersections[i];
path1 = node1.getPath();
// Check if we are splitting same curve multiple times
if (node2 && node2.getPath() === path1 &&
node2._curve === node1._curve) {
// Use the result of last split and interpolate the parameter.
crv = crvNew;
t = node1._parameter / node2._parameter;
} else {
crv = node1._curve;
t = node1._parameter;
isLinear = crv.isLinear();
newSegments.length = 0;
}
// Split the curve at t, while ignoring linearity of curves
if ((crvNew = crv.divide(t, true, true)) === null){
if (t >= 1-tolerance) {
segment = crv._segment2;
} else if (t <= tolerance) {
segment = crv._segment1;
} else {
// Determine the closest segment by comparing curve lengths
segment = crv.getLength(0, t) < crv.getLength(t, 1)
? crv._segment1 : crv._segment2;
}
crvNew = crv;
} else {
segment = crvNew.getSegment1();
crvNew = crvNew.getPrevious();
}
// Link the new segment with the intersection on the other curve
segment._intersection = node1.getIntersection();
node1._segment = segment;
node2 = node1;
// Reset linear segments if they were part of a linear curve
// and if we are done with the entire curve.
newSegments.push(segment);
loc = intersections[i - 1];
if (!(loc && loc.getPath() === path1 &&
loc._curve === node1._curve) && isLinear)
for (j = newSegments.length-1; j >= 0; j--) {
segment = newSegments[j];
// FIXME: Don't reset the appropriate handle if the intersections were on t==0 && t==1
2013-12-29 07:25:48 -05:00
segment._handleOut.set(0, 0);
segment._handleIn.set(0, 0);
}
}
},
/**
* Private static method that returns the winding contribution of the
* given point with respect to a given set of monotone curves
*
*/
_getWindingNumber: function(point, curves, horizontal) {
function getTangent(v, t) {
var tan, sign, i;
sign = t === 0 ? 2 : (t === 1 ? -2 : 0);
if (sign !== 0) {
i = sign > 0 ? 0 : 6;
if (Curve.isLinear(v)) {
// Return slope from this point that follows the direction
// of the line
sign *= 3;
tan = new Point(v[i+sign] - v[i], v[i+sign+1] - v[i+1]);
} else {
// Return the first or last handle
tan = new Point(v[i+sign] - v[i], v[i+sign+1] - v[i+1]);
}
} else {
tan = Curve.evaluate(v, t, 1);
}
return tan;
}
var i, j, li, t, x0, y0, wind, v, slope, stationary,
tolerance = /*#=*/ Numerical.TOLERANCE,
x = point.x,
y = point.y,
xAfter = x + tolerance,
xBefore = x - tolerance,
windLeft = 0,
windRight = 0,
roots = [],
abs = Math.abs;
// Absolutely horizontal curves may return wrong results, since
// the curves are monotonic in y direction and this is an
// indeterminate state.
if (horizontal) {
var yTop = -Infinity,
yBot = Infinity;
// Find the closest yIntercepts in the same vertical line
for (i = 0, li = curves.length-1; i <= li; i++) {
v = curves[i];
if (Curve.solveCubic(v, 0, x, roots, 0, 1) > 0) {
for (j = roots.length - 1; j >= 0; j--) {
t = roots[j];
y0 = Curve.evaluate(v, t, 0).y;
if (y0 > y+tolerance && y0 < yBot) {
yBot = y0;
} else if (y0 < y-tolerance && y0 > yTop) {
yTop = y0;
}
}
}
}
// Shift the point lying on the horizontal curves by
// half of closest top and bottom intercepts.
yTop = (yTop + y) / 2;
yBot = (yBot + y) / 2;
windLeft = yTop > -Infinity
? PathItem._getWindingNumber(new Point(x, yTop), curves)
: 0;
windRight = yBot < Infinity
? PathItem._getWindingNumber(new Point(x, yBot), curves)
: 0;
return Math.max(windLeft, windRight);
}
// Find the winding number for right hand side of the curve,
// inclusive of the curve itself, while tracing along its ±x direction.
for (i = 0, li = curves.length-1; i <= li; i++) {
v = curves[i];
if (Curve.solveCubic(v, 1, y, roots, -tolerance, 1 + -tolerance) === 1) {
t = roots[0];
if ( t >= -tolerance && t <= tolerance)
t = 0;
x0 = Curve.evaluate(v, t, 0).x;
slope = getTangent(v, t).y;
stationary = !Curve.isLinear(v) && abs(slope) < tolerance;
// Take care of cases where the curve and the preceeding
// curve merely touches the ray towards ±x direction, but
// proceeds to the same side of the ray. This essentially is
// not a crossing.
if (t === 0){
// The previous curve's reference is stored at index:9,
// see Path#_getMonotoneCurves for details.
var v2 = v[9];
if (abs(v2[6] - v[0]) < tolerance && abs(v2[7] - v[1]) < tolerance){
var slope2 = getTangent(v2, 1).y;
if(slope * slope2 > 0)
stationary = true;
}
}
wind = v[8];
if (x0 <= xBefore && !stationary)
windLeft += wind;
if (x0 >= xAfter && !stationary)
windRight += wind;
}
}
return Math.max(abs(windLeft), abs(windRight));
},
/**
* Private method to trace closed contours from a set of segments according
* to a set of constraints winding contribution and a custom operator.
*
* @param {Array} segments Array of 'seed' segments for tracing closed
* contours.
* @param {Function} operator A function. It must take one argument, which
* is the winding number contribution of a
* curve, and should return a boolean value
* indicating whether the curve should be
* included in the final contour or not.
* @return {Array} Array of contours traced.
*/
_tracePaths: function(segments, operator) {
// Utility function. Correctly returns entry and exit tangents of an
// intersection, even when the curve[s] are linear.
function getEntryExitTangents(seg) {
var c2 = seg.getCurve(),
c1 = c2.getPrevious();
// Avoid zero length curves
c1 = c1.getLength() === 0 ? c1.getPrevious() : c1;
c2 = c2.getLength() === 0 ? c2.getNext() : c2;
var v1 = c1.getValues(),
v2 = c2.getValues(),
pnt = seg.getPoint(),
ret = [seg.getHandleIn().normalize(),
seg.getHandleOut().normalize()];
if (ret[0].getLength() === 0)
ret[0] = new Point(pnt.x - v1[2], pnt.y - v1[3]);
if (ret[1].getLength() === 0)
ret[1] = new Point(pnt.x - v2[4], pnt.y - v2[5]);
return ret;
}
// Choose a default operator which will return all contours
if (!operator)
operator = function(){ return true; };
var seg, startSeg, startSegIx, i, j, len, path, ixOther, firstHandleIn,
c1, c3, c4, t1, tan, crv, ixOtherSeg, nextSeg, nextHandleIn,
nextHandleOut, direction, entryExitTangents,
// Tangents of all curves at an intersection, except the entry curve
crvTan = [{}, {}],
// Compare curve tangents to sort them counter clockwise.
crvTanCompare = function(a, b){ return a.w - b.w; },
paths = [];
for (i = 0, len = segments.length; i < len; i++) {
startSeg = seg = segments[i];
if (seg._visited || !operator(seg._winding))
continue;
// Initialise a new path chain with the seed segment.
path = new paper.Path();
ixOther = seg._intersection;
startSegIx = ixOther ? ixOther._segment : null;
firstHandleIn = null;
direction = 1;
// DEBUG:--------------------------------------------------------
// hilightCrvN("all");
// hilightCrvN("next", seg.getCurve());
// DEBUG:--------------------------------------------------------
do {
nextHandleIn = direction > 0 ? seg._handleIn : seg._handleOut;
nextHandleOut = direction > 0 ? seg._handleOut : seg._handleIn;
ixOther = seg._intersection;
// If the intersection segment is valid, try switching to
// it, with an appropriate direction to continue traversal.
// else, stay on the same contour.
if (ixOther && (ixOtherSeg = ixOther._segment) &&
ixOtherSeg !== startSeg && firstHandleIn) {
entryExitTangents = getEntryExitTangents(seg);
c1 = seg.getCurve();
if (direction < 1) {
entryExitTangents.reverse();
} else {
c1 = c1.getPrevious();
}
t1 = entryExitTangents[0];
entryExitTangents = getEntryExitTangents(ixOtherSeg);
c4 = crvTan[1].c = ixOtherSeg.getCurve();
c3 = crvTan[0].c = c4.getPrevious();
// Avoid zero length curves
c3 = crvTan[0].c = c3.getLength() === 0 ? c3.getPrevious() : c3;
c4 = crvTan[1].c = c4.getLength() === 0 ? c4.getNext() : c4;
crvTan[0].t = entryExitTangents[0];
crvTan[1].t = entryExitTangents[1];
// cross product of the entry and exit tangent vectors at
// the intersection, will let us select the correct countour
// to traverse next.
for (j = 0; j < 2; j++) {
tan = crvTan[j].t;
crvTan[j].w = t1.x * tan.y - tan.x * t1.y;
}
// Do not attempt to switch contours if we not absolutely
// sure, that there is a possible candidate there.
if (crvTan[0].w * crvTan[1].w < 0) {
crvTan.sort(crvTanCompare);
j = 0;
do {
crv = crvTan[j++].c;
if (crv === c3) {
nextSeg = c3.getSegment1();
// DEBUG:--------------------------------------------------------
// hilightCrvN("nextSeg", nextSeg, "#f00");
// hilightCrvN("nextCrv", c3, "#f00");
// DEBUG:--------------------------------------------------------
// Traverse backward
direction = -1;
} else {
nextSeg = c4.getSegment1();
// DEBUG:--------------------------------------------------------
// hilightCrvN("nextSeg", nextSeg, "#f00");
// hilightCrvN("nextCrv", c4, "#f00");
// DEBUG:--------------------------------------------------------
// Traverse forward
direction = 1;
}
} while (j < 2 && !operator(nextSeg._winding) ||
(crv !== c3 && crv !== c4));
} else {
nextSeg = null;
}
if (!nextSeg || !operator(nextSeg._winding)) {
direction = 1;
} else {
// Switch to the intersection segment.
seg._visited = ixOtherSeg._visited;
seg = ixOtherSeg;
if (nextSeg._visited)
direction = 1;
}
// DEBUG:--------------------------------------------------------
// hilightCrvN("nextCrv");
// hilightCrvN("nextSeg", nextSeg, "#0f0");
// DEBUG:--------------------------------------------------------
nextHandleOut = direction > 0 ? seg._handleOut : seg._handleIn;
}
// DEBUG:--------------------------------------------------------
// hilightCrvN("next", seg.getCurve());
// DEBUG:--------------------------------------------------------
// Add the current segment to the path, and mark
// the added segment as visited
if (!firstHandleIn) {
firstHandleIn = nextHandleIn;
nextHandleIn = null;
}
path.add(new paper.Segment(seg._point, nextHandleIn,
nextHandleOut));
seg._visited = true;
// Move to the next segment according to the traversal direction
seg = direction > 0 ? seg.getNext() : seg. getPrevious();
// DEBUG:--------------------------------------------------------
// seg && hilightCrvN("next", direction ? seg.getCurve() : seg.getCurve().getPrevious(), "#a0a");
// DEBUG:--------------------------------------------------------
} while(seg && seg !== startSeg && seg !== startSegIx &&
!seg._visited && (seg._intersection || operator(seg._winding)));
// Finish with closing the paths if necessary,
// correctly linking up curves etc.
if (seg && (seg == startSeg || seg == startSegIx)){
path.firstSegment.setHandleIn((seg == startSegIx)?
startSegIx._handleIn : seg._handleIn);
path.setClosed(true);
} else {
path.lastSegment._handleOut.set(0, 0);
}
// Add the path to the result
// Try to avoid stray segments and incomplete paths.
if ((path.closed && path.segments.length) || path.segments.length > 2 ||
(path.closed && path.segments.length === 2 &&
(!path.getCurves()[0].isLinear() ||
!path.getCurves()[1].isLinear()))) {
paths.push(path);
} else {
path.remove();
}
}
// DEBUG:--------------------------------------------------------
// hilightCrvN("all");
// DEBUG:--------------------------------------------------------
return paths;
},
_tracePathsOld: function(segments, operator) {
var seg, nextSeg, startSeg, startSegIx, i, len, ixOther, prev,
ixOtherSeg, c1, c2, c3,
wind, w1, w3, s1, s3, path, nextHandleIn,
paths = [];
for (i = 0, len = segments.length; i < len; i++) {
startSeg = seg = segments[i];
if (seg._visited || !operator(seg._winding))
continue;
// Initialise a new path chain with the seed segment.
path = new paper.Path();
wind = seg._winding;
ixOther = seg._intersection;
startSegIx = ixOther ? ixOther._segment : null;
// Set the correct handles for this segment
prev = seg.getPrevious();
if (ixOther && prev && prev._visited)
seg._handleIn = new paper.Point(0, 0);
do {
nextHandleIn = nextHandleIn || seg._handleIn;
path.add(new paper.Segment(seg._point, nextHandleIn,
seg._handleOut));
nextHandleIn = null;
seg._visited = true;
seg = (seg._nextPathSegment ? seg._nextPathSegment :
seg).getNext();
// This segments's _intersection property holds a reference to
// the intersection on the other curve.
ixOther = seg ? seg._intersection : null;
if (ixOther && (ixOtherSeg = ixOther._segment) &&
ixOtherSeg !== startSeg) {
c1 = seg.getCurve();
c2 = c1.getPrevious();
c3 = ixOtherSeg.getCurve();
// c2 is the entry point in the direction we are
// traversing the graph; sort c1 and c3 curves based on c2.
w1 = c1._segment1._winding;
w3 = c3._segment1._winding;
nextSeg = null;
s1 = c1.getSegment1();
s3 = c3.getSegment1();
if (wind === w1 && !s1._visited) {
nextSeg = s1;
} else if (wind === w3 && !s3._visited) {
nextSeg = s3;
}
if (nextSeg)
nextHandleIn = seg._handleIn;
seg = nextSeg || seg;
seg._winding = wind;
}
} while(seg && seg !== startSeg && seg !== startSegIx &&
!seg._visited && operator(seg._winding));
// Finish with closing the paths if necessary, correctly
// linking up curves etc.
if (seg && (seg == startSeg || seg == startSegIx)){
if (path.segments.length === 1) {
// This is still a valid path, in case of self-Intersections
path.add(new paper.Segment(seg._point, seg._handleIn, null));
} else {
path.firstSegment.setHandleIn((seg == startSegIx)?
startSegIx._handleIn : startSeg._handleIn);
}
}
path.setClosed(true);
// Add the path to the result
// Try to avoid stray segments and incomplete paths.
if (path.segments.length > 2 || (path.segments.length === 2 &&
(!path.getCurves()[0].isLinear() ||
!path.getCurves()[1].isLinear()))) {
paths.push(path);
} else {
path.remove();
}
}
return paths;
},
_conditionIntersections: function(locations, expand) {
function compare(loc1, loc2) {
var path1 = loc1.getPath(),
path2 = loc2.getPath();
return path1 === path2
// We can add parameter (0 <= t <= 1) to index
// (a integer) to compare both at the same time
? (loc1.getIndex() + loc1.getParameter())
- (loc2.getIndex() + loc2.getParameter())
// Sort by path id to group all locations on the same path.
: path1._id - path2._id;
}
// Remove duplicate intersections near curve endings
var loc, locNext,
tolerance = Numerical.EPSILON,
tolerance1 = 1 - tolerance,
abs = Math.abs;
// Merge intersections very close to the end of a curve to the
// begining of the next curve
for (var i = locations.length-1; i >= 0; i--) {
loc = locations[i];
locNext = loc._curve.getNext();
if (loc._parameter >= tolerance1 && locNext) {
loc._parameter = 0;
loc._curve = locNext;
}
locNext = loc._curve2.getNext();
if (loc._parameter2 >= tolerance1 && locNext) {
loc._parameter2 = 0;
loc._curve2 = locNext;
}
}
if (locations.length > 1) {
locations.sort(compare);
for (var length1 = locations.length - 1, i = length1; i >= 0; i--) {
loc = locations[i];
locNext = (i === 0)? locations[length1] : locations[i-1];
if (abs(loc._parameter - locNext._parameter) < tolerance &&
loc._curve === locNext._curve &&
abs(loc._parameter2 - locNext._parameter2) < tolerance &&
loc._curve2 === locNext._curve2) {
locations.splice(i, 1);
--length1;
}
}
}
if (expand) {
for (var i = locations.length-1; i >= 0; i--) {
loc = locations[i];
locations.push(loc.getIntersection());
}
locations.sort(compare);
}
return locations;
},
2013-06-12 20:42:38 -04:00
}
/**
* Smooth bezier curves without changing the amount of segments or their
* points, by only smoothing and adjusting their handle points, for both
* open ended and closed paths.
*
* @name PathItem#smooth
* @function
*
* @example {@paperscript}
* // Smoothing a closed shape:
*
* // Create a rectangular path with its top-left point at
* // {x: 30, y: 25} and a size of {width: 50, height: 50}:
* var path = new Path.Rectangle(new Point(30, 25), new Size(50, 50));
* path.strokeColor = 'black';
*
* // Select the path, so we can see its handles:
* path.fullySelected = true;
*
* // Create a copy of the path and move it 100pt to the right:
* var copy = path.clone();
* copy.position.x += 100;
*
* // Smooth the segments of the copy:
* copy.smooth();
*
* @example {@paperscript height=220}
* var path = new Path();
* path.strokeColor = 'black';
*
* path.add(new Point(30, 50));
*
* var y = 5;
* var x = 3;
*
* for (var i = 0; i < 28; i++) {
* y *= -1.1;
* x *= 1.1;
* path.lineBy(x, y);
* }
*
* // Create a copy of the path and move it 100pt down:
* var copy = path.clone();
* copy.position.y += 120;
*
* // Set its stroke color to red:
* copy.strokeColor = 'red';
*
* // Smooth the segments of the copy:
* copy.smooth();
*/
/**
* {@grouptitle Postscript Style Drawing Commands}
*
2012-10-05 19:09:15 -04:00
* On a normal empty {@link Path}, the point is simply added as the path's
* first segment. If called on a {@link CompoundPath}, a new {@link Path} is
* created as a child and the point is added as its first segment.
*
* @name PathItem#moveTo
* @function
* @param {Point} point
*/
2011-12-23 16:47:10 -05:00
// DOCS: Document #lineTo()
/**
* @name PathItem#lineTo
* @function
* @param {Point} point
*/
/**
* Adds a cubic bezier curve to the path, defined by two handles and a to
* point.
*
* @name PathItem#cubicCurveTo
* @function
* @param {Point} handle1
* @param {Point} handle2
* @param {Point} to
*/
/**
* Adds a quadratic bezier curve to the path, defined by a handle and a to
* point.
*
* @name PathItem#quadraticCurveTo
* @function
* @param {Point} handle
* @param {Point} to
*/
2011-12-23 16:47:10 -05:00
// DOCS: Document PathItem#curveTo() 'paramater' param.
/**
* Draws a curve from the position of the last segment point in the path
* that goes through the specified {@code through} point, to the specified
* {@code to} point by adding one segment to the path.
*
* @name PathItem#curveTo
* @function
* @param {Point} through the point through which the curve should go
* @param {Point} to the point where the curve should end
* @param {Number} [parameter=0.5]
*
* @example {@paperscript height=300}
* // Interactive example. Move your mouse around the view below:
*
* var myPath;
* function onMouseMove(event) {
* // If we created a path before, remove it:
* if (myPath) {
* myPath.remove();
* }
*
* // Create a new path and add a segment point to it
* // at {x: 150, y: 150):
* myPath = new Path();
* myPath.add(150, 150);
*
* // Draw a curve through the position of the mouse to 'toPoint'
* var toPoint = new Point(350, 150);
* myPath.curveTo(event.point, toPoint);
*
* // Select the path, so we can see its segments:
* myPath.selected = true;
* }
*/
/**
* Draws an arc from the position of the last segment point in the path that
* goes through the specified {@code through} point, to the specified
* {@code to} point by adding one or more segments to the path.
*
* @name PathItem#arcTo
* @function
* @param {Point} through the point where the arc should pass through
* @param {Point} to the point where the arc should end
*
* @example {@paperscript}
* var path = new Path();
* path.strokeColor = 'black';
*
* var firstPoint = new Point(30, 75);
* path.add(firstPoint);
*
* // The point through which we will create the arc:
* var throughPoint = new Point(40, 40);
*
* // The point at which the arc will end:
* var toPoint = new Point(130, 75);
*
* // Draw an arc through 'throughPoint' to 'toPoint'
* path.arcTo(throughPoint, toPoint);
*
* // Add a red circle shaped path at the position of 'throughPoint':
* var circle = new Path.Circle(throughPoint, 3);
* circle.fillColor = 'red';
*
* @example {@paperscript height=300}
* // Interactive example. Click and drag in the view below:
*
* var myPath;
* function onMouseDrag(event) {
* // If we created a path before, remove it:
* if (myPath) {
* myPath.remove();
* }
*
* // Create a new path and add a segment point to it
* // at {x: 150, y: 150):
* myPath = new Path();
* myPath.add(150, 150);
*
* // Draw an arc through the position of the mouse to 'toPoint'
* var toPoint = new Point(350, 150);
* myPath.arcTo(event.point, toPoint);
*
* // Select the path, so we can see its segments:
* myPath.selected = true;
* }
*
* // When the mouse is released, deselect the path
* // and fill it with black.
* function onMouseUp(event) {
* myPath.selected = false;
* myPath.fillColor = 'black';
* }
*/
/**
* Draws an arc from the position of the last segment point in the path to
* the specified point by adding one or more segments to the path.
*
* @name PathItem#arcTo
* @function
2011-06-16 17:38:58 -04:00
* @param {Point} to the point where the arc should end
* @param {Boolean} [clockwise=true] specifies whether the arc should be
* drawn in clockwise direction.
*
* @example {@paperscript}
* var path = new Path();
* path.strokeColor = 'black';
*
* path.add(new Point(30, 75));
* path.arcTo(new Point(130, 75));
*
* var path2 = new Path();
* path2.strokeColor = 'red';
* path2.add(new Point(180, 25));
*
* // To draw an arc in anticlockwise direction,
* // we pass 'false' as the second argument to arcTo:
* path2.arcTo(new Point(280, 25), false);
*
* @example {@paperscript height=300}
* // Interactive example. Click and drag in the view below:
* var myPath;
*
* // The mouse has to move at least 20 points before
* // the next mouse drag event is fired:
* tool.minDistance = 20;
*
* // When the user clicks, create a new path and add
* // the current mouse position to it as its first segment:
* function onMouseDown(event) {
* myPath = new Path();
* myPath.strokeColor = 'black';
* myPath.add(event.point);
* }
*
* // On each mouse drag event, draw an arc to the current
* // position of the mouse:
* function onMouseDrag(event) {
* myPath.arcTo(event.point);
* }
*/
/**
* Closes the path. When closed, Paper.js connects the first and last
* segments.
*
* @name PathItem#closePath
* @function
* @see Path#closed
*/
/**
* {@grouptitle Relative Drawing Commands}
*
* If called on a {@link CompoundPath}, a new {@link Path} is created as a
2012-10-05 19:09:15 -04:00
* child and a point is added as its first segment relative to the
* position of the last segment of the current path.
*
* @name PathItem#moveBy
* @function
* @param {Point} to
*/
/**
* Adds a segment relative to the last segment point of the path.
*
* @name PathItem#lineBy
* @function
* @param {Point} to the vector which is added to the position of the last
* segment of the path, to get to the position of the new segment.
*
* @example {@paperscript}
* var path = new Path();
* path.strokeColor = 'black';
*
* // Add a segment at {x: 50, y: 50}
* path.add(25, 25);
*
* // Add a segment relative to the last segment of the path.
* // 50 in x direction and 0 in y direction, becomes {x: 75, y: 25}
* path.lineBy(50, 0);
*
* // 0 in x direction and 50 in y direction, becomes {x: 75, y: 75}
* path.lineBy(0, 50);
*
* @example {@paperscript height=300}
* // Drawing a spiral using lineBy:
* var path = new Path();
* path.strokeColor = 'black';
*
* // Add the first segment at {x: 50, y: 50}
* path.add(view.center);
*
* // Loop 500 times:
* for (var i = 0; i < 500; i++) {
* // Create a vector with an ever increasing length
* // and an angle in increments of 45 degrees
* var vector = new Point({
* angle: i * 45,
* length: i / 2
* });
* // Add the vector relatively to the last segment point:
* path.lineBy(vector);
* }
*
* // Smooth the handles of the path:
* path.smooth();
*
* // Uncomment the following line and click on 'run' to see
* // the construction of the path:
* // path.selected = true;
*/
2011-12-23 16:47:10 -05:00
// DOCS: Document Path#curveBy()
/**
* @name PathItem#curveBy
* @function
* @param {Point} through
* @param {Point} to
* @param {Number} [parameter=0.5]
*/
2013-11-24 19:04:51 -05:00
// DOCS: Document Path#cubicCurveBy()
/**
* @name PathItem#cubicCurveBy
* @function
* @param {Point} handle1
* @param {Point} handle2
* @param {Point} to
*/
2013-11-24 19:04:51 -05:00
// DOCS: Document Path#quadraticCurveBy()
/**
* @name PathItem#quadraticCurveBy
* @function
* @param {Point} handle
* @param {Point} to
*/
2013-11-24 19:04:51 -05:00
// DOCS: Document Path#arcBy(through, to)
/**
* @name PathItem#arcBy
* @function
* @param {Point} through
* @param {Point} to
*/
2013-11-24 19:04:51 -05:00
// DOCS: Document Path#arcBy(to, clockwise)
/**
* @name PathItem#arcBy
* @function
* @param {Point} to
* @param {Boolean} [clockwise=true]
*/
});