diff --git a/src/path/Curve.js b/src/path/Curve.js index 68524c0c..82d9ca46 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -730,7 +730,15 @@ statics: { : v; }, - isFlatEnough: function(v, tolerance) { + /** + * Determines if a curve is sufficiently flat, meaning it appears as a + * straight line and has curve-time that is enough linear, as specified by + * the given `flatness` parameter. + * + * @param {Number} flatness the maximum error allowed for the straight line + * to deviate from the curve + */ + isFlatEnough: function(v, flatness) { // Thanks to Kaspar Fischer and Roger Willcocks for the following: // http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf var p1x = v[0], p1y = v[1], @@ -742,7 +750,7 @@ statics: { vx = 3 * c2x - 2 * p2x - p1x, vy = 3 * c2y - 2 * p2y - p1y; return Math.max(ux * ux, vx * vx) + Math.max(uy * uy, vy * vy) - < 10 * tolerance * tolerance; + <= 16 * flatness * flatness; }, getArea: function(v) { diff --git a/src/path/Path.js b/src/path/Path.js index 7c697024..3689c03c 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -1202,42 +1202,6 @@ var Path = PathItem.extend(/** @lends Path# */{ return this; }, - reverse: function() { - this._segments.reverse(); - // Reverse the handles: - for (var i = 0, l = this._segments.length; i < l; i++) { - var segment = this._segments[i]; - var handleIn = segment._handleIn; - segment._handleIn = segment._handleOut; - segment._handleOut = handleIn; - segment._index = i; - } - // Clear curves since it all has changed. - this._curves = null; - // Flip clockwise state if it's defined - if (this._clockwise !== undefined) - this._clockwise = !this._clockwise; - this._changed(/*#=*/Change.GEOMETRY); - }, - - flatten: function(maxDistance) { - var iterator = new PathIterator(this, 64, 0.1), - pos = 0, - // Adapt step = maxDistance so the points distribute evenly. - step = iterator.length / Math.ceil(iterator.length / maxDistance), - // Add/remove half of step to end, so imprecisions are ok too. - // For closed paths, remove it, because we don't want to add last - // segment again - end = iterator.length + (this._closed ? -step : step) / 2; - // Iterate over path and evaluate and add points at given offsets - var segments = []; - while (pos <= end) { - segments.push(new Segment(iterator.getPointAt(pos))); - pos += step; - } - this.setSegments(segments); - }, - /** * Reduces the path by removing curves that have a length of 0, * and unnecessary segments between two collinear flat curves. @@ -1261,6 +1225,38 @@ var Path = PathItem.extend(/** @lends Path# */{ return this; }, + // NOTE: Documentation is in PathItem#reverse() + reverse: function() { + this._segments.reverse(); + // Reverse the handles: + for (var i = 0, l = this._segments.length; i < l; i++) { + var segment = this._segments[i]; + var handleIn = segment._handleIn; + segment._handleIn = segment._handleOut; + segment._handleOut = handleIn; + segment._index = i; + } + // Clear curves since it all has changed. + this._curves = null; + // Flip clockwise state if it's defined + if (this._clockwise !== undefined) + this._clockwise = !this._clockwise; + this._changed(/*#=*/Change.GEOMETRY); + }, + + // NOTE: Documentation is in PathItem#flatten() + flatten: function(flatness) { + // Use PathIterator to subdivide the curves into parts that are flat + // enough, as specified by `flatness` / Curve.isFlatEnough(): + var iterator = new PathIterator(this, flatness || 0.25, 256, true), + parts = iterator.parts, + segments = []; + for (var i = 0, l = parts.length; i < l; i++) { + segments.push(new Segment(parts[i].curve.slice(0, 2))); + } + this.setSegments(segments); + }, + // NOTE: Documentation is in PathItem#simplify() simplify: function(tolerance) { var segments = new PathFitter(this).fit(tolerance || 2.5); @@ -2183,7 +2179,7 @@ new function() { // Scope for drawing // Use PathIterator to draw dashed paths: if (!dontStart) ctx.beginPath(); - var iterator = new PathIterator(this, 32, 0.25, + var iterator = new PathIterator(this, 0.25, 32, false, strokeMatrix), length = iterator.length, from = -style.getDashOffset(), to, diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 404b43ff..e4ace31d 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -388,14 +388,14 @@ var PathItem = Item.extend(/** @lends PathItem# */{ */ /** - * Converts the curves in a path to straight lines with an even distribution - * of points. The distance between the produced segments is as close as - * possible to the value specified by the `maxDistance` parameter. + * Flattens the curves in path items to a sequence of straight lines, by + * subdividing them enough times until the specified maximum error is met. * * @name PathItem#flatten * @function * - * @param {Number} maxDistance the maximum distance between the points + * @param {Number} flatness the maximum error between the flattened lines + * and the original curves * * @example {@paperscript} * // Flattening a circle shaped path: @@ -414,8 +414,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{ * var copy = path.clone(); * copy.position.x += 150; * - * // Convert its curves to points, with a max distance of 20: - * copy.flatten(20); + * // Convert its curves to points, with a maximum error of 10: + * copy.flatten(10); */ // TODO: Write about negative indices, and add an example for ranges. diff --git a/src/path/PathIterator.js b/src/path/PathIterator.js index a3424e8c..d2db26b1 100644 --- a/src/path/PathIterator.js +++ b/src/path/PathIterator.js @@ -19,26 +19,34 @@ var PathIterator = Base.extend({ _class: 'PathIterator', /** - * Creates a path iterator for the given path. + * Creates a path iterator for the given path. The iterator converts curves + * into a sequence of straight lines by the use of curve-subdivision with an + * allowed maximum error to create a lookup table that maps curve-time to + * path offsets, and can be used for efficient iteration over the full + * length of the path, and getting points / tangents / normals and curvature + * in path offset space. * - * @param {Path} path the path to iterate over + * @param {Path} path the path to create the iterator for + * @param {Number} [flatness=0.25] the maximum error allowed for the + * straight lines to deviate from the original curves * @param {Number} [maxRecursion=32] the maximum amount of recursion in - * curve subdivision when mapping offsets to curve parameters - * @param {Number} [tolerance=0.25] the error tolerance at which the - * recursion is interrupted before the maximum number of iterations is - * reached + * curve subdivision when mapping offsets to curve parameters + * @param {Boolean} [ignoreStraight=false] if only interested in the result + * of the sub-division (e.g. for path flattening), passing `true` will + * protect straight curves from being subdivided for curve-time + * translation * @param {Matrix} [matrix] the matrix by which to transform the path's - * coordinates without modifying the actual path. + * coordinates without modifying the actual path. * @return {PathIterator} the newly created path iterator */ - initialize: function(path, maxRecursion, tolerance, matrix) { + initialize: function(path, flatness, maxRecursion, ignoreStraight, matrix) { // Instead of relying on path.curves, we only use segments here and // get the curve values from them. var curves = [], // The curve values as returned by getValues() parts = [], // The calculated, subdivided parts of the path length = 0, // The total length of the path // By default, we're not subdividing more than 32 times. - minDifference = 1 / (maxRecursion || 32), + minSpan = 1 / (maxRecursion || 32), segments = path._segments, segment1 = segments[0], segment2; @@ -51,29 +59,31 @@ var PathIterator = Base.extend({ computeParts(curve, segment1._index, 0, 1); } - function computeParts(curve, index, minT, maxT) { + function computeParts(curve, index, t1, t2) { // Check if the t-span is big enough for subdivision. - if ((maxT - minT) > minDifference - // After quite a bit of testing, a default tolerance of 0.25 + if ((t2 - t1) > minSpan + && !(ignoreStraight && Curve.isStraight(curve)) + // After quite a bit of testing, a default flatness of 0.25 // appears to offer a good trade-off between speed and // precision for display purposes. - && !Curve.isFlatEnough(curve, tolerance || 0.25)) { - var split = Curve.subdivide(curve, 0.5), - halfT = (minT + maxT) / 2; + && !Curve.isFlatEnough(curve, flatness || 0.25)) { + var halves = Curve.subdivide(curve, 0.5), + tMid = (t1 + t2) / 2; // Recursively subdivide and compute parts again. - computeParts(split[0], index, minT, halfT); - computeParts(split[1], index, halfT, maxT); + computeParts(halves[0], index, t1, tMid); + computeParts(halves[1], index, tMid, t2); } else { - // Calculate distance between p1 and p2 - var x = curve[6] - curve[0], - y = curve[7] - curve[1], - dist = Math.sqrt(x * x + y * y); - if (dist > /*#=*/Numerical.TOLERANCE) { + // Calculate the length of the curve interpreted as a line. + var dx = curve[6] - curve[0], + dy = curve[7] - curve[1], + dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 0) { length += dist; parts.push({ offset: length, - value: maxT, - index: index + curve: curve, + index: index, + time: t2, }); } } @@ -86,16 +96,15 @@ var PathIterator = Base.extend({ } if (path._closed) addCurve(segment2, segments[0]); - this.curves = curves; this.parts = parts; this.length = length; // Keep a current index from the part where we last where in - // getTimeAt(), to optimise for iterator-like usage of iterator. + // _get(), to optimise for iterator-like usage of iterator. this.index = 0; }, - getTimeAt: function(offset) { + _get: function(offset) { // Make sure we're not beyond the requested offset already. Search the // start position backwards from where to then process the loop below. var i, j = this.index; @@ -116,41 +125,41 @@ var PathIterator = Base.extend({ var prev = this.parts[i - 1]; // Make sure we only use the previous parameter value if its // for the same curve, by checking index. Use 0 otherwise. - var prevVal = prev && prev.index == part.index ? prev.value : 0, - prevLen = prev ? prev.offset : 0; + var prevTime = prev && prev.index === part.index ? prev.time : 0, + prevOffset = prev ? prev.offset : 0; return { + index: part.index, // Interpolate - value: prevVal + (part.value - prevVal) - * (offset - prevLen) / (part.offset - prevLen), - index: part.index + time: prevTime + (part.time - prevTime) + * (offset - prevOffset) / (part.offset - prevOffset) }; } } - // Return last one + // If we're still here, return last one var part = this.parts[this.parts.length - 1]; return { - value: 1, - index: part.index + index: part.index, + time: 1 }; }, drawPart: function(ctx, from, to) { - from = this.getTimeAt(from); - to = this.getTimeAt(to); - for (var i = from.index; i <= to.index; i++) { + var start = this._get(from), + end = this._get(to); + for (var i = start.index, l = end.index; i <= l; i++) { var curve = Curve.getPart(this.curves[i], - i == from.index ? from.value : 0, - i == to.index ? to.value : 1); - if (i == from.index) + i === start.index ? start.time : 0, + i === end.index ? end.time : 1); + if (i === start.index) ctx.moveTo(curve[0], curve[1]); ctx.bezierCurveTo.apply(ctx, curve.slice(2)); } } }, Base.each(Curve._evaluateMethods, function(name) { - this[name + 'At'] = function(offset, weighted) { - var param = this.getTimeAt(offset); - return Curve[name](this.curves[param.index], param.value, weighted); + this[name + 'At'] = function(offset) { + var param = this._get(offset); + return Curve[name](this.curves[param.index], param.time); }; }, {}) ); diff --git a/test/tests/Path.js b/test/tests/Path.js index a392a74b..30aa0a8f 100644 --- a/test/tests/Path.js +++ b/test/tests/Path.js @@ -370,8 +370,12 @@ test('path.curves on closed paths', function() { test('path.flatten(maxDistance)', function() { var path = new Path.Circle(new Size(80, 50), 35); - // Convert its curves to points, with a max distance of 20: - path.flatten(20); + // Convert its curves to points, with a flatness of 5: + path.flatten(5); + + equals(function() { + return path.segments.length; + }, 8, 'Using a flatness of 10, we should end up with 8 segments.'); equals(function() { return path.lastSegment.point.equals(path.firstSegment.point);