paper.js/src/path/Curve.js

595 lines
15 KiB
JavaScript
Raw Normal View History

2011-03-06 19:50:44 -05:00
/*
* 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.
2011-03-07 20:41:50 -05:00
* http://paperjs.org/
2011-03-06 19:50:44 -05:00
* http://scriptographer.org/
*
2011-03-07 20:41:50 -05:00
* Distributed under the MIT license. See LICENSE file for details.
*
2011-03-06 19:50:44 -05:00
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
* http://lehni.org/ & http://jonathanpuckey.com/
*
2011-03-07 20:41:50 -05:00
* All rights reserved.
2011-03-06 19:50:44 -05:00
*/
var Curve = this.Curve = Base.extend({
2011-05-23 11:24:36 -04:00
/** @lends Curve# */
beans: true,
2011-05-23 11:24:36 -04:00
/**
* Creates a new curve object.
*
* @name Curve
* @constructor
* @param {Segment} segment1
* @param {Segment} segment2
*
* @class The Curve object represents...
*/
2011-03-06 07:24:15 -05:00
initialize: function(arg0, arg1, arg2, arg3) {
var count = arguments.length;
if (count == 0) {
2011-03-06 07:24:15 -05:00
this._segment1 = new Segment();
this._segment2 = new Segment();
} else if (count == 1) {
2011-03-06 07:24:15 -05:00
// 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) {
2011-03-06 07:24:15 -05:00
this._segment1 = new Segment(arg0, null, arg1);
this._segment2 = new Segment(arg3, arg2, null);
}
},
_changed: function() {
// Clear cached values.
delete this._length;
},
2011-03-06 07:24:15 -05:00
/**
* The first anchor point of the curve.
2011-05-23 11:24:36 -04:00
*
* @type Point
* @bean
2011-03-06 07:24:15 -05:00
*/
getPoint1: function() {
return this._segment1._point;
},
setPoint1: function(point) {
point = Point.read(arguments);
2011-03-06 07:24:15 -05:00
this._segment1._point.set(point.x, point.y);
},
/**
* The second anchor point of the curve.
2011-05-23 11:24:36 -04:00
*
* @type Point
* @bean
2011-03-06 07:24:15 -05:00
*/
getPoint2: function() {
return this._segment2._point;
},
setPoint2: function(point) {
point = Point.read(arguments);
2011-03-06 07:24:15 -05:00
this._segment2._point.set(point.x, point.y);
},
/**
* The handle point that describes the tangent in the first anchor point.
2011-05-23 11:24:36 -04:00
*
* @type Point
* @bean
2011-03-06 07:24:15 -05:00
*/
getHandle1: function() {
return this._segment1._handleOut;
},
setHandle1: function(point) {
point = Point.read(arguments);
2011-03-06 07:24:15 -05:00
this._segment1._handleOut.set(point.x, point.y);
},
/**
* The handle point that describes the tangent in the second anchor point.
2011-05-23 11:24:36 -04:00
*
* @type Point
* @bean
2011-03-06 07:24:15 -05:00
*/
getHandle2: function() {
return this._segment2._handleIn;
},
setHandle2: function(point) {
point = Point.read(arguments);
2011-03-06 07:24:15 -05:00
this._segment2._handleIn.set(point.x, point.y);
},
/**
* The first segment of the curve.
2011-05-23 11:24:36 -04:00
*
* @type Segment
* @bean
2011-03-06 07:24:15 -05:00
*/
getSegment1: function() {
return this._segment1;
},
/**
* The second segment of the curve.
2011-05-23 11:24:36 -04:00
*
* @type Segment
* @bean
2011-03-06 07:24:15 -05:00
*/
getSegment2: function() {
return this._segment2;
},
2011-05-23 11:24:36 -04:00
/**
* The path that the curve belongs to.
*
* @type Path
* @bean
*/
2011-03-06 08:26:09 -05:00
getPath: function() {
return this._path;
},
2011-05-23 11:24:36 -04:00
/**
* The index of the curve in the {@link Path#curves} array.
*
* @type Number
2011-05-23 11:24:36 -04:00
* @bean
*/
2011-03-06 08:26:09 -05:00
getIndex: function() {
return this._segment1._index;
2011-03-06 08:26:09 -05:00
},
2011-05-23 11:24:36 -04:00
/**
* The next curve in the {@link Path#curves} array that the curve
* belongs to.
*
* @type Curve
* @bean
*/
2011-03-06 08:26:09 -05:00
getNext: function() {
var curves = this._path && this._path._curves;
return curves && (curves[this._segment1._index + 1]
2011-04-30 18:22:29 -04:00
|| this._path._closed && curves[0]) || null;
2011-03-06 08:26:09 -05:00
},
2011-05-23 11:24:36 -04:00
/**
* The previous curve in the {@link Path#curves} array that the curve
* belongs to.
*
* @type Curve
* @bean
*/
2011-03-06 08:26:09 -05:00
getPrevious: function() {
var curves = this._path && this._path._curves;
return curves && (curves[this._segment1._index - 1]
2011-04-30 18:22:29 -04:00
|| this._path._closed && curves[curves.length - 1]) || null;
2011-03-06 08:26:09 -05:00
},
2011-04-21 13:54:32 -04:00
2011-05-23 11:24:36 -04:00
/**
* Specifies whether the handles of the curve are selected.
*
* @type Boolean
2011-05-23 11:24:36 -04:00
* @bean
*/
2011-04-21 13:54:32 -04:00
isSelected: function() {
return this.getHandle1().isSelected() && this.getHandle2().isSelected();
2011-04-21 13:54:32 -04:00
},
2011-03-06 08:26:09 -05:00
2011-05-23 11:24:36 -04:00
setSelected: function(selected) {
this.getHandle1().setSelected(selected);
this.getHandle2().setSelected(selected);
},
getCurveValues: function() {
return Curve.getCurveValues(this._segment1, this._segment2);
},
2011-05-23 11:24:36 -04:00
// DOCS: document Curve#getLength(from, to)
/**
* The approximated length of the curve in points.
*
* @type Number
2011-05-23 11:24:36 -04:00
* @bean
*/
getLength: function(/* from, to */) {
var from = arguments[0],
to = arguments[1];
fullLength = arguments.length == 0 || from == 0 && to == 1;
if (fullLength && this._length != null)
return this._length;
// Hide parameters from Bootstrap so it injects bean too
2011-03-06 21:38:07 -05:00
var args = this.getCurveValues();
if (!fullLength)
args.push(from, to);
var length = Curve.getLength.apply(Curve, args);
if (fullLength)
this._length = length;
return length;
},
/**
* 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
2011-05-23 11:24:36 -04:00
// DOCS: document Curve#getParameter(length, start)
/**
* @param {Number} length
* @param {Number} [start]
* @return {Boolean} {@true the curve is linear}
2011-05-23 11:24:36 -04:00
*/
2011-03-07 06:58:41 -05:00
getParameter: function(length, start) {
2011-03-06 21:38:07 -05:00
var args = this.getCurveValues();
2011-03-07 06:58:41 -05:00
args.push(length, start !== undefined ? start : length < 0 ? 1 : 0);
2011-03-06 21:38:07 -05:00
return Curve.getParameter.apply(Curve, args);
},
// TODO: getParameter(point, precision)
// TODO: getLocation
// TODO: getIntersections
// TODO: adjustThroughPoint
2011-05-23 11:24:36 -04:00
// DOCS: document Curve#transform(matrix)
/**
* @param {Matrix} matrix
* @return {Curve}
*/
transform: function(matrix) {
return new Curve(
matrix._transformPoint(this._segment1._point),
matrix._transformPoint(this._segment1._handleOut),
matrix._transformPoint(this._segment2._handleIn),
matrix._transformPoint(this._segment2._point));
},
2011-05-23 11:24:36 -04:00
/**
* 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
2011-05-23 11:24:36 -04:00
/**
* Returns a copy of the curve.
*
* @return {Curve}
*/
clone: function() {
return new Curve(this._segment1, this._segment2);
},
2011-05-23 11:24:36 -04:00
/**
* @return {String} A string representation of the curve.
2011-05-23 11:24:36 -04:00
*/
toString: function() {
2011-05-02 03:57:55 -04:00
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;
}
}
}, new function() {
2011-03-07 11:51:12 -05:00
function evaluate(that, t, type) {
// Calculate the polynomial coefficients. caution: handles are relative
// to points
2011-03-06 08:07:49 -05:00
var point1 = that._segment1._point,
handle1 = that._segment1._handleOut,
handle2 = that._segment2._handleIn,
point2 = that._segment2._point,
x, y;
// Handle special case at beginning / end of curve
// PORT: Change in Sg too, so 0.000000000001 won't be
// required anymore
if (t == 0 || t == 1) {
var point;
switch (type) {
case 0: // point
point = t == 0 ? point1 : point2;
break;
case 1: // tangent
case 2: // normal
point = t == 0
? handle1.isZero()
? handle2.isZero()
? point2.subtract(point1)
: point2.add(handle2).subtract(point1)
: handle1
: handle2.isZero() // t == 1
? handle1.isZero()
? point1.subtract(point2)
: point1.add(handle1).subtract(point2)
: handle2;
break;
}
x = point.x;
y = point.y;
} else {
var dx = point2.x - point1.x,
cx = 3 * handle1.x,
bx = 3 * (dx + handle2.x - handle1.x) - cx,
ax = dx - cx - bx,
dy = point2.y - point1.y,
cy = 3 * handle1.y,
by = 3 * (dy + handle2.y - handle1.y) - cy,
ay = dy - cy - by;
switch (type) {
case 0: // point
x = ((ax * t + bx) * t + cx) * t + point1.x;
y = ((ay * t + by) * t + cy) * t + point1.y;
break;
case 1: // tangent
case 2: // normal
// Simply use the derivation of the bezier function for both
// the x and y coordinates:
2011-05-02 06:23:42 -04:00
x = (3 * ax * t + 2 * bx) * t + cx;
y = (3 * ay * t + 2 * by) * t + cy;
2011-03-07 11:51:12 -05:00
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);
}
function getLengthIntegrand(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
// Calculate the coefficients of a Bezier derivative.
var 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);
2011-05-02 06:23:42 -04:00
};
}
2011-03-25 13:58:20 -04:00
// 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 {
2011-05-23 11:24:36 -04:00
/** @lends Curve# */
/**
* 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}.
2011-05-23 11:24:36 -04:00
* @return {Point}
*/
getPoint: function(parameter) {
return evaluate(this, parameter, 0);
},
2011-05-23 11:24:36 -04:00
/**
* 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}.
2011-05-23 11:24:36 -04:00
*/
getTangent: function(parameter) {
return evaluate(this, parameter, 1);
},
2011-05-23 11:24:36 -04:00
/**
* 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}.
2011-05-23 11:24:36 -04:00
*/
getNormal: function(parameter) {
return evaluate(this, parameter, 2);
},
statics: {
getCurveValues: 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
];
},
getLength: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, a, b) {
2011-03-07 06:52:04 -05:00
if (a === undefined)
a = 0;
2011-03-07 06:52:04 -05:00
if (b === undefined)
b = 1;
if (p1x == c1x && p1y == c1y && p2x == c2x && p2y == c2y) {
// Straight line
2011-03-06 21:33:36 -05:00
var dx = p2x - p1x,
dy = p2y - p1y;
return (b - a) * Math.sqrt(dx * dx + dy * dy);
}
var ds = getLengthIntegrand(
p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
return Numerical.integrate(ds, a, b, getIterations(a, b));
},
getParameter: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y,
2011-03-07 06:58:41 -05:00
length, start) {
if (length == 0)
2011-03-07 06:58:41 -05:00
return start;
// See if we're going forward or backward, and handle cases
// differently
2011-03-07 06:51:01 -05:00
var forward = length > 0,
a = forward ? start : 0,
b = forward ? 1 : start,
length = Math.abs(length),
// Use integrand to calculate both range length and part
// lengths in f(t) below.
ds = getLengthIntegrand(
2011-03-07 06:51:01 -05:00
p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y),
// Get length of total range
rangeLength = Numerical.integrate(ds, a, b,
2011-04-26 07:55:20 -04:00
getIterations(a, b));
if (length >= rangeLength)
return forward ? b : a;
2011-04-26 07:55:20 -04:00
// Use length / rangeLength for an initial guess for t, to
// bring us closer:
var guess = length / rangeLength,
len = 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);
if (start < t) {
len += Numerical.integrate(ds, start, t, count);
} else {
len -= Numerical.integrate(ds, t, start, count);
}
start = t;
return len - length;
}
return Numerical.findRoot(f, ds,
forward ? a + guess : b - guess, // Initial guess for x
a, b, 16, Numerical.TOLERANCE);
},
subdivide: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
if (t === undefined)
t = 0.5;
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;
2011-03-06 21:38:07 -05:00
// 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
];
},
// TODO: Find better name
getPart: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, from, to) {
var curve = [p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y];
if (from > 0) {
// 8th argument of Curve.subdivide() == t, and values can be
// directly used as arguments list for apply().
curve[8] = from;
curve = Curve.subdivide.apply(Curve, curve)[1]; // right
}
if (to < 1) {
// Se above about curve[8].
// Interpolate the parameter at 'to' in the new curve and
// cut there
curve[8] = (to - from) / (1 - from);
curve = Curve.subdivide.apply(Curve, curve)[0]; // left
}
return curve;
},
isSufficientlyFlat: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
// Inspired by Skia, but to be tested:
// Calculate 1/3 (m1) and 2/3 (m2) along the line between start
// (p1) and end (p2), measure distance from there the control
// points and see if they are further away than 1/2.
// Seems all very inaccurate, especially since the distance
// measurement is just the bigger one of x / y...
// TODO: Find a more accurate and still fast way to determine
// this.
var vx = (p2x - p1x) / 3,
vy = (p2y - p1y) / 3,
m1x = p1x + vx,
m1y = p1y + vy,
m2x = p2x - vx,
m2y = p2y - vy;
return Math.max(
Math.abs(m1x - c1x), Math.abs(m1y - c1y),
Math.abs(m2x - c1x), Math.abs(m1y - c1y)) < 1 / 2;
/*
// Thanks to Kaspar Fischer for the following:
// http://www.inf.ethz.ch/personal/fischerk/pubs/bez.pdf
var ux = 3 * c1x - 2 * p1x - p2x;
ux *= ux;
var uy = 3 * c1y - 2 * p1y - p2y;
uy *= uy;
var vx = 3 * c2x - 2 * p2x - p1x;
vx *= vx;
var vy = 3 * c2y - 2 * p2y - p1y;
vy *= vy;
if (ux < vx)
ux = vx;
if (uy < vy)
uy = vy;
2011-06-04 10:16:30 -04:00
// Tolerance is 16 * tol ^ 2
return ux + uy <= 16 * Numerical.TOLERNACE * Numerical.TOLERNACE;
*/
}
}
};
});