diff --git a/src/path/Curve.js b/src/path/Curve.js index 6d8a791c..4d6392d4 100644 --- a/src/path/Curve.js +++ b/src/path/Curve.js @@ -518,8 +518,7 @@ statics: { return values; }, - // TODO: Instead of constants for type, use a "enum" and code substitution. - evaluate: function(v, t, type) { + _evaluate: function(v, t, type, normalized) { // Do not produce results if parameter is out of range or invalid. if (t == null || t < 0 || t > 1) return null; @@ -552,31 +551,45 @@ statics: { // 1: tangent, 1st derivative // 2: normal, 1st derivative // 3: curvature, 1st derivative & 2nd derivative + // Simply use the derivation of the bezier function for both + // the x and y coordinates: // Prevent tangents and normals of length 0: // http://stackoverflow.com/questions/10506868/ - if (t < tolerance && c1x === p1x && c1y === p1y - || t > 1 - tolerance && c2x === p2x && c2y === p2y) { - x = c2x - c1x; - y = c2y - c1y; - } else if (t < tolerance) { + if (t < tolerance) { x = cx; y = cy; } else if (t > 1 - tolerance) { x = 3 * (p2x - c2x); y = 3 * (p2y - c2y); } else { - // Simply use the derivation of the bezier function for both - // the x and y coordinates: x = (3 * ax * t + 2 * bx) * t + cx; y = (3 * ay * t + 2 * by) * t + cy; } + if (normalized) { + // When the tangent at t is zero and we're at the beginning + // or the end, we can use the vector between the handles, + // but only when normalizing as its weighted length is 0. + if (x === 0 && y === 0 + && (t < tolerance || t > 1 - tolerance)) { + x = c2x - c1x; + y = c2y - c1y; + } + // Now normalize x & y + var len = Math.sqrt(x * x + y * y); + x /= len; + y /= len; + } if (type === 3) { // Calculate 2nd derivative, and curvature from there: // http://cagd.cs.byu.edu/~557/text/ch2.pdf page#31 // k = |dx * d2y - dy * d2x| / (( dx^2 + dy^2 )^(3/2)) var x2 = 6 * ax * t + 2 * bx, - y2 = 6 * ay * t + 2 * by; - return (x * y2 - y * x2) / Math.pow(x * x + y * y, 3 / 2); + y2 = 6 * ay * t + 2 * by, + d = Math.pow(x * x + y * y, 3 / 2); + // For JS optimizations we always return a Point, although + // curvature is just a numeric value, stored in x: + x = d !== 0 ? (x * y2 - y * x2) / d : 0; + y = 0; } } } @@ -584,6 +597,30 @@ statics: { return type === 2 ? new Point(y, -x) : new Point(x, y); }, + getPoint: function(v, t) { + return Curve._evaluate(v, t, 0, false); + }, + + getTangent: function(v, t) { + return Curve._evaluate(v, t, 1, true); + }, + + getWeightedTangent: function(v, t) { + return Curve._evaluate(v, t, 1, false); + }, + + getNormal: function(v, t) { + return Curve._evaluate(v, t, 2, true); + }, + + getWeightedNormal: function(v, t) { + return Curve._evaluate(v, t, 2, false); + }, + + getCurvature: function(v, t) { + return Curve._evaluate(v, t, 3, false).x; + }, + subdivide: function(v, t) { var p1x = v[0], p1y = v[1], c1x = v[2], c1y = v[3], @@ -835,22 +872,18 @@ statics: { * @type Rectangle * @ignore */ -}), Base.each(['getPoint', 'getTangent', 'getNormal', 'getCurvature'], +}), Base.each(['getPoint', 'getTangent', 'getNormal', 'getWeightedTangent', + 'getWeightedNormal', 'getCurvature'], // Note: Although Curve.getBounds() exists, we are using Path.getBounds() to // determine the bounds of Curve objects with defined segment1 and segment2 // values Curve.getBounds() can be used directly on curve arrays, without // the need to create a Curve object first, as required by the code that // finds path interesections. - function(name, index) { + function(name) { this[name + 'At'] = function(offset, isParameter) { var values = this.getValues(); - return Curve.evaluate(values, isParameter - ? offset : Curve.getParameterAt(values, offset, 0), index); - }; - // Deprecated and undocumented, but keep around for now. - // TODO: Remove once enough time has passed (28.01.2013) - this[name] = function(parameter) { - return Curve.evaluate(this.getValues(), parameter, index); + return Curve[name](values, isParameter ? offset + : Curve.getParameterAt(values, offset, 0)); }; }, /** @lends Curve# */{ @@ -936,8 +969,7 @@ statics: { function refine(t) { if (t >= 0 && t <= 1) { - var dist = point.getDistance( - Curve.evaluate(values, t, 0), true); + var dist = point.getDistance(Curve.getPoint(values, t), true); if (dist < minDist) { minDist = dist; minT = t; @@ -955,7 +987,7 @@ statics: { if (!refine(minT - step) && !refine(minT + step)) step /= 2; } - var pt = Curve.evaluate(values, minT, 0); + var pt = Curve.getPoint(values, minT); return new CurveLocation(this, minT, pt, null, null, null, point.getDistance(pt)); }, @@ -973,11 +1005,12 @@ statics: { * parameter if {@code isParameter} is {@code true} * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter - * @return {Point} the point on the curve at the specified offset + * @return {Point} the point on the curve at the given offset */ /** - * Calculates the tangent vector of the curve at the given offset. + * Calculates the normalized tangent vector of the curve at the given + * offset. * * @name Curve#getTangentAt * @function @@ -985,7 +1018,7 @@ statics: { * parameter if {@code isParameter} is {@code true} * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter - * @return {Point} the tangent of the curve at the specified offset + * @return {Point} the normalized tangent of the curve at the given offset */ /** @@ -997,7 +1030,33 @@ statics: { * parameter if {@code isParameter} is {@code true} * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter - * @return {Point} the normal of the curve at the specified offset + * @return {Point} the normal of the curve at the given offset + */ + + /** + * Calculates the weighted tangent vector of the curve at the given + * offset, its length reflecting the curve velocity at that location. + * + * @name Curve#getWeightedTangentAt + * @function + * @param {Number} offset the offset on the curve, or the curve time + * parameter if {@code isParameter} is {@code true} + * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} + * is a curve time parameter + * @return {Point} the weighted tangent of the curve at the given offset + */ + + /** + * Calculates the weighted normal vector of the curve at the given offset, + * its length reflecting the curve velocity at that location. + * + * @name Curve#getWeightedNormalAt + * @function + * @param {Number} offset the offset on the curve, or the curve time + * parameter if {@code isParameter} is {@code true} + * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} + * is a curve time parameter + * @return {Point} the weighted normal of the curve at the given offset */ /** @@ -1012,7 +1071,7 @@ statics: { * parameter if {@code isParameter} is {@code true} * @param {Boolean} [isParameter=false] pass {@code true} if {@code offset} * is a curve time parameter - * @return {Number} the curvature of the curve at the specified offset + * @return {Number} the curvature of the curve at the given offset */ }), new function() { // Scope for methods that require numerical integration @@ -1210,12 +1269,12 @@ new function() { // Scope for methods that require numerical integration t2 = uMin + (uMax - uMin) / 2; if (reverse) { addLocation(locations, include, - curve2, t2, Curve.evaluate(v2, t2, 0), - curve1, t1, Curve.evaluate(v1, t1, 0)); + curve2, t2, Curve.getPoint(v2, t2), + curve1, t1, Curve.getPoint(v1, t1)); } else { addLocation(locations, include, - curve1, t1, Curve.evaluate(v1, t1, 0), - curve2, t2, Curve.evaluate(v2, t2, 0)); + curve1, t1, Curve.getPoint(v1, t1), + curve2, t2, Curve.getPoint(v2, t2)); } } else if (tDiff > 0) { // Iterate addCurveIntersections(v2, v1, curve2, curve1, locations, include, @@ -1393,7 +1452,7 @@ new function() { // Scope for methods that require numerical integration // happen with lines, in which case we should not be here. for (var i = 0; i < count; i++) { var tc = roots[i], - x = Curve.evaluate(rvc, tc, 0).x; + x = Curve.getPoint(rvc, tc).x; // We do have a point on the infinite line. Check if it falls on // the line *segment*. if (x >= 0 && x <= rlx2) { @@ -1402,8 +1461,8 @@ new function() { // Scope for methods that require numerical integration t1 = flip ? tl : tc, t2 = flip ? tc : tl; addLocation(locations, include, - curve1, t1, Curve.evaluate(v1, t1, 0), - curve2, t2, Curve.evaluate(v2, t2, 0)); + curve1, t1, Curve.getPoint(v1, t1), + curve2, t2, Curve.getPoint(v2, t2)); } } } diff --git a/src/path/CurveLocation.js b/src/path/CurveLocation.js index 6bc1e5b7..40fa63d3 100644 --- a/src/path/CurveLocation.js +++ b/src/path/CurveLocation.js @@ -307,7 +307,8 @@ var CurveLocation = Base.extend(/** @lends CurveLocation# */{ parts.push('distance: ' + f.number(this._distance)); return '{ ' + parts.join(', ') + ' }'; } -}, Base.each(['getTangent', 'getNormal', 'getCurvature'], function(name) { +}, Base.each(['getTangent', 'getNormal', 'getWeightedTangent', + 'getWeightedNormal', 'getCurvature'], function(name) { // Produce getters for #getTangent() / #getNormal() / #getCurvature() var get = name + 'At'; this[name] = function() { diff --git a/src/path/Path.js b/src/path/Path.js index f51b861d..7aaa63bb 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -996,7 +996,7 @@ var Path = PathItem.extend(/** @lends Path# */{ // Iterate over path and evaluate and add points at given offsets var segments = []; while (pos <= end) { - segments.push(new Segment(iterator.evaluate(pos, 0))); + segments.push(new Segment(iterator.getPointAt(pos))); pos += step; } this.setSegments(segments); @@ -1638,7 +1638,8 @@ var Path = PathItem.extend(/** @lends Path# */{ // TODO: intersects(item) // TODO: contains(item) -}, Base.each(['getPoint', 'getTangent', 'getNormal', 'getCurvature'], +}, Base.each(['getPoint', 'getTangent', 'getNormal', 'getWeightedTangent', + 'getWeightedNormal', 'getCurvature'], function(name) { this[name + 'At'] = function(offset, isParameter) { var loc = this.getLocationAt(offset, isParameter); @@ -1713,8 +1714,9 @@ var Path = PathItem.extend(/** @lends Path# */{ if (isParameter) { // offset consists of curve index and curve parameter, before and // after the fractional digit. - var index = ~~offset; // = Math.floor() - return curves[index].getLocationAt(offset - index, true); + var index = ~~offset, // = Math.floor() + curve = curves[index]; + return curve ? curve.getLocationAt(offset - index, true) : null; } for (var i = 0, l = curves.length; i < l; i++) { var start = length, @@ -1796,14 +1798,14 @@ var Path = PathItem.extend(/** @lends Path# */{ */ /** - * Calculates the tangent vector of the path at the given offset. + * Calculates the normalized tangent vector of the path at the given offset. * * @name Path#getTangentAt * @function * @param {Number} offset the offset on the path, where {@code 0} is at * the beginning of the path and {@link Path#length} at the end * @param {Boolean} [isParameter=false] - * @return {Point} the tangent vector at the given offset + * @return {Point} the normalized tangent vector at the given offset * * @example {@paperscript height=150} * // Working with the tangent vector at a given offset: @@ -1823,11 +1825,9 @@ var Path = PathItem.extend(/** @lends Path# */{ * // Find the point on the path: * var point = path.getPointAt(offset); * - * // Find the tangent vector at the given offset: - * var tangent = path.getTangentAt(offset); - * - * // Make the tangent vector 60pt long: - * tangent.length = 60; + * // Find the tangent vector at the given offset + * // and give it a length of 60: + * var tangent = path.getTangentAt(offset) * 60; * * var line = new Path({ * segments: [point, point + tangent], @@ -1853,11 +1853,9 @@ var Path = PathItem.extend(/** @lends Path# */{ * // Find the point on the path at the given offset: * var point = path.getPointAt(offset); * - * // Find the normal vector on the path at the given offset: - * var tangent = path.getTangentAt(offset); - * - * // Make the tangent vector 60pt long: - * tangent.length = 60; + * // Find the tangent vector on the path at the given offset + * // and give it a length of 60: + * var tangent = path.getTangentAt(offset) * 60; * * var line = new Path({ * segments: [point, point + tangent], @@ -1894,11 +1892,9 @@ var Path = PathItem.extend(/** @lends Path# */{ * // Find the point on the path: * var point = path.getPointAt(offset); * - * // Find the normal vector at the given offset: - * var normal = path.getNormalAt(offset); - * - * // Make the normal vector 30pt long: - * normal.length = 30; + * // Find the normal vector on the path at the given offset + * // and give it a length of 30: + * var normal = path.getNormalAt(offset) * 30; * * var line = new Path({ * segments: [point, point + normal], @@ -1924,11 +1920,9 @@ var Path = PathItem.extend(/** @lends Path# */{ * // Find the point on the path at the given offset: * var point = path.getPointAt(offset); * - * // Find the normal vector on the path at the given offset: - * var normal = path.getNormalAt(offset); - * - * // Make the normal vector 30pt long: - * normal.length = 30; + * // Find the normal vector on the path at the given offset + * // and give it a length of 30: + * var normal = path.getNormalAt(offset) * 30; * * var line = new Path({ * segments: [point, point + normal], @@ -1937,6 +1931,28 @@ var Path = PathItem.extend(/** @lends Path# */{ * } */ + /** + * Calculates the weighted tangent vector of the path at the given offset. + * + * @name Path#getWeightedTangentAt + * @function + * @param {Number} offset the offset on the path, where {@code 0} is at + * the beginning of the path and {@link Path#length} at the end + * @param {Boolean} [isParameter=false] + * @return {Point} the weighted tangent vector at the given offset + */ + + /** + * Calculates the weighted normal vector of the path at the given offset. + * + * @name Path#getWeightedNormalAt + * @function + * @param {Number} offset the offset on the path, where {@code 0} is at + * the beginning of the path and {@link Path#length} at the end + * @param {Boolean} [isParameter=false] + * @return {Point} the weighted normal vector at the given offset + */ + /** * Calculates the curvature of the path at the given offset. Curvatures * indicate how sharply a path changes direction. A straight line has zero @@ -1949,7 +1965,7 @@ var Path = PathItem.extend(/** @lends Path# */{ * the beginning of the path and {@link Path#length} at the end * @param {Boolean} [isParameter=false] * @return {Number} the normal vector at the given offset - * + */ /** * Returns the nearest location on the path to the specified point. @@ -2848,7 +2864,7 @@ statics: { // Calculate the corner points of butt and square caps var point = segment._point, loc = segment.getLocation(), - normal = loc.getNormal().normalize(radius); + normal = loc.getNormal().multiply(radius); // normal is normalized if (area) { addPoint(point.subtract(normal)); addPoint(point.add(normal)); diff --git a/src/path/PathItem.Boolean.js b/src/path/PathItem.Boolean.js index dda6afbf..ce311579 100644 --- a/src/path/PathItem.Boolean.js +++ b/src/path/PathItem.Boolean.js @@ -271,7 +271,7 @@ PathItem.inject(new function() { var values = curves[i].values; if (Curve.solveCubic(values, 0, px, roots, 0, 1) > 0) { for (var j = roots.length - 1; j >= 0; j--) { - var y = Curve.evaluate(values, roots[j], 0).y; + var y = Curve.getPoint(values, roots[j]).y; if (y < yBefore && y > yTop) { yTop = y; } else if (y > yAfter && y < yBottom) { @@ -325,8 +325,8 @@ PathItem.inject(new function() { // sure we're still on the same loop. || t < tMin && prevT > tMax && curve.previous === prevCurve)) { - var x = Curve.evaluate(values, t, 0).x, - slope = Curve.evaluate(values, t, 1).y, + var x = Curve.getPoint(values, t).x, + slope = Curve.getTangent(values, t).y, counted = false; // Take care of cases where the curve and the preceding // curve merely touches the ray towards +-x direction, @@ -334,11 +334,11 @@ PathItem.inject(new function() { // This essentially is not a crossing. if (Numerical.isZero(slope) && !Curve.isLinear(values) // Does the slope over curve beginning change? - || t < tMin && slope * Curve.evaluate( - curve.previous.values, 1, 1).y < 0 + || t < tMin && slope * Curve.getTangent( + curve.previous.values, 1).y < 0 // Does the slope over curve end change? - || t > tMax && slope * Curve.evaluate( - curve.next.values, 0, 1).y < 0) { + || t > tMax && slope * Curve.getTangent( + curve.next.values, 0).y < 0) { if (testContains && x >= xBefore && x <= xAfter) { ++windLeft; ++windRight; @@ -695,7 +695,7 @@ Path.inject(/** @lends Path# */{ || y >= values[7] && y <= values[1]) && Curve.solveCubic(values, 1, y, roots, 0, 1) > 0) { for (var j = roots.length - 1; j >= 0; j--) - xIntercepts.push(Curve.evaluate(values, roots[j], 0).x); + xIntercepts.push(Curve.getPoint(values, roots[j]).x); } if (xIntercepts.length > 1) break; diff --git a/src/path/PathIterator.js b/src/path/PathIterator.js index 072c2f9a..4548d510 100644 --- a/src/path/PathIterator.js +++ b/src/path/PathIterator.js @@ -134,11 +134,6 @@ var PathIterator = Base.extend({ }; }, - evaluate: function(offset, type) { - var param = this.getParameterAt(offset); - return Curve.evaluate(this.curves[param.index], param.value, type); - }, - drawPart: function(ctx, from, to) { from = this.getParameterAt(from); to = this.getParameterAt(to); @@ -151,10 +146,12 @@ var PathIterator = Base.extend({ ctx.bezierCurveTo.apply(ctx, curve.slice(2)); } } -}, Base.each(['getPoint', 'getTangent', 'getNormal', 'getCurvature'], - function(name, index) { - this[name + 'At'] = function(offset) { - return this.evaluate(offset, index); +}, Base.each(['getPoint', 'getTangent', 'getNormal', 'getWeightedTangent', + 'getWeightedNormal', 'getCurvature'], + function(name) { + this[name + 'At'] = function(offset, weighted) { + var param = this.getParameterAt(offset); + return Curve[name](this.curves[param.index], param.value, weighted); }; }, {}) ); diff --git a/test/tests/Curve.js b/test/tests/Curve.js index 0bc72462..f125f1c3 100644 --- a/test/tests/Curve.js +++ b/test/tests/Curve.js @@ -76,8 +76,10 @@ test('Curve#getTangentAt()', function() { for (var i = 0; i < tangents.length; i++) { var entry = tangents[i]; - equals(curve.getTangentAt(entry[0], true), entry[1], + equals(curve.getTangentAt(entry[0], true), entry[1].normalize(), 'curve.getTangentAt(' + entry[0] + ', true);'); + equals(curve.getWeightedTangentAt(entry[0], true), entry[1], + 'curve.getWeightedTangentAt(' + entry[0] + ', true);'); } }); @@ -97,8 +99,10 @@ test('Curve#getNormalAt()', function() { for (var i = 0; i < normals.length; i++) { var entry = normals[i]; - equals(curve.getNormalAt(entry[0], true), entry[1], + equals(curve.getNormalAt(entry[0], true), entry[1].normalize(), 'curve.getNormalAt(' + entry[0] + ', true);'); + equals(curve.getWeightedNormalAt(entry[0], true), entry[1], + 'curve.getWeightedNormalAt(' + entry[0] + ', true);'); } }); diff --git a/test/tests/PathItem_Contains.js b/test/tests/PathItem_Contains.js index 9967f000..3fd29b63 100644 --- a/test/tests/PathItem_Contains.js +++ b/test/tests/PathItem_Contains.js @@ -168,8 +168,8 @@ test('Path#contains() (Rectangle Contours)', function() { curves = path.getCurves(); for (var i = 0; i < curves.length; i++) { - testPoint(path, curves[i].getPoint(0), true); - testPoint(path, curves[i].getPoint(0.5), true); + testPoint(path, curves[i].getPointAt(0, true), true); + testPoint(path, curves[i].getPointAt(0.5, true), true); } }); @@ -181,8 +181,8 @@ test('Path#contains() (Rotated Rectangle Contours)', function() { path.rotate(45); for (var i = 0; i < curves.length; i++) { - testPoint(path, curves[i].getPoint(0), true); - testPoint(path, curves[i].getPoint(0.5), true); + testPoint(path, curves[i].getPointAt(0, true), true); + testPoint(path, curves[i].getPointAt(0.5, true), true); } });