Rework handling of weighted and normalized curve tangents and normals.

Relates to #563
This commit is contained in:
Jürg Lehni 2015-08-19 17:15:41 +02:00
parent eb8c5b4a3e
commit da82116501
7 changed files with 164 additions and 87 deletions

View file

@ -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));
}
}
}

View file

@ -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() {

View file

@ -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));

View file

@ -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;

View file

@ -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);
};
}, {})
);

View file

@ -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);');
}
});

View file

@ -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);
}
});