mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2024-12-29 09:22:22 -05:00
Implement Curve#getTimesWithTangent()
and Path#getOffsetsWithTangent()
This commit is contained in:
parent
47af603173
commit
c235d6a917
5 changed files with 209 additions and 2 deletions
73
examples/Scripts/PathTangentsToVector.html
Normal file
73
examples/Scripts/PathTangentsToVector.html
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Path Tangents To Vector</title>
|
||||||
|
<link rel="stylesheet" href="../css/style.css">
|
||||||
|
<script type="text/javascript" src="../../dist/paper-full.js"></script>
|
||||||
|
<script type="text/paperscript" canvas="canvas">
|
||||||
|
|
||||||
|
// draw path
|
||||||
|
var path = new Path('M185.972,83.103c18.542-27.813,41.083-63.865,72.633-78.412c40.768-18.787,56.519,22.783,48.118,55.174c-7.694,29.634-23.246,56.887-33.519,85.651c-2.092,5.856-12.005,28.226,1.46,28.226c13.623,0,30.341-20.748,38.719-29.609c13.322-14.092,25.403-29.262,36.591-45.092c18.532,0,21.893,16.679,15.512,30.659c-15.041,32.952-45.633,61.693-74.315,82.812c-20.822,15.332-62.421,39.657-85.61,14.639c-26.43-28.497,5.643-88.375,16.151-117.448c15.172-41.98-26.439-5.818-37.874,8.852c-17.928,22.999-16.922,77.719-18.303,106.529c-21.793,21.793-63.141,0.942-66.759-26.731c-2.207-16.876,2.573-34.851,7.098-50.965c4.793-17.07,17.809-38.034,17.809-55.889c0-15.34-20.016,2.606-23.117,5.779c-12.837,13.14-18.843,22.942-21.953,41.102c-3.221,18.811-1.106,85.684-22.392,87.86c-29.787,3.014-51.93-20.085-55.6-48.556c-2.067-16.034,1.385-33.132,5.247-48.637c2.243-9.004,5.006-17.888,8.197-26.599c-4.147-9.616-4.988-20.426-4.988-30.78c0-33.391,44.299-71.678,77.411-65.772c31.311,5.585,6.408,61.28-0.642,76.722c16.999-29.448,43.73-77.256,83.217-77.256C211.992,5.36,197.721,59.599,185.972,83.103z');
|
||||||
|
path.position = view.center;
|
||||||
|
path.fitBounds(view.bounds.scale(0.7));
|
||||||
|
path.fillColor = 'orange';
|
||||||
|
|
||||||
|
// create a marking layer to put temporary items
|
||||||
|
var layer = new Layer();
|
||||||
|
layer.activate();
|
||||||
|
|
||||||
|
// init drawing with a vertical vector
|
||||||
|
drawTangentsToVector(new Point(0, 1));
|
||||||
|
|
||||||
|
function onMouseMove(event) {
|
||||||
|
// draw tangents to vector between view center and mouse pointer
|
||||||
|
drawTangentsToVector(event.point - view.center);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTangentsToVector(vector) {
|
||||||
|
// adapt vector length for display
|
||||||
|
vector.length = 50;
|
||||||
|
|
||||||
|
// remove existing marking items
|
||||||
|
layer.removeChildren();
|
||||||
|
|
||||||
|
// draw a line at view center to show vector direction
|
||||||
|
var line = new Path.Line({
|
||||||
|
from: view.center + vector,
|
||||||
|
to: view.center - vector,
|
||||||
|
strokeColor: 'black',
|
||||||
|
strokeWidth: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// get path times where path is tangent to vector
|
||||||
|
var offsets = path.getOffsetsWithTangent(vector);
|
||||||
|
for (var i = 0; i < offsets.length; i++) {
|
||||||
|
var point = path.getPointAt(offsets[i]);
|
||||||
|
// draw a circle around point
|
||||||
|
new Path.Circle({
|
||||||
|
center: point,
|
||||||
|
radius: 5,
|
||||||
|
strokeColor: 'red'
|
||||||
|
});
|
||||||
|
// draw a line showing tangent
|
||||||
|
new Path.Line({
|
||||||
|
from: point + vector,
|
||||||
|
to: point - vector,
|
||||||
|
strokeColor: 'red'
|
||||||
|
});
|
||||||
|
// draw a line showing point precisely on the path
|
||||||
|
new Path.Line({
|
||||||
|
from: point + vector.rotate(90).normalize(10),
|
||||||
|
to: point - vector.rotate(90).normalize(10),
|
||||||
|
strokeColor: 'red'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id='canvas' resize></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1147,6 +1147,23 @@ statics: /** @lends Curve */{
|
||||||
*/
|
*/
|
||||||
getParameterAt: '#getTimeAt',
|
getParameterAt: '#getTimeAt',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the curve-time parameters where the curve is tangential to
|
||||||
|
* provided tangent. Note that tangents at the start or end are included.
|
||||||
|
*
|
||||||
|
* @param {Point} tangent the tangent to which the curve must be tangential
|
||||||
|
* @return {Number[]} at most two curve-time parameters, where the curve is
|
||||||
|
* tangential to the given tangent
|
||||||
|
*/
|
||||||
|
getTimesWithTangent: function (/* tangent */) {
|
||||||
|
var vector = Point.read(arguments);
|
||||||
|
if (vector.isZero()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Curve.getTimesWithTangent(this.getValues(), vector);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the curve offset at the specified curve-time parameter on
|
* Calculates the curve offset at the specified curve-time parameter on
|
||||||
* the curve.
|
* the curve.
|
||||||
|
@ -2230,6 +2247,56 @@ new function() { // Scope for bezier intersection using fat-line clipping
|
||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to calculates the curve-time parameters where the curve
|
||||||
|
* is tangential to provided tangent.
|
||||||
|
* Tangents at the start or end are included.
|
||||||
|
*
|
||||||
|
* @param {Number[]} v curve values
|
||||||
|
* @param {Point} point the tangent to which the curve must be tangential
|
||||||
|
* @return {Number[]} at most two curve-time parameters, where the curve is
|
||||||
|
* tangential to the given tangent
|
||||||
|
*/
|
||||||
|
function getTimesWithTangent(v, point) {
|
||||||
|
// Algorithm adapted from: https://stackoverflow.com/a/34837312/7615922
|
||||||
|
var x0 = v[0], y0 = v[1],
|
||||||
|
x1 = v[2], y1 = v[3],
|
||||||
|
x2 = v[4], y2 = v[5],
|
||||||
|
x3 = v[6], y3 = v[7],
|
||||||
|
normalized = point.normalize(),
|
||||||
|
tx = normalized.x,
|
||||||
|
ty = normalized.y,
|
||||||
|
ax = 3 * x3 - 9 * x2 + 9 * x1 - 3 * x0,
|
||||||
|
ay = 3 * y3 - 9 * y2 + 9 * y1 - 3 * y0,
|
||||||
|
bx = 6 * x2 - 12 * x1 + 6 * x0,
|
||||||
|
by = 6 * y2 - 12 * y1 + 6 * y0,
|
||||||
|
cx = 3 * x1 - 3 * x0,
|
||||||
|
cy = 3 * y1 - 3 * y0,
|
||||||
|
den = 2 * ax * ty - 2 * ay * tx,
|
||||||
|
times = [];
|
||||||
|
if (Math.abs(den) < Numerical.CURVETIME_EPSILON) {
|
||||||
|
var num = ax * cy - ay * cx;
|
||||||
|
var den = ax * by - ay * bx;
|
||||||
|
if (den != 0) {
|
||||||
|
var t = -num / den;
|
||||||
|
if (t >= 0 && t <= 1) times.push(t);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var delta = (bx * bx - 4 * ax * cx) * ty * ty +
|
||||||
|
(-2 * bx * by + 4 * ay * cx + 4 * ax * cy) * tx * ty +
|
||||||
|
(by * by - 4 * ay * cy) * tx * tx;
|
||||||
|
var k = bx * ty - by * tx;
|
||||||
|
if (delta >= 0 && den != 0) {
|
||||||
|
var d = Math.sqrt(delta);
|
||||||
|
var t0 = -(k + d) / den;
|
||||||
|
var t1 = (-k + d) / den;
|
||||||
|
if (t0 >= 0 && t0 <= 1) times.push(t0);
|
||||||
|
if (t1 >= 0 && t1 <= 1) times.push(t1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return times;
|
||||||
|
}
|
||||||
|
|
||||||
return /** @lends Curve# */{
|
return /** @lends Curve# */{
|
||||||
/**
|
/**
|
||||||
* Returns all intersections between two {@link Curve} objects as an
|
* Returns all intersections between two {@link Curve} objects as an
|
||||||
|
@ -2252,7 +2319,8 @@ new function() { // Scope for bezier intersection using fat-line clipping
|
||||||
getOverlaps: getOverlaps,
|
getOverlaps: getOverlaps,
|
||||||
// Exposed for use in boolean offsetting
|
// Exposed for use in boolean offsetting
|
||||||
getIntersections: getIntersections,
|
getIntersections: getIntersections,
|
||||||
getCurveLineIntersections: getCurveLineIntersections
|
getCurveLineIntersections: getCurveLineIntersections,
|
||||||
|
getTimesWithTangent: getTimesWithTangent
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1895,7 +1895,7 @@ var Path = PathItem.extend(/** @lends Path# */{
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the point on the path at the given offset.
|
* Calculates the point on the path at the given offset.
|
||||||
|
@ -2123,6 +2123,42 @@ var Path = PathItem.extend(/** @lends Path# */{
|
||||||
* the beginning of the path and {@link Path#length} at the end
|
* the beginning of the path and {@link Path#length} at the end
|
||||||
* @return {Number} the normal vector at the given offset
|
* @return {Number} the normal vector at the given offset
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates path offsets where the path is tangential to provided tangent.
|
||||||
|
* Note that tangent at start or end are included.
|
||||||
|
* Tangent at segment point is returned even if only one of its handles is
|
||||||
|
* collinear with the provided tangent.
|
||||||
|
*
|
||||||
|
* @param {Point} tangent the tangent to which the path must be tangential
|
||||||
|
* @return {Number[]} path offsets where the path is tangential to the
|
||||||
|
* provided tangent
|
||||||
|
*/
|
||||||
|
getOffsetsWithTangent: function(/* tangent */) {
|
||||||
|
var tangent = Point.read(arguments);
|
||||||
|
if (tangent.isZero()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var offsets = [];
|
||||||
|
var offsetBeforeCurve = 0;
|
||||||
|
var curves = this.getCurves();
|
||||||
|
for (var i = 0; i < curves.length; i++) {
|
||||||
|
var curve = curves[i];
|
||||||
|
// Calculate curves times at vector tangent...
|
||||||
|
var curveTimes = curve.getTimesWithTangent(tangent);
|
||||||
|
for (var j = 0; j < curveTimes.length; j++) {
|
||||||
|
// ...and convert them to path offsets...
|
||||||
|
var offset = offsetBeforeCurve + curve.getOffsetAtTime(curveTimes[j]);
|
||||||
|
// ...avoiding duplicates.
|
||||||
|
if (offsets.indexOf(offset) < 0) {
|
||||||
|
offsets.push(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offsetBeforeCurve += curve.length;
|
||||||
|
}
|
||||||
|
return offsets;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
new function() { // Scope for drawing
|
new function() { // Scope for drawing
|
||||||
|
|
||||||
|
|
|
@ -347,3 +347,25 @@ test('Curve#divideAt(offset)', function() {
|
||||||
return new Curve(point1, point2).divideAtTime(0.5).point1;
|
return new Curve(point1, point2).divideAtTime(0.5).point1;
|
||||||
}, middle);
|
}, middle);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Curve#getTimesWithTangent()', function() {
|
||||||
|
var curve = new Curve([0, 0], [100, 0], [0, -100], [200, 200]);
|
||||||
|
equals(curve.getTimesWithTangent(), [], 'should return empty array when called without argument');
|
||||||
|
equals(curve.getTimesWithTangent([1, 0]), [0], 'should return tangent at start');
|
||||||
|
equals(curve.getTimesWithTangent([-1, 0]), [0], 'should return the same when called with opposite direction vector');
|
||||||
|
equals(curve.getTimesWithTangent([0, 1]), [1], 'should return tangent at end');
|
||||||
|
equals(curve.getTimesWithTangent([1, 1]), [0.5], 'should return tangent at middle');
|
||||||
|
equals(curve.getTimesWithTangent([1, -1]), [], 'should return empty array when there is no tangent');
|
||||||
|
|
||||||
|
equals(
|
||||||
|
new Curve([0, 0], [100, 0], [500, -500], [-500, -500]).getTimesWithTangent([1, 0]).length,
|
||||||
|
2,
|
||||||
|
'should return 2 values for specific self-intersecting path case'
|
||||||
|
);
|
||||||
|
|
||||||
|
equals(
|
||||||
|
new Curve([0, 0], [100, 0], [0, -100], [0, -100]).getTimesWithTangent([1, 0]).length,
|
||||||
|
2,
|
||||||
|
'should return 2 values for specific parabollic path case'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -611,6 +611,14 @@ test('Path#arcTo(from, through, to); where from, through and to all share the sa
|
||||||
equals(error != null, true, 'We expect this arcTo() command to throw an error');
|
equals(error != null, true, 'We expect this arcTo() command to throw an error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Path#getOffsetsWithTangent()', function() {
|
||||||
|
var path = new Path.Circle(new Point(0, 0), 50);
|
||||||
|
var length = path.length;
|
||||||
|
equals(path.getOffsetsWithTangent(), [], 'should return empty array when called without argument');
|
||||||
|
equals(path.getOffsetsWithTangent([1, 0]), [0.25 * length, 0.75 * length], 'should not return duplicates when tangent is at segment point');
|
||||||
|
equals(path.getOffsetsWithTangent([1, 1]).length, 2, 'should return 2 values when called on a circle with a diagonal vector');
|
||||||
|
});
|
||||||
|
|
||||||
test('Path#add() with a lot of segments (#1493)', function() {
|
test('Path#add() with a lot of segments (#1493)', function() {
|
||||||
var segments = [];
|
var segments = [];
|
||||||
for (var i = 0; i < 100000; i++) {
|
for (var i = 0; i < 100000; i++) {
|
||||||
|
|
Loading…
Reference in a new issue