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] 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