mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-08 05:42:07 -05:00
c30ff47930
We better inverse-transform the point and save the code!
827 lines
23 KiB
JavaScript
827 lines
23 KiB
JavaScript
/*
|
|
* Paper.js
|
|
*
|
|
* This file is part of Paper.js, a JavaScript Vector Graphics Library,
|
|
* based on Scriptographer.org and designed to be largely API compatible.
|
|
* http://paperjs.org/
|
|
* http://scriptographer.org/
|
|
*
|
|
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
|
|
* http://lehni.org/ & http://jonathanpuckey.com/
|
|
*
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
*
|
|
* All rights reserved.
|
|
*/
|
|
|
|
/**
|
|
* @name Curve
|
|
*
|
|
* @class The Curve object represents the parts of a path that are connected by
|
|
* two following {@link Segment} objects. The curves of a path can be accessed
|
|
* through its {@link Path#curves} array.
|
|
*
|
|
* While a segment describe the anchor point and its incoming and outgoing
|
|
* handles, a Curve object describes the curve passing between two such
|
|
* segments. Curves and segments represent two different ways of looking at the
|
|
* same thing, but focusing on different aspects. Curves for example offer many
|
|
* convenient ways to work with parts of the path, finding lengths, positions or
|
|
* tangents at given offsets.
|
|
*/
|
|
var Curve = this.Curve = Base.extend(/** @lends Curve# */{
|
|
/**
|
|
* Creates a new curve object.
|
|
*
|
|
* @param {Segment} segment1
|
|
* @param {Segment} segment2
|
|
*/
|
|
initialize: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) {
|
|
var count = arguments.length;
|
|
if (count == 0) {
|
|
this._segment1 = new Segment();
|
|
this._segment2 = new Segment();
|
|
} else if (count == 1) {
|
|
// TODO: If beans are not activated, this won't copy from
|
|
// an existing segment. OK?
|
|
this._segment1 = new Segment(arg0.segment1);
|
|
this._segment2 = new Segment(arg0.segment2);
|
|
} else if (count == 2) {
|
|
this._segment1 = new Segment(arg0);
|
|
this._segment2 = new Segment(arg1);
|
|
} else if (count == 4) {
|
|
this._segment1 = new Segment(arg0, null, arg1);
|
|
this._segment2 = new Segment(arg3, arg2, null);
|
|
} else if (count == 8) {
|
|
// An array as returned by getValues
|
|
var p1 = Point.create(arg0, arg1),
|
|
p2 = Point.create(arg6, arg7);
|
|
this._segment1 = new Segment(p1, null,
|
|
Point.create(arg2, arg3).subtract(p1));
|
|
this._segment2 = new Segment(p2,
|
|
Point.create(arg4, arg5).subtract(p2), null);
|
|
}
|
|
},
|
|
|
|
_changed: function() {
|
|
// Clear cached values.
|
|
delete this._length;
|
|
},
|
|
|
|
/**
|
|
* The first anchor point of the curve.
|
|
*
|
|
* @type Point
|
|
* @bean
|
|
*/
|
|
getPoint1: function() {
|
|
return this._segment1._point;
|
|
},
|
|
|
|
setPoint1: function(point) {
|
|
point = Point.read(arguments);
|
|
this._segment1._point.set(point.x, point.y);
|
|
},
|
|
|
|
/**
|
|
* The second anchor point of the curve.
|
|
*
|
|
* @type Point
|
|
* @bean
|
|
*/
|
|
getPoint2: function() {
|
|
return this._segment2._point;
|
|
},
|
|
|
|
setPoint2: function(point) {
|
|
point = Point.read(arguments);
|
|
this._segment2._point.set(point.x, point.y);
|
|
},
|
|
|
|
/**
|
|
* The handle point that describes the tangent in the first anchor point.
|
|
*
|
|
* @type Point
|
|
* @bean
|
|
*/
|
|
getHandle1: function() {
|
|
return this._segment1._handleOut;
|
|
},
|
|
|
|
setHandle1: function(point) {
|
|
point = Point.read(arguments);
|
|
this._segment1._handleOut.set(point.x, point.y);
|
|
},
|
|
|
|
/**
|
|
* The handle point that describes the tangent in the second anchor point.
|
|
*
|
|
* @type Point
|
|
* @bean
|
|
*/
|
|
getHandle2: function() {
|
|
return this._segment2._handleIn;
|
|
},
|
|
|
|
setHandle2: function(point) {
|
|
point = Point.read(arguments);
|
|
this._segment2._handleIn.set(point.x, point.y);
|
|
},
|
|
|
|
/**
|
|
* The first segment of the curve.
|
|
*
|
|
* @type Segment
|
|
* @bean
|
|
*/
|
|
getSegment1: function() {
|
|
return this._segment1;
|
|
},
|
|
|
|
/**
|
|
* The second segment of the curve.
|
|
*
|
|
* @type Segment
|
|
* @bean
|
|
*/
|
|
getSegment2: function() {
|
|
return this._segment2;
|
|
},
|
|
|
|
/**
|
|
* The path that the curve belongs to.
|
|
*
|
|
* @type Path
|
|
* @bean
|
|
*/
|
|
getPath: function() {
|
|
return this._path;
|
|
},
|
|
|
|
/**
|
|
* The index of the curve in the {@link Path#curves} array.
|
|
*
|
|
* @type Number
|
|
* @bean
|
|
*/
|
|
getIndex: function() {
|
|
return this._segment1._index;
|
|
},
|
|
|
|
/**
|
|
* The next curve in the {@link Path#curves} array that the curve
|
|
* belongs to.
|
|
*
|
|
* @type Curve
|
|
* @bean
|
|
*/
|
|
getNext: function() {
|
|
var curves = this._path && this._path._curves;
|
|
return curves && (curves[this._segment1._index + 1]
|
|
|| this._path._closed && curves[0]) || null;
|
|
},
|
|
|
|
/**
|
|
* The previous curve in the {@link Path#curves} array that the curve
|
|
* belongs to.
|
|
*
|
|
* @type Curve
|
|
* @bean
|
|
*/
|
|
getPrevious: function() {
|
|
var curves = this._path && this._path._curves;
|
|
return curves && (curves[this._segment1._index - 1]
|
|
|| this._path._closed && curves[curves.length - 1]) || null;
|
|
},
|
|
|
|
/**
|
|
* Specifies whether the handles of the curve are selected.
|
|
*
|
|
* @type Boolean
|
|
* @bean
|
|
*/
|
|
isSelected: function() {
|
|
return this.getHandle1().isSelected() && this.getHandle2().isSelected();
|
|
},
|
|
|
|
setSelected: function(selected) {
|
|
this.getHandle1().setSelected(selected);
|
|
this.getHandle2().setSelected(selected);
|
|
},
|
|
|
|
getValues: function() {
|
|
return Curve.getValues(this._segment1, this._segment2);
|
|
},
|
|
|
|
getPoints: function() {
|
|
// Convert to array of absolute points
|
|
var coords = this.getValues(),
|
|
points = [];
|
|
for (var i = 0; i < 8; i += 2)
|
|
points.push(Point.create(coords[i], coords[i + 1]));
|
|
return points;
|
|
},
|
|
|
|
// DOCS: document Curve#getLength(from, to)
|
|
/**
|
|
* The approximated length of the curve in points.
|
|
*
|
|
* @type Number
|
|
* @bean
|
|
*/
|
|
getLength: function(/* from, to */) {
|
|
// Hide parameters from Bootstrap so it injects bean too
|
|
var from = arguments[0],
|
|
to = arguments[1];
|
|
fullLength = arguments.length == 0 || from == 0 && to == 1;
|
|
if (fullLength && this._length != null)
|
|
return this._length;
|
|
var length = Curve.getLength(this.getValues(), from, to);
|
|
if (fullLength)
|
|
this._length = length;
|
|
return length;
|
|
},
|
|
|
|
getPart: function(from, to) {
|
|
return new Curve(Curve.getPart(this.getValues(), from, to));
|
|
},
|
|
|
|
/**
|
|
* Checks if this curve is linear, meaning it does not define any curve
|
|
* handle.
|
|
|
|
* @return {Boolean} {@true the curve is linear}
|
|
*/
|
|
isLinear: function() {
|
|
return this._segment1._handleOut.isZero()
|
|
&& this._segment2._handleIn.isZero();
|
|
},
|
|
|
|
// PORT: Add support for start parameter to Sg
|
|
// PORT: Rename #getParameter(length) -> #getParameterAt(offset)
|
|
// DOCS: Document #getParameter(length, start)
|
|
/**
|
|
* @param {Number} offset
|
|
* @param {Number} [start]
|
|
* @return {Number}
|
|
*/
|
|
getParameterAt: function(offset, start) {
|
|
return Curve.getParameterAt(this.getValues(), offset,
|
|
start !== undefined ? start : offset < 0 ? 1 : 0);
|
|
},
|
|
|
|
/**
|
|
* Returns the point on the curve at the specified position.
|
|
*
|
|
* @param {Number} parameter the position at which to find the point as
|
|
* a value between {@code 0} and {@code 1}.
|
|
* @return {Point}
|
|
*/
|
|
getPoint: function(parameter) {
|
|
return Curve.evaluate(this.getValues(), parameter, 0);
|
|
},
|
|
|
|
/**
|
|
* Returns the tangent point on the curve at the specified position.
|
|
*
|
|
* @param {Number} parameter the position at which to find the tangent
|
|
* point as a value between {@code 0} and {@code 1}.
|
|
*/
|
|
getTangent: function(parameter) {
|
|
return Curve.evaluate(this.getValues(), parameter, 1);
|
|
},
|
|
|
|
/**
|
|
* Returns the normal point on the curve at the specified position.
|
|
*
|
|
* @param {Number} parameter the position at which to find the normal
|
|
* point as a value between {@code 0} and {@code 1}.
|
|
*/
|
|
getNormal: function(parameter) {
|
|
return Curve.evaluate(this.getValues(), parameter, 2);
|
|
},
|
|
|
|
/**
|
|
* @param {Point} point
|
|
* @return {Number}
|
|
*/
|
|
getParameter: function(point) {
|
|
point = Point.read(point);
|
|
return Curve.getParameter(this.getValues(), point.x, point.y);
|
|
},
|
|
|
|
getCrossings: function(point, roots) {
|
|
// Implement the crossing number algorithm:
|
|
// http://en.wikipedia.org/wiki/Point_in_polygon
|
|
// Solve the y-axis cubic polynominal for point.y and count all
|
|
// solutions to the right of point.x as crossings.
|
|
var vals = this.getValues(),
|
|
num = Curve.solveCubic(vals, 1, point.y, roots),
|
|
crossings = 0;
|
|
for (var i = 0; i < num; i++) {
|
|
var t = roots[i];
|
|
if (t >= 0 && t < 1 && Curve.evaluate(vals, t, 0).x > point.x) {
|
|
// If we're close to 0 and are not changing y-direction from the
|
|
// previous curve, do not count this root, as we're merely
|
|
// touching a tip. Passing 1 for Curve.evaluate()'s type means
|
|
// we're calculating tangents, and then check their y-slope for
|
|
// a change of direction:
|
|
if (t < Numerical.TOLERANCE && Curve.evaluate(
|
|
this.getPrevious().getValues(), 1, 1).y
|
|
* Curve.evaluate(vals, t, 1).y >= 0)
|
|
continue;
|
|
crossings++;
|
|
}
|
|
}
|
|
return crossings;
|
|
},
|
|
|
|
// TODO: getLocation
|
|
// TODO: getIntersections
|
|
// TODO: adjustThroughPoint
|
|
|
|
/**
|
|
* Returns a reversed version of the curve, without modifying the curve
|
|
* itself.
|
|
*
|
|
* @return {Curve} a reversed version of the curve
|
|
*/
|
|
reverse: function() {
|
|
return new Curve(this._segment2.reverse(), this._segment1.reverse());
|
|
},
|
|
|
|
// TODO: divide
|
|
// TODO: split
|
|
|
|
/**
|
|
* Returns a copy of the curve.
|
|
*
|
|
* @return {Curve}
|
|
*/
|
|
clone: function() {
|
|
return new Curve(this._segment1, this._segment2);
|
|
},
|
|
|
|
/**
|
|
* @return {String} A string representation of the curve.
|
|
*/
|
|
toString: function() {
|
|
var parts = [ 'point1: ' + this._segment1._point ];
|
|
if (!this._segment1._handleOut.isZero())
|
|
parts.push('handle1: ' + this._segment1._handleOut);
|
|
if (!this._segment2._handleIn.isZero())
|
|
parts.push('handle2: ' + this._segment2._handleIn);
|
|
parts.push('point2: ' + this._segment2._point);
|
|
return '{ ' + parts.join(', ') + ' }';
|
|
},
|
|
|
|
statics: {
|
|
create: function(path, segment1, segment2) {
|
|
var curve = new Curve(Curve.dont);
|
|
curve._path = path;
|
|
curve._segment1 = segment1;
|
|
curve._segment2 = segment2;
|
|
return curve;
|
|
},
|
|
|
|
getValues: function(segment1, segment2) {
|
|
var p1 = segment1._point,
|
|
h1 = segment1._handleOut,
|
|
h2 = segment2._handleIn,
|
|
p2 = segment2._point;
|
|
return [
|
|
p1._x, p1._y,
|
|
p1._x + h1._x, p1._y + h1._y,
|
|
p2._x + h2._x, p2._y + h2._y,
|
|
p2._x, p2._y
|
|
];
|
|
},
|
|
|
|
evaluate: function(v, t, type) {
|
|
var p1x = v[0], p1y = v[1],
|
|
c1x = v[2], c1y = v[3],
|
|
c2x = v[4], c2y = v[5],
|
|
p2x = v[6], p2y = v[7],
|
|
x, y;
|
|
|
|
// Handle special case at beginning / end of curve
|
|
// PORT: Change in Sg too, so 0.000000000001 won't be
|
|
// required anymore
|
|
if (type == 0 && (t == 0 || t == 1)) {
|
|
x = t == 0 ? p1x : p2x;
|
|
y = t == 0 ? p1y : p2y;
|
|
} else {
|
|
// TODO: Find a better solution for this:
|
|
// Prevent tangents and normals of length 0:
|
|
var tMin = Numerical.TOLERANCE;
|
|
if (t < tMin && c1x == p1x && c1y == p1y)
|
|
t = tMin;
|
|
else if (t > 1 - tMin && c2x == p2x && c2y == p2y)
|
|
t = 1 - tMin;
|
|
// Calculate the polynomial coefficients.
|
|
var cx = 3 * (c1x - p1x),
|
|
bx = 3 * (c2x - c1x) - cx,
|
|
ax = p2x - p1x - cx - bx,
|
|
|
|
cy = 3 * (c1y - p1y),
|
|
by = 3 * (c2y - c1y) - cy,
|
|
ay = p2y - p1y - cy - by;
|
|
|
|
switch (type) {
|
|
case 0: // point
|
|
// Calculate the curve point at parameter value t
|
|
x = ((ax * t + bx) * t + cx) * t + p1x;
|
|
y = ((ay * t + by) * t + cy) * t + p1y;
|
|
break;
|
|
case 1: // tangent
|
|
case 2: // normal
|
|
// 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;
|
|
break;
|
|
}
|
|
}
|
|
// The normal is simply the rotated tangent:
|
|
// TODO: Rotate normals the other way in Scriptographer too?
|
|
// (Depending on orientation, I guess?)
|
|
return type == 2 ? new Point(y, -x) : new Point(x, y);
|
|
},
|
|
|
|
subdivide: function(v, t) {
|
|
var p1x = v[0], p1y = v[1],
|
|
c1x = v[2], c1y = v[3],
|
|
c2x = v[4], c2y = v[5],
|
|
p2x = v[6], p2y = v[7];
|
|
if (t === undefined)
|
|
t = 0.5;
|
|
// Triangle computation, with loops unrolled.
|
|
var u = 1 - t,
|
|
// Interpolate from 4 to 3 points
|
|
p3x = u * p1x + t * c1x, p3y = u * p1y + t * c1y,
|
|
p4x = u * c1x + t * c2x, p4y = u * c1y + t * c2y,
|
|
p5x = u * c2x + t * p2x, p5y = u * c2y + t * p2y,
|
|
// Interpolate from 3 to 2 points
|
|
p6x = u * p3x + t * p4x, p6y = u * p3y + t * p4y,
|
|
p7x = u * p4x + t * p5x, p7y = u * p4y + t * p5y,
|
|
// Interpolate from 2 points to 1 point
|
|
p8x = u * p6x + t * p7x, p8y = u * p6y + t * p7y;
|
|
// We now have all the values we need to build the subcurves:
|
|
return [
|
|
[p1x, p1y, p3x, p3y, p6x, p6y, p8x, p8y], // left
|
|
[p8x, p8y, p7x, p7y, p5x, p5y, p2x, p2y] // right
|
|
];
|
|
},
|
|
|
|
// Converts from the point coordinates (p1, c1, c2, p2) for one axis to
|
|
// the polynomial coefficients and solves the polynomial for val
|
|
solveCubic: function (v, coord, val, roots) {
|
|
var p1 = v[coord],
|
|
c1 = v[coord + 2],
|
|
c2 = v[coord + 4],
|
|
p2 = v[coord + 6],
|
|
c = 3 * (c1 - p1),
|
|
b = 3 * (c2 - c1) - c,
|
|
a = p2 - p1 - c - b;
|
|
return Numerical.solveCubic(a, b, c, p1 - val, roots,
|
|
Numerical.TOLERANCE);
|
|
},
|
|
|
|
getParameter: function(v, x, y) {
|
|
var txs = [],
|
|
tys = [],
|
|
sx = Curve.solveCubic(v, 0, x, txs),
|
|
sy = Curve.solveCubic(v, 1, y, tys),
|
|
tx, ty;
|
|
// sx, sy == -1 means infinite solutions:
|
|
// Loop through all solutions for x and match with solutions for y,
|
|
// to see if we either have a matching pair, or infinite solutions
|
|
// for one or the other.
|
|
for (var cx = 0; sx == -1 || cx < sx;) {
|
|
if (sx == -1 || (tx = txs[cx++]) >= 0 && tx <= 1) {
|
|
for (var cy = 0; sy == -1 || cy < sy;) {
|
|
if (sy == -1 || (ty = tys[cy++]) >= 0 && ty <= 1) {
|
|
// Handle infinite solutions by assigning root of
|
|
// the other polynomial
|
|
if (sx == -1) tx = ty;
|
|
else if (sy == -1) ty = tx;
|
|
// Use average if we're within tolerance
|
|
if (Math.abs(tx - ty) < Numerical.TOLERANCE)
|
|
return (tx + ty) * 0.5;
|
|
}
|
|
}
|
|
// Avoid endless loops here: If sx is infinite and there was
|
|
// no fitting ty, there's no solution for this bezier
|
|
if (sx == -1)
|
|
break;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// TODO: Find better name
|
|
getPart: function(v, from, to) {
|
|
if (from > 0)
|
|
v = Curve.subdivide(v, from)[1]; // [1] right
|
|
// Interpolate the parameter at 'to' in the new curve and
|
|
// cut there.
|
|
if (to < 1)
|
|
v = Curve.subdivide(v, (to - from) / (1 - from))[0]; // [0] left
|
|
return v;
|
|
},
|
|
|
|
isFlatEnough: function(v) {
|
|
// Thanks to Kaspar Fischer for the following:
|
|
// http://www.inf.ethz.ch/personal/fischerk/pubs/bez.pdf
|
|
var p1x = v[0], p1y = v[1],
|
|
c1x = v[2], c1y = v[3],
|
|
c2x = v[4], c2y = v[5],
|
|
p2x = v[6], p2y = v[7],
|
|
ux = 3 * c1x - 2 * p1x - p2x,
|
|
uy = 3 * c1y - 2 * p1y - p2y,
|
|
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) < 1;
|
|
}
|
|
}
|
|
}, new function() { // Scope for methods that require numerical integration
|
|
|
|
function getLengthIntegrand(v) {
|
|
// Calculate the coefficients of a Bezier derivative.
|
|
var p1x = v[0], p1y = v[1],
|
|
c1x = v[2], c1y = v[3],
|
|
c2x = v[4], c2y = v[5],
|
|
p2x = v[6], p2y = v[7],
|
|
|
|
ax = 9 * (c1x - c2x) + 3 * (p2x - p1x),
|
|
bx = 6 * (p1x + c2x) - 12 * c1x,
|
|
cx = 3 * (c1x - p1x),
|
|
|
|
ay = 9 * (c1y - c2y) + 3 * (p2y - p1y),
|
|
by = 6 * (p1y + c2y) - 12 * c1y,
|
|
cy = 3 * (c1y - p1y);
|
|
|
|
return function(t) {
|
|
// Calculate quadratic equations of derivatives for x and y
|
|
var dx = (ax * t + bx) * t + cx,
|
|
dy = (ay * t + by) * t + cy;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
};
|
|
}
|
|
|
|
// Amount of integral evaluations for the interval 0 <= a < b <= 1
|
|
function getIterations(a, b) {
|
|
// Guess required precision based and size of range...
|
|
// TODO: There should be much better educated guesses for
|
|
// this. Also, what does this depend on? Required precision?
|
|
return Math.max(2, Math.min(16, Math.ceil(Math.abs(b - a) * 32)));
|
|
}
|
|
|
|
return {
|
|
statics: true,
|
|
|
|
getLength: function(v, a, b) {
|
|
if (a === undefined)
|
|
a = 0;
|
|
if (b === undefined)
|
|
b = 1;
|
|
// if (p1 == c1 && p2 == c2):
|
|
if (v[0] == v[2] && v[1] == v[3] && v[6] == v[4] && v[7] == v[5]) {
|
|
// Straight line
|
|
var dx = v[6] - v[0], // p2x - p1x
|
|
dy = v[7] - v[1]; // p2y - p1y
|
|
return (b - a) * Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
var ds = getLengthIntegrand(v);
|
|
return Numerical.integrate(ds, a, b, getIterations(a, b));
|
|
},
|
|
|
|
getParameterAt: function(v, offset, start) {
|
|
if (offset == 0)
|
|
return start;
|
|
// See if we're going forward or backward, and handle cases
|
|
// differently
|
|
var forward = offset > 0,
|
|
a = forward ? start : 0,
|
|
b = forward ? 1 : start,
|
|
offset = Math.abs(offset),
|
|
// Use integrand to calculate both range length and part
|
|
// lengths in f(t) below.
|
|
ds = getLengthIntegrand(v),
|
|
// Get length of total range
|
|
rangeLength = Numerical.integrate(ds, a, b,
|
|
getIterations(a, b));
|
|
if (offset >= rangeLength)
|
|
return forward ? b : a;
|
|
// Use offset / rangeLength for an initial guess for t, to
|
|
// bring us closer:
|
|
var guess = offset / rangeLength,
|
|
length = 0;
|
|
// Iteratively calculate curve range lengths, and add them up,
|
|
// using integration precision depending on the size of the
|
|
// range. This is much faster and also more precise than not
|
|
// modifing start and calculating total length each time.
|
|
function f(t) {
|
|
var count = getIterations(start, t);
|
|
length += start < t
|
|
? Numerical.integrate(ds, start, t, count)
|
|
: -Numerical.integrate(ds, t, start, count);
|
|
start = t;
|
|
return length - offset;
|
|
}
|
|
return Numerical.findRoot(f, ds,
|
|
forward ? a + guess : b - guess, // Initial guess for x
|
|
a, b, 16, Numerical.TOLERANCE);
|
|
}
|
|
};
|
|
}, new function() { // Scope for nearest point on curve problem
|
|
|
|
// Solving the Nearest Point-on-Curve Problem and A Bezier-Based Root-Finder
|
|
// by Philip J. Schneider from "Graphics Gems", Academic Press, 1990
|
|
// Optimised for Paper.js
|
|
|
|
var maxDepth = 32,
|
|
epsilon = Math.pow(2, -maxDepth - 1);
|
|
|
|
var zCubic = [
|
|
[1.0, 0.6, 0.3, 0.1],
|
|
[0.4, 0.6, 0.6, 0.4],
|
|
[0.1, 0.3, 0.6, 1.0]
|
|
];
|
|
|
|
var xAxis = new Line(new Point(0, 0), new Point(1, 0));
|
|
|
|
/**
|
|
* Given a point and a Bezier curve, generate a 5th-degree Bezier-format
|
|
* equation whose solution finds the point on the curve nearest the
|
|
* user-defined point.
|
|
*/
|
|
function toBezierForm(v, point) {
|
|
var n = 3, // degree of B(t)
|
|
degree = 5, // degree of B(t) . P
|
|
c = [],
|
|
d = [],
|
|
cd = [],
|
|
w = [];
|
|
for(var i = 0; i <= n; i++) {
|
|
// Determine the c's -- these are vectors created by subtracting
|
|
// point point from each of the control points
|
|
c[i] = v[i].subtract(point);
|
|
// Determine the d's -- these are vectors created by subtracting
|
|
// each control point from the next
|
|
if (i < n)
|
|
d[i] = v[i + 1].subtract(v[i]).multiply(n);
|
|
}
|
|
|
|
// Create the c,d table -- this is a table of dot products of the
|
|
// c's and d's
|
|
for (var row = 0; row < n; row++) {
|
|
cd[row] = [];
|
|
for (var column = 0; column <= n; column++)
|
|
cd[row][column] = d[row].dot(c[column]);
|
|
}
|
|
|
|
// Now, apply the z's to the dot products, on the skew diagonal
|
|
// Also, set up the x-values, making these "points"
|
|
for (var i = 0; i <= degree; i++)
|
|
w[i] = new Point(i / degree, 0);
|
|
|
|
for (k = 0; k <= degree; k++) {
|
|
var lb = Math.max(0, k - n + 1),
|
|
ub = Math.min(k, n);
|
|
for (var i = lb; i <= ub; i++) {
|
|
var j = k - i;
|
|
w[k].y += cd[j][i] * zCubic[j][i];
|
|
}
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
/**
|
|
* Given a 5th-degree equation in Bernstein-Bezier form, find all of the
|
|
* roots in the interval [0, 1]. Return the number of roots found.
|
|
*/
|
|
function findRoots(w, depth) {
|
|
switch (countCrossings(w)) {
|
|
case 0:
|
|
// No solutions here
|
|
return [];
|
|
case 1:
|
|
// Unique solution
|
|
// Stop recursion when the tree is deep enough
|
|
// if deep enough, return 1 solution at midpoint
|
|
if (depth >= maxDepth)
|
|
return [0.5 * (w[0].x + w[5].x)];
|
|
// Compute intersection of chord from first control point to last
|
|
// with x-axis.
|
|
if (isFlatEnough(w)) {
|
|
var line = new Line(w[0], w[5], true);
|
|
// Compare the line's squared length with EPSILON. If we're
|
|
// below, #intersect() will return null because of division
|
|
// by near-zero.
|
|
return [ line.vector.getLength(true) <= Numerical.EPSILON
|
|
? line.point.x
|
|
: xAxis.intersect(line).x ];
|
|
}
|
|
}
|
|
|
|
// Otherwise, solve recursively after
|
|
// subdividing control polygon
|
|
var p = [[]],
|
|
left = [],
|
|
right = [];
|
|
for (var j = 0; j <= 5; j++)
|
|
p[0][j] = new Point(w[j]);
|
|
|
|
// Triangle computation
|
|
for (var i = 1; i <= 5; i++) {
|
|
p[i] = [];
|
|
for (var j = 0 ; j <= 5 - i; j++)
|
|
p[i][j] = p[i - 1][j].add(p[i - 1][j + 1]).multiply(0.5);
|
|
}
|
|
for (var j = 0; j <= 5; j++) {
|
|
left[j] = p[j][0];
|
|
right[j] = p[5 - j][j];
|
|
}
|
|
|
|
return findRoots(left, depth + 1).concat(findRoots(right, depth + 1));
|
|
}
|
|
|
|
/**
|
|
* Count the number of times a Bezier control polygon crosses the x-axis.
|
|
* This number is >= the number of roots.
|
|
*/
|
|
function countCrossings(v) {
|
|
var crossings = 0,
|
|
prevSign = null;
|
|
for (var i = 0, l = v.length; i < l; i++) {
|
|
var sign = v[i].y < 0 ? -1 : 1;
|
|
if (prevSign != null && sign != prevSign)
|
|
crossings++;
|
|
prevSign = sign;
|
|
}
|
|
return crossings;
|
|
}
|
|
|
|
/**
|
|
* Check if the control polygon of a Bezier curve is flat enough for
|
|
* recursive subdivision to bottom out.
|
|
*/
|
|
function isFlatEnough(v) {
|
|
// Find the perpendicular distance from each interior control point to
|
|
// line connecting v[0] and v[degree]
|
|
|
|
// Derive the implicit equation for line connecting first
|
|
// and last control points
|
|
var n = v.length - 1,
|
|
a = v[0].y - v[n].y,
|
|
b = v[n].x - v[0].x,
|
|
c = v[0].x * v[n].y - v[n].x * v[0].y,
|
|
maxAbove = 0,
|
|
maxBelow = 0;
|
|
// Find the largest distance
|
|
for (var i = 1; i < n; i++) {
|
|
// Compute distance from each of the points to that line
|
|
var val = a * v[i].x + b * v[i].y + c,
|
|
dist = val * val;
|
|
if (val < 0 && dist > maxBelow) {
|
|
maxBelow = dist;
|
|
} else if (dist > maxAbove) {
|
|
maxAbove = dist;
|
|
}
|
|
}
|
|
// Compute intercepts of bounding box
|
|
return Math.abs((maxAbove + maxBelow) / (2 * a * (a * a + b * b)))
|
|
< epsilon;
|
|
}
|
|
|
|
return {
|
|
getNearestLocation: function(point) {
|
|
// NOTE: If we allow #matrix on Path, we need to inverse-transform
|
|
// point here first.
|
|
// point = this._matrix.inverseTransform(point);
|
|
var w = toBezierForm(this.getPoints(), point);
|
|
// Also look at beginning and end of curve (t = 0 / 1)
|
|
var roots = findRoots(w, 0).concat([0, 1]);
|
|
var minDist = Infinity,
|
|
minT,
|
|
minPoint;
|
|
// There are always roots, since we add [0, 1] above.
|
|
for (var i = 0; i < roots.length; i++) {
|
|
var pt = this.getPoint(roots[i]),
|
|
dist = point.getDistance(pt, true);
|
|
// We're comparing squared distances
|
|
if (dist < minDist) {
|
|
minDist = dist;
|
|
minT = roots[i];
|
|
minPoint = pt;
|
|
}
|
|
}
|
|
return new CurveLocation(this, minT, minPoint, Math.sqrt(minDist));
|
|
},
|
|
|
|
getNearestPoint: function(point) {
|
|
return this.getNearestLocation(point).getPoint();
|
|
}
|
|
};
|
|
});
|