mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2024-12-29 09:22:22 -05:00
Change implementation of PathItem#flatten(flatness)
- flatness parameter specifies maximum allowed error instead of maximum allowed distance between point - Parts that are already flat are not further flattened - Corners are preserved Closes #618
This commit is contained in:
parent
ed4347714b
commit
9e8fcee8cd
5 changed files with 108 additions and 91 deletions
|
@ -730,7 +730,15 @@ statics: {
|
|||
: v;
|
||||
},
|
||||
|
||||
isFlatEnough: function(v, tolerance) {
|
||||
/**
|
||||
* Determines if a curve is sufficiently flat, meaning it appears as a
|
||||
* straight line and has curve-time that is enough linear, as specified by
|
||||
* the given `flatness` parameter.
|
||||
*
|
||||
* @param {Number} flatness the maximum error allowed for the straight line
|
||||
* to deviate from the curve
|
||||
*/
|
||||
isFlatEnough: function(v, flatness) {
|
||||
// Thanks to Kaspar Fischer and Roger Willcocks for the following:
|
||||
// http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf
|
||||
var p1x = v[0], p1y = v[1],
|
||||
|
@ -742,7 +750,7 @@ statics: {
|
|||
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)
|
||||
< 10 * tolerance * tolerance;
|
||||
<= 16 * flatness * flatness;
|
||||
},
|
||||
|
||||
getArea: function(v) {
|
||||
|
|
|
@ -1202,42 +1202,6 @@ var Path = PathItem.extend(/** @lends Path# */{
|
|||
return this;
|
||||
},
|
||||
|
||||
reverse: function() {
|
||||
this._segments.reverse();
|
||||
// Reverse the handles:
|
||||
for (var i = 0, l = this._segments.length; i < l; i++) {
|
||||
var segment = this._segments[i];
|
||||
var handleIn = segment._handleIn;
|
||||
segment._handleIn = segment._handleOut;
|
||||
segment._handleOut = handleIn;
|
||||
segment._index = i;
|
||||
}
|
||||
// Clear curves since it all has changed.
|
||||
this._curves = null;
|
||||
// Flip clockwise state if it's defined
|
||||
if (this._clockwise !== undefined)
|
||||
this._clockwise = !this._clockwise;
|
||||
this._changed(/*#=*/Change.GEOMETRY);
|
||||
},
|
||||
|
||||
flatten: function(maxDistance) {
|
||||
var iterator = new PathIterator(this, 64, 0.1),
|
||||
pos = 0,
|
||||
// Adapt step = maxDistance so the points distribute evenly.
|
||||
step = iterator.length / Math.ceil(iterator.length / maxDistance),
|
||||
// Add/remove half of step to end, so imprecisions are ok too.
|
||||
// For closed paths, remove it, because we don't want to add last
|
||||
// segment again
|
||||
end = iterator.length + (this._closed ? -step : step) / 2;
|
||||
// Iterate over path and evaluate and add points at given offsets
|
||||
var segments = [];
|
||||
while (pos <= end) {
|
||||
segments.push(new Segment(iterator.getPointAt(pos)));
|
||||
pos += step;
|
||||
}
|
||||
this.setSegments(segments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reduces the path by removing curves that have a length of 0,
|
||||
* and unnecessary segments between two collinear flat curves.
|
||||
|
@ -1261,6 +1225,38 @@ var Path = PathItem.extend(/** @lends Path# */{
|
|||
return this;
|
||||
},
|
||||
|
||||
// NOTE: Documentation is in PathItem#reverse()
|
||||
reverse: function() {
|
||||
this._segments.reverse();
|
||||
// Reverse the handles:
|
||||
for (var i = 0, l = this._segments.length; i < l; i++) {
|
||||
var segment = this._segments[i];
|
||||
var handleIn = segment._handleIn;
|
||||
segment._handleIn = segment._handleOut;
|
||||
segment._handleOut = handleIn;
|
||||
segment._index = i;
|
||||
}
|
||||
// Clear curves since it all has changed.
|
||||
this._curves = null;
|
||||
// Flip clockwise state if it's defined
|
||||
if (this._clockwise !== undefined)
|
||||
this._clockwise = !this._clockwise;
|
||||
this._changed(/*#=*/Change.GEOMETRY);
|
||||
},
|
||||
|
||||
// NOTE: Documentation is in PathItem#flatten()
|
||||
flatten: function(flatness) {
|
||||
// Use PathIterator to subdivide the curves into parts that are flat
|
||||
// enough, as specified by `flatness` / Curve.isFlatEnough():
|
||||
var iterator = new PathIterator(this, flatness || 0.25, 256, true),
|
||||
parts = iterator.parts,
|
||||
segments = [];
|
||||
for (var i = 0, l = parts.length; i < l; i++) {
|
||||
segments.push(new Segment(parts[i].curve.slice(0, 2)));
|
||||
}
|
||||
this.setSegments(segments);
|
||||
},
|
||||
|
||||
// NOTE: Documentation is in PathItem#simplify()
|
||||
simplify: function(tolerance) {
|
||||
var segments = new PathFitter(this).fit(tolerance || 2.5);
|
||||
|
@ -2183,7 +2179,7 @@ new function() { // Scope for drawing
|
|||
// Use PathIterator to draw dashed paths:
|
||||
if (!dontStart)
|
||||
ctx.beginPath();
|
||||
var iterator = new PathIterator(this, 32, 0.25,
|
||||
var iterator = new PathIterator(this, 0.25, 32, false,
|
||||
strokeMatrix),
|
||||
length = iterator.length,
|
||||
from = -style.getDashOffset(), to,
|
||||
|
|
|
@ -388,14 +388,14 @@ var PathItem = Item.extend(/** @lends PathItem# */{
|
|||
*/
|
||||
|
||||
/**
|
||||
* Converts the curves in a path to straight lines with an even distribution
|
||||
* of points. The distance between the produced segments is as close as
|
||||
* possible to the value specified by the `maxDistance` parameter.
|
||||
* Flattens the curves in path items to a sequence of straight lines, by
|
||||
* subdividing them enough times until the specified maximum error is met.
|
||||
*
|
||||
* @name PathItem#flatten
|
||||
* @function
|
||||
*
|
||||
* @param {Number} maxDistance the maximum distance between the points
|
||||
* @param {Number} flatness the maximum error between the flattened lines
|
||||
* and the original curves
|
||||
*
|
||||
* @example {@paperscript}
|
||||
* // Flattening a circle shaped path:
|
||||
|
@ -414,8 +414,8 @@ var PathItem = Item.extend(/** @lends PathItem# */{
|
|||
* var copy = path.clone();
|
||||
* copy.position.x += 150;
|
||||
*
|
||||
* // Convert its curves to points, with a max distance of 20:
|
||||
* copy.flatten(20);
|
||||
* // Convert its curves to points, with a maximum error of 10:
|
||||
* copy.flatten(10);
|
||||
*/
|
||||
|
||||
// TODO: Write about negative indices, and add an example for ranges.
|
||||
|
|
|
@ -19,26 +19,34 @@ var PathIterator = Base.extend({
|
|||
_class: 'PathIterator',
|
||||
|
||||
/**
|
||||
* Creates a path iterator for the given path.
|
||||
* Creates a path iterator for the given path. The iterator converts curves
|
||||
* into a sequence of straight lines by the use of curve-subdivision with an
|
||||
* allowed maximum error to create a lookup table that maps curve-time to
|
||||
* path offsets, and can be used for efficient iteration over the full
|
||||
* length of the path, and getting points / tangents / normals and curvature
|
||||
* in path offset space.
|
||||
*
|
||||
* @param {Path} path the path to iterate over
|
||||
* @param {Path} path the path to create the iterator for
|
||||
* @param {Number} [flatness=0.25] the maximum error allowed for the
|
||||
* straight lines to deviate from the original curves
|
||||
* @param {Number} [maxRecursion=32] the maximum amount of recursion in
|
||||
* curve subdivision when mapping offsets to curve parameters
|
||||
* @param {Number} [tolerance=0.25] the error tolerance at which the
|
||||
* recursion is interrupted before the maximum number of iterations is
|
||||
* reached
|
||||
* curve subdivision when mapping offsets to curve parameters
|
||||
* @param {Boolean} [ignoreStraight=false] if only interested in the result
|
||||
* of the sub-division (e.g. for path flattening), passing `true` will
|
||||
* protect straight curves from being subdivided for curve-time
|
||||
* translation
|
||||
* @param {Matrix} [matrix] the matrix by which to transform the path's
|
||||
* coordinates without modifying the actual path.
|
||||
* coordinates without modifying the actual path.
|
||||
* @return {PathIterator} the newly created path iterator
|
||||
*/
|
||||
initialize: function(path, maxRecursion, tolerance, matrix) {
|
||||
initialize: function(path, flatness, maxRecursion, ignoreStraight, matrix) {
|
||||
// Instead of relying on path.curves, we only use segments here and
|
||||
// get the curve values from them.
|
||||
var curves = [], // The curve values as returned by getValues()
|
||||
parts = [], // The calculated, subdivided parts of the path
|
||||
length = 0, // The total length of the path
|
||||
// By default, we're not subdividing more than 32 times.
|
||||
minDifference = 1 / (maxRecursion || 32),
|
||||
minSpan = 1 / (maxRecursion || 32),
|
||||
segments = path._segments,
|
||||
segment1 = segments[0],
|
||||
segment2;
|
||||
|
@ -51,29 +59,31 @@ var PathIterator = Base.extend({
|
|||
computeParts(curve, segment1._index, 0, 1);
|
||||
}
|
||||
|
||||
function computeParts(curve, index, minT, maxT) {
|
||||
function computeParts(curve, index, t1, t2) {
|
||||
// Check if the t-span is big enough for subdivision.
|
||||
if ((maxT - minT) > minDifference
|
||||
// After quite a bit of testing, a default tolerance of 0.25
|
||||
if ((t2 - t1) > minSpan
|
||||
&& !(ignoreStraight && Curve.isStraight(curve))
|
||||
// After quite a bit of testing, a default flatness of 0.25
|
||||
// appears to offer a good trade-off between speed and
|
||||
// precision for display purposes.
|
||||
&& !Curve.isFlatEnough(curve, tolerance || 0.25)) {
|
||||
var split = Curve.subdivide(curve, 0.5),
|
||||
halfT = (minT + maxT) / 2;
|
||||
&& !Curve.isFlatEnough(curve, flatness || 0.25)) {
|
||||
var halves = Curve.subdivide(curve, 0.5),
|
||||
tMid = (t1 + t2) / 2;
|
||||
// Recursively subdivide and compute parts again.
|
||||
computeParts(split[0], index, minT, halfT);
|
||||
computeParts(split[1], index, halfT, maxT);
|
||||
computeParts(halves[0], index, t1, tMid);
|
||||
computeParts(halves[1], index, tMid, t2);
|
||||
} else {
|
||||
// Calculate distance between p1 and p2
|
||||
var x = curve[6] - curve[0],
|
||||
y = curve[7] - curve[1],
|
||||
dist = Math.sqrt(x * x + y * y);
|
||||
if (dist > /*#=*/Numerical.TOLERANCE) {
|
||||
// Calculate the length of the curve interpreted as a line.
|
||||
var dx = curve[6] - curve[0],
|
||||
dy = curve[7] - curve[1],
|
||||
dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist > 0) {
|
||||
length += dist;
|
||||
parts.push({
|
||||
offset: length,
|
||||
value: maxT,
|
||||
index: index
|
||||
curve: curve,
|
||||
index: index,
|
||||
time: t2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -86,16 +96,15 @@ var PathIterator = Base.extend({
|
|||
}
|
||||
if (path._closed)
|
||||
addCurve(segment2, segments[0]);
|
||||
|
||||
this.curves = curves;
|
||||
this.parts = parts;
|
||||
this.length = length;
|
||||
// Keep a current index from the part where we last where in
|
||||
// getTimeAt(), to optimise for iterator-like usage of iterator.
|
||||
// _get(), to optimise for iterator-like usage of iterator.
|
||||
this.index = 0;
|
||||
},
|
||||
|
||||
getTimeAt: function(offset) {
|
||||
_get: function(offset) {
|
||||
// Make sure we're not beyond the requested offset already. Search the
|
||||
// start position backwards from where to then process the loop below.
|
||||
var i, j = this.index;
|
||||
|
@ -116,41 +125,41 @@ var PathIterator = Base.extend({
|
|||
var prev = this.parts[i - 1];
|
||||
// Make sure we only use the previous parameter value if its
|
||||
// for the same curve, by checking index. Use 0 otherwise.
|
||||
var prevVal = prev && prev.index == part.index ? prev.value : 0,
|
||||
prevLen = prev ? prev.offset : 0;
|
||||
var prevTime = prev && prev.index === part.index ? prev.time : 0,
|
||||
prevOffset = prev ? prev.offset : 0;
|
||||
return {
|
||||
index: part.index,
|
||||
// Interpolate
|
||||
value: prevVal + (part.value - prevVal)
|
||||
* (offset - prevLen) / (part.offset - prevLen),
|
||||
index: part.index
|
||||
time: prevTime + (part.time - prevTime)
|
||||
* (offset - prevOffset) / (part.offset - prevOffset)
|
||||
};
|
||||
}
|
||||
}
|
||||
// Return last one
|
||||
// If we're still here, return last one
|
||||
var part = this.parts[this.parts.length - 1];
|
||||
return {
|
||||
value: 1,
|
||||
index: part.index
|
||||
index: part.index,
|
||||
time: 1
|
||||
};
|
||||
},
|
||||
|
||||
drawPart: function(ctx, from, to) {
|
||||
from = this.getTimeAt(from);
|
||||
to = this.getTimeAt(to);
|
||||
for (var i = from.index; i <= to.index; i++) {
|
||||
var start = this._get(from),
|
||||
end = this._get(to);
|
||||
for (var i = start.index, l = end.index; i <= l; i++) {
|
||||
var curve = Curve.getPart(this.curves[i],
|
||||
i == from.index ? from.value : 0,
|
||||
i == to.index ? to.value : 1);
|
||||
if (i == from.index)
|
||||
i === start.index ? start.time : 0,
|
||||
i === end.index ? end.time : 1);
|
||||
if (i === start.index)
|
||||
ctx.moveTo(curve[0], curve[1]);
|
||||
ctx.bezierCurveTo.apply(ctx, curve.slice(2));
|
||||
}
|
||||
}
|
||||
}, Base.each(Curve._evaluateMethods,
|
||||
function(name) {
|
||||
this[name + 'At'] = function(offset, weighted) {
|
||||
var param = this.getTimeAt(offset);
|
||||
return Curve[name](this.curves[param.index], param.value, weighted);
|
||||
this[name + 'At'] = function(offset) {
|
||||
var param = this._get(offset);
|
||||
return Curve[name](this.curves[param.index], param.time);
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
|
|
|
@ -370,8 +370,12 @@ test('path.curves on closed paths', function() {
|
|||
test('path.flatten(maxDistance)', function() {
|
||||
var path = new Path.Circle(new Size(80, 50), 35);
|
||||
|
||||
// Convert its curves to points, with a max distance of 20:
|
||||
path.flatten(20);
|
||||
// Convert its curves to points, with a flatness of 5:
|
||||
path.flatten(5);
|
||||
|
||||
equals(function() {
|
||||
return path.segments.length;
|
||||
}, 8, 'Using a flatness of 10, we should end up with 8 segments.');
|
||||
|
||||
equals(function() {
|
||||
return path.lastSegment.point.equals(path.firstSegment.point);
|
||||
|
|
Loading…
Reference in a new issue