From bf4eb47fae1e15177ff9b5068abf77d4c41cfef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 6 Jan 2016 16:11:19 +0100 Subject: [PATCH 1/6] Start implementing new smooth() functions that merge all approaches. Work in progress, needs more work on range handling for 'continous', and docs. --- src/path/CompoundPath.js | 13 +- src/path/Path.js | 300 +++++++++++++++++++++++---------------- src/path/PathItem.js | 52 ------- src/path/Segment.js | 72 ++++++++++ 4 files changed, 253 insertions(+), 184 deletions(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 440b4000..dfeffc4a 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -135,12 +135,6 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ children[i].reverse(); }, - smooth: function() { - var children = this._children; - for (var i = 0, l = children.length; i < l; i++) - children[i].smooth(); - }, - // DOCS: reduce() // TEST: reduce() reduce: function reduce(options) { @@ -160,6 +154,13 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ return reduce.base.call(this); }, + // TODO: Docs + smooth: function(options) { + var children = this._children; + for (var i = 0, l = children.length; i < l; i++) + children[i].smooth(options); + }, + /** * Specifies whether the compound path is oriented clock-wise. * diff --git a/src/path/Path.js b/src/path/Path.js index d41f3084..d3633dcd 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2060,9 +2060,183 @@ var Path = PathItem.extend(/** @lends Path# */{ */ getNearestPoint: function(/* point */) { return this.getNearestLocation.apply(this, arguments).getPoint(); + }, + + /** + * TODO: continuous: + * Smooths the path by adjusting its curve handles so that the first and + * second derivatives of all involved curves are continuous across their + * boundaries. + */ + /** + * Smooths the path without changing the amount of segments in the path + * or moving their locations, by only smoothing and adjusting the angle and + * length of their handles. + * This works for open paths as well as closed paths. + * + * @param {Object} [options] TODO + * TODO: controls the amount of smoothing as a factor by which to scale each + * handle. + * + * @see Segment#smooth(options) + * + * @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(); + */ + smooth: function(options) { + function getIndex(value, _default) { + return value == null + ? _default + : typeof value === 'number' + ? value + : value.getIndex + ? value.getIndex() + : _default; + } + + var opts = options || {}, + type = opts.type, + segments = this._segments, + length = segments.length, + from = getIndex(opts.from, 0), + to = getIndex(opts.to, length - 1); + + if (!type || type === 'continuous') { + // Continuous smoothing approach based on work by Lubos Brieda, + // Particle In Cell Consulting LLC, but further simplified by + // addressing handle symmetry across segments, and the possibility + // to process x and y coordinates simultaneously. Also added + // handling of closed paths. + // https://www.particleincell.com/2012/bezier-splines/ + var closed = this._closed, + n = length - 1, + // Add overlapping ends for closed paths. + overlap = 0; + if (length <= 2) + return; + if (closed) { + // Overlap by up to 4 points since a current segment is affected + // by 4 neighbors. + overlap = Math.min(length, 4); + n += Math.min(length, overlap) * 2; + } + var knots = []; + for (var i = 0; i < length; i++) + knots[i + overlap] = segments[i]._point; + if (closed) { + // Add the last points again at the beginning, and the first + // ones at the end. + for (var i = 0; i < overlap; i++) { + knots[i] = knots[i + length]; + knots[i + length + overlap] = knots[i + overlap]; + } + } + + // Right-hand side vectors, with left most segment added + var a = [0], + b = [2], + c = [1], + rx = [knots[0]._x + 2 * knots[1]._x], + ry = [knots[0]._y + 2 * knots[1]._y], + n_1 = n - 1; + + // Internal segments + for (var i = 1; i < n_1; i++) { + a[i] = 1; + b[i] = 4; + c[i] = 1; + rx[i] = 4 * knots[i]._x + 2 * knots[i + 1]._x; + ry[i] = 4 * knots[i]._y + 2 * knots[i + 1]._y; + } + + // Right segment + a[n_1] = 2; + b[n_1] = 7; + c[n_1] = 0; + rx[n_1] = 8 * knots[n_1]._x + knots[n]._x; + ry[n_1] = 8 * knots[n_1]._y + knots[n]._y; + + // Solve Ax = b with the Thomas algorithm (from Wikipedia) + for (var i = 1, j = 0; i < n; i++, j++) { + var m = a[i] / b[j]; + b[i] = b[i] - m * c[j]; + rx[i] = rx[i] - m * rx[j]; + ry[i] = ry[i] - m * ry[j]; + } + + var px = [], + py = []; + + px[n_1] = rx[n_1] / b[n_1]; + py[n_1] = ry[n_1] / b[n_1]; + for (var i = n - 2; i >= 0; i--) { + px[i] = (rx[i] - c[i] * px[i + 1]) / b[i]; + py[i] = (ry[i] - c[i] * py[i + 1]) / b[i]; + } + px[n] = (3 * knots[n]._x - px[n_1]) / 2; + py[n] = (3 * knots[n]._y - py[n_1]) / 2; + + // Now update the segments + n -= overlap; + for (var i = overlap; i <= n; i++) { + var segment = segments[i - overlap], + pt = segment._point, + hx = px[i] - pt._x, + hy = py[i] - pt._y; + if (closed || i < n) + segment.setHandleOut(hx, hy); + if (closed || i > 0) + segment.setHandleIn(-hx, -hy); + } + } else { + // AlL other smoothing methods are handled directly on the segments: + for (var i = from; i <= to; i++) + segments[i].smooth(opts); + } } }), new function() { // Scope for drawing + // Note that in the code below we're often accessing _x and _y on point // objects that were read from segments. This is because the SegmentPoint // class overrides the plain x / y properties with getter / setters and @@ -2262,132 +2436,6 @@ new function() { // Scope for drawing } }; }, -new function() { // Path Smoothing - /** - * Solves a tri-diagonal system for one of coordinates (x or y) of first - * bezier control points. - * - * @param rhs right hand side vector - * @return Solution vector - */ - function getFirstControlPoints(rhs) { - var n = rhs.length, - x = [], // Solution vector. - tmp = [], // Temporary workspace. - b = 2; - x[0] = rhs[0] / b; - // Decomposition and forward substitution. - for (var i = 1; i < n; i++) { - tmp[i] = 1 / b; - b = (i < n - 1 ? 4 : 2) - tmp[i]; - x[i] = (rhs[i] - x[i - 1]) / b; - } - // Back-substitution. - for (var i = 1; i < n; i++) { - x[n - i - 1] -= tmp[n - i] * x[n - i]; - } - return x; - } - - return { - // NOTE: Documentation for smooth() is in PathItem - smooth: function() { - // This code is based on the work by Oleg V. Polikarpotchkin, - // http://ov-p.spaces.live.com/blog/cns!39D56F0C7A08D703!147.entry - // It was extended to support closed paths by averaging overlapping - // beginnings and ends. The result of this approach is very close to - // Polikarpotchkin's closed curve solution, but reuses the same - // algorithm as for open paths, and is probably executing faster as - // well, so it is preferred. - var segments = this._segments, - size = segments.length, - closed = this._closed, - n = size, - // Add overlapping ends for averaging handles in closed paths - overlap = 0; - if (size <= 2) - return; - if (closed) { - // Overlap up to 4 points since averaging beziers affect the 4 - // neighboring points - overlap = Math.min(size, 4); - n += Math.min(size, overlap) * 2; - } - var knots = []; - for (var i = 0; i < size; i++) - knots[i + overlap] = segments[i]._point; - if (closed) { - // If we're averaging, add the 4 last points again at the - // beginning, and the 4 first ones at the end. - for (var i = 0; i < overlap; i++) { - knots[i] = segments[i + size - overlap]._point; - knots[i + size + overlap] = segments[i]._point; - } - } else { - n--; - } - // Calculate first Bezier control points - // Right hand side vector - var rhs = []; - - // Set right hand side X values - for (var i = 1; i < n - 1; i++) - rhs[i] = 4 * knots[i]._x + 2 * knots[i + 1]._x; - rhs[0] = knots[0]._x + 2 * knots[1]._x; - rhs[n - 1] = 3 * knots[n - 1]._x; - // Get first control points X-values - var x = getFirstControlPoints(rhs); - - // Set right hand side Y values - for (var i = 1; i < n - 1; i++) - rhs[i] = 4 * knots[i]._y + 2 * knots[i + 1]._y; - rhs[0] = knots[0]._y + 2 * knots[1]._y; - rhs[n - 1] = 3 * knots[n - 1]._y; - // Get first control points Y-values - var y = getFirstControlPoints(rhs); - - if (closed) { - // Do the actual averaging simply by linearly fading between the - // overlapping values. - for (var i = 0, j = size; i < overlap; i++, j++) { - var f1 = i / overlap, - f2 = 1 - f1, - ie = i + overlap, - je = j + overlap; - // Beginning - x[j] = x[i] * f1 + x[j] * f2; - y[j] = y[i] * f1 + y[j] * f2; - // End - x[je] = x[ie] * f2 + x[je] * f1; - y[je] = y[ie] * f2 + y[je] * f1; - } - n--; - } - var handleIn = null; - // Now set the calculated handles - for (var i = overlap; i <= n - overlap; i++) { - var segment = segments[i - overlap]; - if (handleIn) - segment.setHandleIn(handleIn.subtract(segment._point)); - if (i < n) { - segment.setHandleOut( - new Point(x[i], y[i]).subtract(segment._point)); - handleIn = i < n - 1 - ? new Point( - 2 * knots[i + 1]._x - x[i + 1], - 2 * knots[i + 1]._y - y[i + 1]) - : new Point( - (knots[n]._x + x[n - 1]) / 2, - (knots[n]._y + y[n - 1]) / 2); - } - } - if (closed && handleIn) { - var segment = this._segments[0]; - segment.setHandleIn(handleIn.subtract(segment._point)); - } - } - }; -}, new function() { // PostScript-style drawing commands /** * Helper method that returns the current segment and checks if a moveTo() diff --git a/src/path/PathItem.js b/src/path/PathItem.js index f10e22aa..04c4e638 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -314,58 +314,6 @@ var PathItem = Item.extend(/** @lends PathItem# */{ /*#*/ } // !__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} * diff --git a/src/path/Segment.js b/src/path/Segment.js index 206b5c89..fbd0dd48 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -378,6 +378,78 @@ var Segment = Base.extend(/** @lends Segment# */{ || this._path._closed && segments[0]) || null; }, + /** + * Smooths the bezier curves that pass through this segment without moving + * its point, by taking into its distance to the neighboring segments and + * changing the direction and length of the segment's handles accordingly. + * + * @param {Object} [options] TODO + * TODO: controls the amount of smoothing as a factor by which to scale each + * handle. + * + * @see PathItem#smooth(options) + */ + smooth: function(options) { + var opts = options || {}, + type = opts.type, + factor = opts.factor, + prev = this.getPrevious(), + next = this.getNext(), + // Some precalculations valid for both 'catmull-rom' and 'geometric' + p0 = (prev || this)._point, + p1 = this._point, + p2 = (next || this)._point, + d1 = p0.getDistance(p1), + d2 = p1.getDistance(p2); + if (!type || type === 'catmull-rom') { + // Implementation of by Catmull-Rom splines with factor parameter + // based on work by @nicholaswmin: + // https://github.com/nicholaswmin/VectorTests + // Using these factor values produces different types of splines: + // 0.0: the standard, uniform Catmull-Rom spline + // 0.5: the centripetal Catmull-Rom spline, guaranteeing no self- + // intersections + // 1.0: the chordal Catmull-Rom spline. + var alpha = factor === undefined ? 0.5 : factor, + d1_a = Math.pow(d1, alpha), + d1_2a = d1_a * d1_a, + d2_a = Math.pow(d2, alpha), + d2_2a = d2_a * d2_a; + if (prev) { + var A = 2 * d2_2a + 3 * d2_a * d1_a + d1_2a, + N = 3 * d2_a * (d2_a + d1_a); + this.setHandleIn(N !== 0 + ? new Point( + (d2_2a * p0._x + A * p1._x - d1_2a * p2._x) / N - p1._x, + (d2_2a * p0._y + A * p1._y - d1_2a * p2._y) / N - p1._y) + : new Point()); + } + if (next) { + var A = 2 * d1_2a + 3 * d1_a * d2_a + d2_2a, + N = 3 * d1_a * (d1_a + d2_a); + this.setHandleOut(N !== 0 + ? new Point( + (d1_2a * p2._x + A * p1._x - d2_2a * p0._x) / N - p1._x, + (d1_2a * p2._y + A * p1._y - d2_2a * p0._y) / N - p1._y) + : new Point()); + } + } else if (type === 'geometric') { + // Geometric smoothing approach based on: + // http://www.antigrain.com/research/bezier_interpolation/ + // http://scaledinnovation.com/analytics/splines/aboutSplines.html + // http://bseth99.github.io/projects/animate/2-bezier-curves.html + if (prev && next) { + var vector = p0.subtract(p2), + t = factor === undefined ? 0.4 : factor, + k = t * d1 / (d1 + d2); + this.setHandleIn(vector.multiply(k)); + this.setHandleOut(vector.multiply(k - t)); + } + } else { + throw new Error('Smoothing method \'' + type + '\' not supported.'); + } + }, + /** * The previous segment in the {@link Path#segments} array that the * segment belongs to. If the segments belongs to a closed path, the last From 71c7405d6bf7748cdc9dce5bf78091d58275ddc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 6 Jan 2016 17:16:04 +0100 Subject: [PATCH 2/6] Some work on documentation structure for #smooth(). --- src/path/CompoundPath.js | 2 +- src/path/Path.js | 68 +++------------------------------------- src/path/PathItem.js | 64 +++++++++++++++++++++++++++++++++++++ src/path/Segment.js | 8 ++--- 4 files changed, 74 insertions(+), 68 deletions(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index dfeffc4a..e0dd3753 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -154,7 +154,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ return reduce.base.call(this); }, - // TODO: Docs + // NOTE: Documentation is in PathItem.js smooth: function(options) { var children = this._children; for (var i = 0, l = children.length; i < l; i++) diff --git a/src/path/Path.js b/src/path/Path.js index d3633dcd..20e2fc68 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2062,68 +2062,10 @@ var Path = PathItem.extend(/** @lends Path# */{ return this.getNearestLocation.apply(this, arguments).getPoint(); }, - /** - * TODO: continuous: - * Smooths the path by adjusting its curve handles so that the first and - * second derivatives of all involved curves are continuous across their - * boundaries. - */ - /** - * Smooths the path without changing the amount of segments in the path - * or moving their locations, by only smoothing and adjusting the angle and - * length of their handles. - * This works for open paths as well as closed paths. - * - * @param {Object} [options] TODO - * TODO: controls the amount of smoothing as a factor by which to scale each - * handle. - * - * @see Segment#smooth(options) - * - * @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(); - */ + // NOTE: Documentation is in PathItem.js smooth: function(options) { + // Helper method to pick the right from / to indices. + // Supports numbers and segment objects. function getIndex(value, _default) { return value == null ? _default @@ -2172,7 +2114,7 @@ var Path = PathItem.extend(/** @lends Path# */{ } } - // Right-hand side vectors, with left most segment added + // Right-hand side vectors, with left most segment added. var a = [0], b = [2], c = [1], @@ -2229,7 +2171,7 @@ var Path = PathItem.extend(/** @lends Path# */{ segment.setHandleIn(-hx, -hy); } } else { - // AlL other smoothing methods are handled directly on the segments: + // All other smoothing methods are handled directly on the segments: for (var i = from; i <= to; i++) segments[i].smooth(opts); } diff --git a/src/path/PathItem.js b/src/path/PathItem.js index 04c4e638..ebec0e34 100644 --- a/src/path/PathItem.js +++ b/src/path/PathItem.js @@ -314,6 +314,70 @@ var PathItem = Item.extend(/** @lends PathItem# */{ /*#*/ } // !__options.nativeContains && __options.booleanOperations } + /** + * TODO: continuous: + * Smooths the path item by adjusting its curve handles so that the first + * and second derivatives of all involved curves are continuous across their + * boundaries. + */ + /** + * Smooths the path item without changing the amount of segments in the path + * or moving their locations, by only smoothing and adjusting the angle and + * length of their handles. + * This works for open paths as well as closed paths. + * + * @name PathItem#smooth + * @function + * @param {Object} [options] TODO + * TODO: controls the amount of smoothing as a factor by which to scale each + * handle. + * + * @see Segment#smooth(options) + * + * @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} * diff --git a/src/path/Segment.js b/src/path/Segment.js index fbd0dd48..018e12dc 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -405,15 +405,15 @@ var Segment = Base.extend(/** @lends Segment# */{ // Implementation of by Catmull-Rom splines with factor parameter // based on work by @nicholaswmin: // https://github.com/nicholaswmin/VectorTests - // Using these factor values produces different types of splines: + // Using these factors produces different types of splines: // 0.0: the standard, uniform Catmull-Rom spline // 0.5: the centripetal Catmull-Rom spline, guaranteeing no self- // intersections // 1.0: the chordal Catmull-Rom spline. - var alpha = factor === undefined ? 0.5 : factor, - d1_a = Math.pow(d1, alpha), + var a = factor === undefined ? 0.5 : factor, + d1_a = Math.pow(d1, a), d1_2a = d1_a * d1_a, - d2_a = Math.pow(d2, alpha), + d2_a = Math.pow(d2, a), d2_2a = d2_a * d2_a; if (prev) { var A = 2 * d2_2a + 3 * d2_a * d1_a + d1_2a, From 5f11345fc95b3c26f9bdc1aa22bff63b463ec533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 6 Jan 2016 22:31:02 +0100 Subject: [PATCH 3/6] Implement from / to options for 'continuous' smooth(). --- src/path/Path.js | 83 ++++++++++++++++++++++++++++----------------- src/path/Segment.js | 17 ++++++---- 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/path/Path.js b/src/path/Path.js index 20e2fc68..3516419f 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2066,23 +2066,41 @@ var Path = PathItem.extend(/** @lends Path# */{ smooth: function(options) { // Helper method to pick the right from / to indices. // Supports numbers and segment objects. + // For numbers, the `to` index is exclusive, while for segments and + // curves, it is inclusive, handled by the `offset` parameter. function getIndex(value, _default) { - return value == null + var index = value == null ? _default : typeof value === 'number' ? value : value.getIndex ? value.getIndex() : _default; + // Handle negative values based on whether a path is open or not: + // Closed paths are wrapped around the end (allowing values to be + // negative), while open paths stay within the open range. + return index < 0 && closed + ? index % length + : (Math.min(index, length - 1) % length + length) % length; } var opts = options || {}, type = opts.type, segments = this._segments, length = segments.length, + range = opts.from !== undefined || opts.to !== undefined, from = getIndex(opts.from, 0), - to = getIndex(opts.to, length - 1); - + to = getIndex(opts.to, length - 1), + closed = this._closed; + if (from > to) { + if (closed) { + from -= length; + } else { + var tmp = from; + from = to; + to = tmp; + } + } if (!type || type === 'continuous') { // Continuous smoothing approach based on work by Lubos Brieda, // Particle In Cell Consulting LLC, but further simplified by @@ -2090,28 +2108,28 @@ var Path = PathItem.extend(/** @lends Path# */{ // to process x and y coordinates simultaneously. Also added // handling of closed paths. // https://www.particleincell.com/2012/bezier-splines/ - var closed = this._closed, - n = length - 1, - // Add overlapping ends for closed paths. - overlap = 0; - if (length <= 2) - return; - if (closed) { - // Overlap by up to 4 points since a current segment is affected - // by 4 neighbors. - overlap = Math.min(length, 4); - n += Math.min(length, overlap) * 2; + var min = Math.min, + amount = to - from + 1, + n = amount - 1; + // Overlap by up to 4 points on closed paths since a current segment + // is affected by its 4 neighbors on both sides (?). + var loop = closed && !range, + padding = loop ? min(amount, 4) : 1, + paddingLeft = padding, + paddingRight = padding, + knots = []; + if (!closed) { + // If the path is open and a range is defined, try using a + // padding of 1 on either side. + paddingLeft = min(1, from); + paddingRight = min(1, length - to - 1); } - var knots = []; - for (var i = 0; i < length; i++) - knots[i + overlap] = segments[i]._point; - if (closed) { - // Add the last points again at the beginning, and the first - // ones at the end. - for (var i = 0; i < overlap; i++) { - knots[i] = knots[i + length]; - knots[i + length + overlap] = knots[i + overlap]; - } + // Set up the knots array now, taking the paddings into account. + n += paddingLeft + paddingRight; + if (n <= 1) + return; + for (var i = 0, j = from - paddingLeft; i <= n; i++, j++) { + knots[i] = segments[(j < 0 ? j + length : j) % length]._point; } // Right-hand side vectors, with left most segment added. @@ -2148,7 +2166,6 @@ var Path = PathItem.extend(/** @lends Path# */{ var px = [], py = []; - px[n_1] = rx[n_1] / b[n_1]; py[n_1] = ry[n_1] / b[n_1]; for (var i = n - 2; i >= 0; i--) { @@ -2159,21 +2176,23 @@ var Path = PathItem.extend(/** @lends Path# */{ py[n] = (3 * knots[n]._y - py[n_1]) / 2; // Now update the segments - n -= overlap; - for (var i = overlap; i <= n; i++) { - var segment = segments[i - overlap], + for (var i = paddingLeft, max = n - paddingRight, j = from; + i <= max; i++, j++) { + var segment = segments[j < 0 ? j + length : j], pt = segment._point, hx = px[i] - pt._x, hy = py[i] - pt._y; - if (closed || i < n) + if (loop || i < max) segment.setHandleOut(hx, hy); - if (closed || i > 0) + if (loop || i > paddingLeft) segment.setHandleIn(-hx, -hy); } } else { // All other smoothing methods are handled directly on the segments: - for (var i = from; i <= to; i++) - segments[i].smooth(opts); + for (var i = from; i <= to; i++) { + segments[i < 0 ? i + length : i].smooth(opts, + i === from, i === to); + } } } }), diff --git a/src/path/Segment.js b/src/path/Segment.js index 018e12dc..cc1aed48 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -389,7 +389,8 @@ var Segment = Base.extend(/** @lends Segment# */{ * * @see PathItem#smooth(options) */ - smooth: function(options) { + smooth: function(options, _first, _last) { + // _first = _last = false; var opts = options || {}, type = opts.type, factor = opts.factor, @@ -408,14 +409,14 @@ var Segment = Base.extend(/** @lends Segment# */{ // Using these factors produces different types of splines: // 0.0: the standard, uniform Catmull-Rom spline // 0.5: the centripetal Catmull-Rom spline, guaranteeing no self- - // intersections - // 1.0: the chordal Catmull-Rom spline. + // intersections + // 1.0: the chordal Catmull-Rom spline var a = factor === undefined ? 0.5 : factor, d1_a = Math.pow(d1, a), d1_2a = d1_a * d1_a, d2_a = Math.pow(d2, a), d2_2a = d2_a * d2_a; - if (prev) { + if (!_first && prev) { var A = 2 * d2_2a + 3 * d2_a * d1_a + d1_2a, N = 3 * d2_a * (d2_a + d1_a); this.setHandleIn(N !== 0 @@ -424,7 +425,7 @@ var Segment = Base.extend(/** @lends Segment# */{ (d2_2a * p0._y + A * p1._y - d1_2a * p2._y) / N - p1._y) : new Point()); } - if (next) { + if (!_last && next) { var A = 2 * d1_2a + 3 * d1_a * d2_a + d2_2a, N = 3 * d1_a * (d1_a + d2_a); this.setHandleOut(N !== 0 @@ -442,8 +443,10 @@ var Segment = Base.extend(/** @lends Segment# */{ var vector = p0.subtract(p2), t = factor === undefined ? 0.4 : factor, k = t * d1 / (d1 + d2); - this.setHandleIn(vector.multiply(k)); - this.setHandleOut(vector.multiply(k - t)); + if (!_first) + this.setHandleIn(vector.multiply(k)); + if (!_last) + this.setHandleOut(vector.multiply(k - t)); } } else { throw new Error('Smoothing method \'' + type + '\' not supported.'); From 7ad6dc2d5fb8d0f4e4aae4b9cb8097a915fe98f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Wed, 6 Jan 2016 22:37:51 +0100 Subject: [PATCH 4/6] Add support for Curve objects in smooth() ranges. --- src/path/Path.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/path/Path.js b/src/path/Path.js index 3516419f..9f43088a 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2073,8 +2073,10 @@ var Path = PathItem.extend(/** @lends Path# */{ ? _default : typeof value === 'number' ? value + // Support both Segment and Curve through getIndex() : value.getIndex ? value.getIndex() + + (_default && value instanceof Curve ? 1 : 0) : _default; // Handle negative values based on whether a path is open or not: // Closed paths are wrapped around the end (allowing values to be From 86b9d04c4374df4746471a706a18c0a61628cb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 7 Jan 2016 11:29:36 +0100 Subject: [PATCH 5/6] Rename from, to parameters to start, end in methods where the end is exclusive. Relates to #338 --- src/item/Item.js | 16 ++++++++-------- src/path/Path.js | 44 ++++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index a39d7e48..70d7bb09 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -2442,24 +2442,24 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * @return {Item[]} an array containing the removed items */ /** - * Removes the children from the specified `from` index to the `to` index - * from the parent's {@link #children} array. + * Removes the children from the specified `start` index to and excluding + * the `end` index from the parent's {@link #children} array. * * @name Item#removeChildren * @function - * @param {Number} from the beginning index, inclusive - * @param {Number} [to=children.length] the ending index, exclusive + * @param {Number} start the beginning index, inclusive + * @param {Number} [end=children.length] the ending index, exclusive * @return {Item[]} an array containing the removed items */ - removeChildren: function(from, to) { + removeChildren: function(start, end) { if (!this._children) return null; - from = from || 0; - to = Base.pick(to, this._children.length); + start = start || 0; + end = Base.pick(end, this._children.length); // Use Base.splice(), which adjusts #_index for the items above, and // deletes it for the removed items. Calling #_remove() afterwards is // fine, since it only calls Base.splice() if #_index is set. - var removed = Base.splice(this._children, null, from, to - from); + var removed = Base.splice(this._children, null, start, end - start); for (var i = removed.length - 1; i >= 0; i--) { // Don't notify parent each time, notify it separately after. removed[i]._remove(true, false); diff --git a/src/path/Path.js b/src/path/Path.js index 9f43088a..51f6ca0c 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -417,21 +417,21 @@ var Path = PathItem.extend(/** @lends Path# */{ var total = this._countCurves(), // If we're adding a new segment to the end of an open path, // we need to step one index down to get its curve. - from = index > 0 && index + amount - 1 === total ? index - 1 + start = index > 0 && index + amount - 1 === total ? index - 1 : index, - start = from, - to = Math.min(from + amount, total); + insert = start, + end = Math.min(start + amount, total); if (segs._curves) { // Reuse removed curves. - curves.splice.apply(curves, [from, 0].concat(segs._curves)); - start += segs._curves.length; + curves.splice.apply(curves, [start, 0].concat(segs._curves)); + insert += segs._curves.length; } // Insert new curves, but do not initialize their segments yet, // since #_adjustCurves() handles all that for us. - for (var i = start; i < to; i++) + for (var i = insert; i < end; i++) curves.splice(i, 0, new Curve(this, null, null)); // Adjust segments for the curves before and after the removed ones - this._adjustCurves(from, to); + this._adjustCurves(start, end); } // Use SEGMENTS notification instead of GEOMETRY since curves are kept // up-to-date by _adjustCurves() and don't need notification. @@ -442,11 +442,11 @@ var Path = PathItem.extend(/** @lends Path# */{ /** * Adjusts segments of curves before and after inserted / removed segments. */ - _adjustCurves: function(from, to) { + _adjustCurves: function(start, end) { var segments = this._segments, curves = this._curves, curve; - for (var i = from; i < to; i++) { + for (var i = start; i < end; i++) { curve = curves[i]; curve._path = this; curve._segment1 = segments[i]; @@ -455,14 +455,14 @@ var Path = PathItem.extend(/** @lends Path# */{ } // If it's the first segment, correct the last segment of closed // paths too: - if (curve = curves[this._closed && from === 0 ? segments.length - 1 - : from - 1]) { - curve._segment2 = segments[from] || segments[0]; + if (curve = curves[this._closed && start === 0 ? segments.length - 1 + : start - 1]) { + curve._segment2 = segments[start] || segments[0]; curve._changed(); } // Fix the segment after the modified range, if it exists - if (curve = curves[to]) { - curve._segment1 = segments[to]; + if (curve = curves[end]) { + curve._segment1 = segments[end]; curve._changed(); } }, @@ -725,13 +725,13 @@ var Path = PathItem.extend(/** @lends Path# */{ * // Select the path, so we can see its segments: * path.selected = true; */ - removeSegments: function(from, to, _includeCurves) { - from = from || 0; - to = Base.pick(to, this._segments.length); + removeSegments: function(start, end, _includeCurves) { + start = start || 0; + end = Base.pick(end, this._segments.length); var segments = this._segments, curves = this._curves, count = segments.length, // segment count before removal - removed = segments.splice(from, to - from), + removed = segments.splice(start, end - start), amount = removed.length; if (!amount) return removed; @@ -744,7 +744,7 @@ var Path = PathItem.extend(/** @lends Path# */{ segment._index = segment._path = null; } // Adjust the indices of the segments above. - for (var i = from, l = segments.length; i < l; i++) + for (var i = start, l = segments.length; i < l; i++) segments[i]._index = i; // Keep curves in sync if (curves) { @@ -752,9 +752,9 @@ var Path = PathItem.extend(/** @lends Path# */{ // one to the left of the segment, not to the right, as normally). // Also take into account closed paths, which have one curve more // than segments. - var index = from > 0 && to === count + (this._closed ? 1 : 0) - ? from - 1 - : from, + var index = start > 0 && end === count + (this._closed ? 1 : 0) + ? start - 1 + : start, curves = curves.splice(index, amount); // Unlink the removed curves from the path. for (var i = curves.length - 1; i >= 0; i--) From 4c92c0739ef38d144d0903e3664ef97aae87efcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 11 Jan 2016 20:21:27 +0100 Subject: [PATCH 6/6] Correctly handle negative smooth() indices on open paths. --- src/path/Path.js | 9 +++++---- src/util/Numerical.js | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/path/Path.js b/src/path/Path.js index 51f6ca0c..48a4d5ee 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -2079,11 +2079,12 @@ var Path = PathItem.extend(/** @lends Path# */{ + (_default && value instanceof Curve ? 1 : 0) : _default; // Handle negative values based on whether a path is open or not: - // Closed paths are wrapped around the end (allowing values to be - // negative), while open paths stay within the open range. - return index < 0 && closed + // Ranges on closed paths are allowed to wrapped around the + // beginning/end (e.g. start near the end, end near the beginning), + // while ranges on open paths stay within the path's open range. + return Numerical.clamp(index < 0 && closed ? index % length - : (Math.min(index, length - 1) % length + length) % length; + : index < 0 ? index + length : index, 0, length); } var opts = options || {}, diff --git a/src/util/Numerical.js b/src/util/Numerical.js index 75cec64a..220aa14e 100644 --- a/src/util/Numerical.js +++ b/src/util/Numerical.js @@ -63,7 +63,7 @@ var Numerical = new function() { EPSILON = 1e-12, MACHINE_EPSILON = 1.12e-16; - function clip(value, min, max) { + function clamp(value, min, max) { return value < min ? min : value > max ? max : value; } @@ -133,6 +133,16 @@ var Numerical = new function() { return val >= -EPSILON && val <= EPSILON; }, + /** + * Returns a number whose value is clamped by the given range. + * + * @param {Number} value the value to be clamped + * @param {Number} min the lower boundary of the range + * @param {Number} max the upper boundary of the range + * @return {Number} a number in the range of [min, max] + */ + clamp: clamp, + /** * Gauss-Legendre Numerical Integration. */ @@ -254,10 +264,10 @@ var Numerical = new function() { // We need to include EPSILON in the comparisons with min / max, // as some solutions are ever so lightly out of bounds. if (isFinite(x1) && (min == null || x1 > eMin && x1 < eMax)) - roots[count++] = min == null ? x1 : clip(x1, min, max); + roots[count++] = min == null ? x1 : clamp(x1, min, max); if (x2 !== x1 && isFinite(x2) && (min == null || x2 > eMin && x2 < eMax)) - roots[count++] = min == null ? x2 : clip(x2, min, max); + roots[count++] = min == null ? x2 : clamp(x2, min, max); return count; }, @@ -349,7 +359,7 @@ var Numerical = new function() { var count = Numerical.solveQuadratic(a, b1, c2, roots, min, max); if (isFinite(x) && (count === 0 || x !== roots[count - 1]) && (min == null || x > min - EPSILON && x < max + EPSILON)) - roots[count++] = min == null ? x : clip(x, min, max); + roots[count++] = min == null ? x : clamp(x, min, max); return count; } };