/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey * http://scratchdisk.com/ & http://jonathanpuckey.com/ * * Distributed under the MIT license. See LICENSE file for details. * * All rights reserved. */ /** * @name PathItem * * @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 with * @param {Function} [include] a callback function that can be used to * filter out undesired locations right while they are collected. When * defined, it shall return {@true to include a location}. * @return {CurveLocation[]} the locations of all intersection between the * paths * @see #getCrossings(path) * @example {@paperscript} // Finding the intersections between two paths * 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(1); * * 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', * parent: intersectionGroup * }); * } * } */ getIntersections: function(path, include, _matrix, _returnFirst) { // NOTE: For self-intersection, path is null. This means you can also // just call path.getIntersections() without an argument to get self // intersections. // NOTE: The hidden argument _matrix is used internally to override the // passed path's transformation matrix. var self = this === path || !path, // self-intersections? matrix1 = this._matrix.orNullIfIdentity(), matrix2 = self ? matrix1 : (_matrix || path._matrix).orNullIfIdentity(); // First check the bounds of the two paths. If they don't intersect, // we don't need to iterate through their curves. if (!self && !this.getBounds(matrix1).touches(path.getBounds(matrix2))) return []; var curves1 = this.getCurves(), curves2 = self ? curves1 : path.getCurves(), length1 = curves1.length, length2 = self ? length1 : curves2.length, values2 = [], arrays = [], locations, path; // Cache values for curves2 as we re-iterate them for each in curves1. for (var i = 0; i < length2; i++) values2[i] = curves2[i].getValues(matrix2); for (var i = 0; i < length1; i++) { var curve1 = curves1[i], values1 = self ? values2[i] : curve1.getValues(matrix1), path1 = curve1.getPath(); // NOTE: Due to the nature of Curve._getIntersections(), we need to // use separate location arrays per path1, to make sure the // circularity checks are not getting confused by locations on // separate paths. We are flattening the separate arrays at the end. if (path1 !== path) { path = path1; locations = []; arrays.push(locations); } if (self) { // First check for self-intersections within the same curve. Curve._getSelfIntersection(values1, curve1, locations, { include: include, // Only possible if there is only one closed curve: startConnected: length1 === 1 && curve1.getPoint1().equals(curve1.getPoint2()) }); } // Check for intersections with other curves. For self intersection, // we can start at i + 1 instead of 0 for (var j = self ? i + 1 : 0; j < length2; j++) { // There might be already one location from the above // self-intersection check: if (_returnFirst && locations.length) return locations; var curve2 = curves2[j]; // Avoid end point intersections on consecutive curves when // self intersecting. Curve._getIntersections( values1, values2[j], curve1, curve2, locations, { include: include, // Do not compare indices here to determine connection, // since one array of curves can contain curves from // separate sup-paths of a compound path. startConnected: self && curve1.getPrevious() === curve2, endConnected: self && curve1.getNext() === curve2 } ); } } // Now flatten the list of location arrays to one array and return it. locations = []; for (var i = 0, l = arrays.length; i < l; i++) { locations.push.apply(locations, arrays[i]); } return locations; }, /** * Returns all crossings between two {@link PathItem} items as an array of * {@link CurveLocation} objects. {@link CompoundPath} items are also * supported. Crossings are intersections where the paths actually are * crossing each other, as opposed to simply touching. * * @param {PathItem} path the other item to find the crossings with * @param {Boolean} includeOverlaps whether to also count overlaps as * crossings * @see #getIntersections(path) */ getCrossings: function(path, includeOverlaps) { return this.getIntersections(path, function(inter) { // Check overlap first since it's the cheaper test between the two. return includeOverlaps && inter.isOverlap() || inter.isCrossing(); }); }, _asPathItem: function() { // See Item#_asPathItem() return this; }, /** * The path's geometry, formatted as SVG style path data. * * @name PathItem#getPathData * @type String * @bean */ setPathData: function(data) { // NOTE: #getPathData() is defined in CompoundPath / Path // This is a very compact SVG Path Data parser that works both for Path // and CompoundPath. // 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, previous, control, current = new Point(), start = new Point(); function getCoord(index, coord) { var val = +coords[index]; if (relative) val += current[coord]; return val; } function getPoint(index) { return new Point( getCoord(index, 'x'), getCoord(index + 1, 'y') ); } // First clear the previous content this.clear(); for (var i = 0, l = parts && parts.length; i < l; i++) { var part = parts[i], command = part[0], lower = command.toLowerCase(); // Match all coordinate values coords = part.match(/[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g); var length = coords && coords.length; relative = command === lower; if (previous === 'z' && !/[mz]/.test(lower)) this.moveTo(current = start); switch (lower) { case 'm': case 'l': var move = lower === 'm'; for (var j = 0; j < length; j += 2) this[j === 0 && move ? 'moveTo' : 'lineTo']( current = getPoint(j)); control = current; if (move) start = current; break; case 'h': case 'v': var coord = lower === 'h' ? 'x' : 'y'; for (var j = 0; j < length; j++) { current[coord] = getCoord(j, coord); this.lineTo(current); } control = current; break; case 'c': for (var j = 0; j < length; j += 6) { this.cubicCurveTo( getPoint(j), control = getPoint(j + 2), current = getPoint(j + 4)); } break; case 's': // Smooth cubicCurveTo for (var j = 0; j < length; j += 4) { this.cubicCurveTo( /[cs]/.test(previous) ? current.multiply(2).subtract(control) : current, control = getPoint(j), current = getPoint(j + 2)); previous = lower; } break; case 'q': for (var j = 0; j < length; j += 4) { this.quadraticCurveTo( control = getPoint(j), current = getPoint(j + 2)); } break; case 't': // Smooth quadraticCurveTo for (var j = 0; j < length; j += 2) { this.quadraticCurveTo( control = (/[qt]/.test(previous) ? current.multiply(2).subtract(control) : current), current = getPoint(j)); previous = lower; } break; case 'a': for (var j = 0; j < length; j += 7) { this.arcTo(current = getPoint(j + 5), new Size(+coords[j], +coords[j + 1]), +coords[j + 2], +coords[j + 4], +coords[j + 3]); } break; case 'z': this.closePath(true); break; } previous = lower; } }, _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()); }, _contains: function(point) { // NOTE: point is reverse transformed by _matrix, so we don't need to // apply here. /*#*/ if (__options.nativeContains || !__options.booleanOperations) { // To compare with native canvas approach: var ctx = CanvasProvider.getContext(1, 1); // 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.getFillRule()); CanvasProvider.release(ctx); return res; /*#*/ } else { // !__options.nativeContains && __options.booleanOperations // Check the transformed point against the untransformed (internal) // handle bounds, which is the fastest rough bounding box to calculate // for a quick check before calculating the actual winding. var winding = point.isInside(this.getInternalHandleBounds()) && this._getWinding(point); return !!(this.getFillRule() === 'evenodd' ? winding & 1 : winding); /*#*/ } // !__options.nativeContains && __options.booleanOperations } /** * 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} * * 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 */ // 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 */ // 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 `through` point, to the specified `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 `through` point, to the specified `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 * @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); * } */ // DOCS: PathItem#arcTo(to, radius, rotation, clockwise, large) /** * Closes the path. When closed, Paper.js connects the first and last * segment of the path with an additional curve. * * @name PathItem#closePath * @function * @param {Boolean} join controls whether the method should attempt to merge * the first segment with the last if they lie in the same location * @see Path#closed */ /** * {@grouptitle Relative Drawing Commands} * * If called on a {@link CompoundPath}, a new {@link Path} is created as a * 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; */ // DOCS: Document Path#curveBy() /** * @name PathItem#curveBy * @function * @param {Point} through * @param {Point} to * @param {Number} [parameter=0.5] */ // DOCS: Document Path#cubicCurveBy() /** * @name PathItem#cubicCurveBy * @function * @param {Point} handle1 * @param {Point} handle2 * @param {Point} to */ // DOCS: Document Path#quadraticCurveBy() /** * @name PathItem#quadraticCurveBy * @function * @param {Point} handle * @param {Point} to */ // DOCS: Document Path#arcBy(through, to) /** * @name PathItem#arcBy * @function * @param {Point} through * @param {Point} to */ // DOCS: Document Path#arcBy(to, clockwise) /** * @name PathItem#arcBy * @function * @param {Point} to * @param {Boolean} [clockwise=true] */ });