gfx/2d/Path.cpp

branch
TOR_BUG_9701
changeset 8
97036ab72558
equal deleted inserted replaced
-1:000000000000 0:ded075acf0b6
1 /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 #include "2D.h"
7 #include "PathAnalysis.h"
8 #include "PathHelpers.h"
9
10 namespace mozilla {
11 namespace gfx {
12
13 static float CubicRoot(float aValue) {
14 if (aValue < 0.0) {
15 return -CubicRoot(-aValue);
16 }
17 else {
18 return powf(aValue, 1.0f / 3.0f);
19 }
20 }
21
22 struct BezierControlPoints
23 {
24 BezierControlPoints() {}
25 BezierControlPoints(const Point &aCP1, const Point &aCP2,
26 const Point &aCP3, const Point &aCP4)
27 : mCP1(aCP1), mCP2(aCP2), mCP3(aCP3), mCP4(aCP4)
28 {
29 }
30
31 Point mCP1, mCP2, mCP3, mCP4;
32 };
33
34 void
35 FlattenBezier(const BezierControlPoints &aPoints,
36 PathSink *aSink, Float aTolerance);
37
38
39 Path::Path()
40 {
41 }
42
43 Path::~Path()
44 {
45 }
46
47 Float
48 Path::ComputeLength()
49 {
50 EnsureFlattenedPath();
51 return mFlattenedPath->ComputeLength();
52 }
53
54 Point
55 Path::ComputePointAtLength(Float aLength, Point* aTangent)
56 {
57 EnsureFlattenedPath();
58 return mFlattenedPath->ComputePointAtLength(aLength, aTangent);
59 }
60
61 void
62 Path::EnsureFlattenedPath()
63 {
64 if (!mFlattenedPath) {
65 mFlattenedPath = new FlattenedPath();
66 StreamToSink(mFlattenedPath);
67 }
68 }
69
70 // This is the maximum deviation we allow (with an additional ~20% margin of
71 // error) of the approximation from the actual Bezier curve.
72 const Float kFlatteningTolerance = 0.0001f;
73
74 void
75 FlattenedPath::MoveTo(const Point &aPoint)
76 {
77 MOZ_ASSERT(!mCalculatedLength);
78 FlatPathOp op;
79 op.mType = FlatPathOp::OP_MOVETO;
80 op.mPoint = aPoint;
81 mPathOps.push_back(op);
82
83 mLastMove = aPoint;
84 }
85
86 void
87 FlattenedPath::LineTo(const Point &aPoint)
88 {
89 MOZ_ASSERT(!mCalculatedLength);
90 FlatPathOp op;
91 op.mType = FlatPathOp::OP_LINETO;
92 op.mPoint = aPoint;
93 mPathOps.push_back(op);
94 }
95
96 void
97 FlattenedPath::BezierTo(const Point &aCP1,
98 const Point &aCP2,
99 const Point &aCP3)
100 {
101 MOZ_ASSERT(!mCalculatedLength);
102 FlattenBezier(BezierControlPoints(CurrentPoint(), aCP1, aCP2, aCP3), this, kFlatteningTolerance);
103 }
104
105 void
106 FlattenedPath::QuadraticBezierTo(const Point &aCP1,
107 const Point &aCP2)
108 {
109 MOZ_ASSERT(!mCalculatedLength);
110 // We need to elevate the degree of this quadratic Bézier to cubic, so we're
111 // going to add an intermediate control point, and recompute control point 1.
112 // The first and last control points remain the same.
113 // This formula can be found on http://fontforge.sourceforge.net/bezier.html
114 Point CP0 = CurrentPoint();
115 Point CP1 = (CP0 + aCP1 * 2.0) / 3.0;
116 Point CP2 = (aCP2 + aCP1 * 2.0) / 3.0;
117 Point CP3 = aCP2;
118
119 BezierTo(CP1, CP2, CP3);
120 }
121
122 void
123 FlattenedPath::Close()
124 {
125 MOZ_ASSERT(!mCalculatedLength);
126 LineTo(mLastMove);
127 }
128
129 void
130 FlattenedPath::Arc(const Point &aOrigin, float aRadius, float aStartAngle,
131 float aEndAngle, bool aAntiClockwise)
132 {
133 ArcToBezier(this, aOrigin, Size(aRadius, aRadius), aStartAngle, aEndAngle, aAntiClockwise);
134 }
135
136 Float
137 FlattenedPath::ComputeLength()
138 {
139 if (!mCalculatedLength) {
140 Point currentPoint;
141
142 for (uint32_t i = 0; i < mPathOps.size(); i++) {
143 if (mPathOps[i].mType == FlatPathOp::OP_MOVETO) {
144 currentPoint = mPathOps[i].mPoint;
145 } else {
146 mCachedLength += Distance(currentPoint, mPathOps[i].mPoint);
147 currentPoint = mPathOps[i].mPoint;
148 }
149 }
150
151 mCalculatedLength = true;
152 }
153
154 return mCachedLength;
155 }
156
157 Point
158 FlattenedPath::ComputePointAtLength(Float aLength, Point *aTangent)
159 {
160 // We track the last point that -wasn't- in the same place as the current
161 // point so if we pass the edge of the path with a bunch of zero length
162 // paths we still get the correct tangent vector.
163 Point lastPointSinceMove;
164 Point currentPoint;
165 for (uint32_t i = 0; i < mPathOps.size(); i++) {
166 if (mPathOps[i].mType == FlatPathOp::OP_MOVETO) {
167 if (Distance(currentPoint, mPathOps[i].mPoint)) {
168 lastPointSinceMove = currentPoint;
169 }
170 currentPoint = mPathOps[i].mPoint;
171 } else {
172 Float segmentLength = Distance(currentPoint, mPathOps[i].mPoint);
173
174 if (segmentLength) {
175 lastPointSinceMove = currentPoint;
176 if (segmentLength > aLength) {
177 Point currentVector = mPathOps[i].mPoint - currentPoint;
178 Point tangent = currentVector / segmentLength;
179 if (aTangent) {
180 *aTangent = tangent;
181 }
182 return currentPoint + tangent * aLength;
183 }
184 }
185
186 aLength -= segmentLength;
187 currentPoint = mPathOps[i].mPoint;
188 }
189 }
190
191 Point currentVector = currentPoint - lastPointSinceMove;
192 if (aTangent) {
193 if (hypotf(currentVector.x, currentVector.y)) {
194 *aTangent = currentVector / hypotf(currentVector.x, currentVector.y);
195 } else {
196 *aTangent = Point();
197 }
198 }
199 return currentPoint;
200 }
201
202 // This function explicitly permits aControlPoints to refer to the same object
203 // as either of the other arguments.
204 static void
205 SplitBezier(const BezierControlPoints &aControlPoints,
206 BezierControlPoints *aFirstSegmentControlPoints,
207 BezierControlPoints *aSecondSegmentControlPoints,
208 Float t)
209 {
210 MOZ_ASSERT(aSecondSegmentControlPoints);
211
212 *aSecondSegmentControlPoints = aControlPoints;
213
214 Point cp1a = aControlPoints.mCP1 + (aControlPoints.mCP2 - aControlPoints.mCP1) * t;
215 Point cp2a = aControlPoints.mCP2 + (aControlPoints.mCP3 - aControlPoints.mCP2) * t;
216 Point cp1aa = cp1a + (cp2a - cp1a) * t;
217 Point cp3a = aControlPoints.mCP3 + (aControlPoints.mCP4 - aControlPoints.mCP3) * t;
218 Point cp2aa = cp2a + (cp3a - cp2a) * t;
219 Point cp1aaa = cp1aa + (cp2aa - cp1aa) * t;
220 aSecondSegmentControlPoints->mCP4 = aControlPoints.mCP4;
221
222 if(aFirstSegmentControlPoints) {
223 aFirstSegmentControlPoints->mCP1 = aControlPoints.mCP1;
224 aFirstSegmentControlPoints->mCP2 = cp1a;
225 aFirstSegmentControlPoints->mCP3 = cp1aa;
226 aFirstSegmentControlPoints->mCP4 = cp1aaa;
227 }
228 aSecondSegmentControlPoints->mCP1 = cp1aaa;
229 aSecondSegmentControlPoints->mCP2 = cp2aa;
230 aSecondSegmentControlPoints->mCP3 = cp3a;
231 }
232
233 static void
234 FlattenBezierCurveSegment(const BezierControlPoints &aControlPoints,
235 PathSink *aSink,
236 Float aTolerance)
237 {
238 /* The algorithm implemented here is based on:
239 * http://cis.usouthal.edu/~hain/general/Publications/Bezier/Bezier%20Offset%20Curves.pdf
240 *
241 * The basic premise is that for a small t the third order term in the
242 * equation of a cubic bezier curve is insignificantly small. This can
243 * then be approximated by a quadratic equation for which the maximum
244 * difference from a linear approximation can be much more easily determined.
245 */
246 BezierControlPoints currentCP = aControlPoints;
247
248 Float t = 0;
249 while (t < 1.0f) {
250 Point cp21 = currentCP.mCP2 - currentCP.mCP3;
251 Point cp31 = currentCP.mCP3 - currentCP.mCP1;
252
253 Float s3 = (cp31.x * cp21.y - cp31.y * cp21.x) / hypotf(cp21.x, cp21.y);
254
255 t = 2 * Float(sqrt(aTolerance / (3. * abs(s3))));
256
257 if (t >= 1.0f) {
258 aSink->LineTo(aControlPoints.mCP4);
259 break;
260 }
261
262 Point prevCP2, prevCP3, nextCP1, nextCP2, nextCP3;
263 SplitBezier(currentCP, nullptr, &currentCP, t);
264
265 aSink->LineTo(currentCP.mCP1);
266 }
267 }
268
269 static inline void
270 FindInflectionApproximationRange(BezierControlPoints aControlPoints,
271 Float *aMin, Float *aMax, Float aT,
272 Float aTolerance)
273 {
274 SplitBezier(aControlPoints, nullptr, &aControlPoints, aT);
275
276 Point cp21 = aControlPoints.mCP2 - aControlPoints.mCP1;
277 Point cp41 = aControlPoints.mCP4 - aControlPoints.mCP1;
278
279 if (cp21.x == 0 && cp21.y == 0) {
280 // In this case s3 becomes lim[n->0] (cp41.x * n) / n - (cp41.y * n) / n = cp41.x - cp41.y.
281
282 // Use the absolute value so that Min and Max will correspond with the
283 // minimum and maximum of the range.
284 *aMin = aT - CubicRoot(abs(aTolerance / (cp41.x - cp41.y)));
285 *aMax = aT + CubicRoot(abs(aTolerance / (cp41.x - cp41.y)));
286 return;
287 }
288
289 Float s3 = (cp41.x * cp21.y - cp41.y * cp21.x) / hypotf(cp21.x, cp21.y);
290
291 if (s3 == 0) {
292 // This means within the precision we have it can be approximated
293 // infinitely by a linear segment. Deal with this by specifying the
294 // approximation range as extending beyond the entire curve.
295 *aMin = -1.0f;
296 *aMax = 2.0f;
297 return;
298 }
299
300 Float tf = CubicRoot(abs(aTolerance / s3));
301
302 *aMin = aT - tf * (1 - aT);
303 *aMax = aT + tf * (1 - aT);
304 }
305
306 /* Find the inflection points of a bezier curve. Will return false if the
307 * curve is degenerate in such a way that it is best approximated by a straight
308 * line.
309 *
310 * The below algorithm was written by Jeff Muizelaar <jmuizelaar@mozilla.com>, explanation follows:
311 *
312 * The lower inflection point is returned in aT1, the higher one in aT2. In the
313 * case of a single inflection point this will be in aT1.
314 *
315 * The method is inspired by the algorithm in "analysis of in?ection points for planar cubic bezier curve"
316 *
317 * Here are some differences between this algorithm and versions discussed elsewhere in the literature:
318 *
319 * zhang et. al compute a0, d0 and e0 incrementally using the follow formula:
320 *
321 * Point a0 = CP2 - CP1
322 * Point a1 = CP3 - CP2
323 * Point a2 = CP4 - CP1
324 *
325 * Point d0 = a1 - a0
326 * Point d1 = a2 - a1
327
328 * Point e0 = d1 - d0
329 *
330 * this avoids any multiplications and may or may not be faster than the approach take below.
331 *
332 * "fast, precise flattening of cubic bezier path and ofset curves" by hain et. al
333 * Point a = CP1 + 3 * CP2 - 3 * CP3 + CP4
334 * Point b = 3 * CP1 - 6 * CP2 + 3 * CP3
335 * Point c = -3 * CP1 + 3 * CP2
336 * Point d = CP1
337 * the a, b, c, d can be expressed in terms of a0, d0 and e0 defined above as:
338 * c = 3 * a0
339 * b = 3 * d0
340 * a = e0
341 *
342 *
343 * a = 3a = a.y * b.x - a.x * b.y
344 * b = 3b = a.y * c.x - a.x * c.y
345 * c = 9c = b.y * c.x - b.x * c.y
346 *
347 * The additional multiples of 3 cancel each other out as show below:
348 *
349 * x = (-b + sqrt(b * b - 4 * a * c)) / (2 * a)
350 * x = (-3 * b + sqrt(3 * b * 3 * b - 4 * a * 3 * 9 * c / 3)) / (2 * 3 * a)
351 * x = 3 * (-b + sqrt(b * b - 4 * a * c)) / (2 * 3 * a)
352 * x = (-b + sqrt(b * b - 4 * a * c)) / (2 * a)
353 *
354 * I haven't looked into whether the formulation of the quadratic formula in
355 * hain has any numerical advantages over the one used below.
356 */
357 static inline void
358 FindInflectionPoints(const BezierControlPoints &aControlPoints,
359 Float *aT1, Float *aT2, uint32_t *aCount)
360 {
361 // Find inflection points.
362 // See www.faculty.idc.ac.il/arik/quality/appendixa.html for an explanation
363 // of this approach.
364 Point A = aControlPoints.mCP2 - aControlPoints.mCP1;
365 Point B = aControlPoints.mCP3 - (aControlPoints.mCP2 * 2) + aControlPoints.mCP1;
366 Point C = aControlPoints.mCP4 - (aControlPoints.mCP3 * 3) + (aControlPoints.mCP2 * 3) - aControlPoints.mCP1;
367
368 Float a = Float(B.x) * C.y - Float(B.y) * C.x;
369 Float b = Float(A.x) * C.y - Float(A.y) * C.x;
370 Float c = Float(A.x) * B.y - Float(A.y) * B.x;
371
372 if (a == 0) {
373 // Not a quadratic equation.
374 if (b == 0) {
375 // Instead of a linear acceleration change we have a constant
376 // acceleration change. This means the equation has no solution
377 // and there are no inflection points, unless the constant is 0.
378 // In that case the curve is a straight line, but we'll let
379 // FlattenBezierCurveSegment deal with this.
380 *aCount = 0;
381 return;
382 }
383 *aT1 = -c / b;
384 *aCount = 1;
385 return;
386 } else {
387 Float discriminant = b * b - 4 * a * c;
388
389 if (discriminant < 0) {
390 // No inflection points.
391 *aCount = 0;
392 } else if (discriminant == 0) {
393 *aCount = 1;
394 *aT1 = -b / (2 * a);
395 } else {
396 /* Use the following formula for computing the roots:
397 *
398 * q = -1/2 * (b + sign(b) * sqrt(b^2 - 4ac))
399 * t1 = q / a
400 * t2 = c / q
401 */
402 Float q = sqrtf(discriminant);
403 if (b < 0) {
404 q = b - q;
405 } else {
406 q = b + q;
407 }
408 q *= Float(-1./2);
409
410 *aT1 = q / a;
411 *aT2 = c / q;
412 if (*aT1 > *aT2) {
413 std::swap(*aT1, *aT2);
414 }
415 *aCount = 2;
416 }
417 }
418
419 return;
420 }
421
422 void
423 FlattenBezier(const BezierControlPoints &aControlPoints,
424 PathSink *aSink, Float aTolerance)
425 {
426 Float t1;
427 Float t2;
428 uint32_t count;
429
430 FindInflectionPoints(aControlPoints, &t1, &t2, &count);
431
432 // Check that at least one of the inflection points is inside [0..1]
433 if (count == 0 || ((t1 < 0 || t1 > 1.0) && ((t2 < 0 || t2 > 1.0) || count == 1)) ) {
434 FlattenBezierCurveSegment(aControlPoints, aSink, aTolerance);
435 return;
436 }
437
438 Float t1min = t1, t1max = t1, t2min = t2, t2max = t2;
439
440 BezierControlPoints remainingCP = aControlPoints;
441
442 // For both inflection points, calulate the range where they can be linearly
443 // approximated if they are positioned within [0,1]
444 if (count > 0 && t1 >= 0 && t1 < 1.0) {
445 FindInflectionApproximationRange(aControlPoints, &t1min, &t1max, t1, aTolerance);
446 }
447 if (count > 1 && t2 >= 0 && t2 < 1.0) {
448 FindInflectionApproximationRange(aControlPoints, &t2min, &t2max, t2, aTolerance);
449 }
450 BezierControlPoints nextCPs = aControlPoints;
451 BezierControlPoints prevCPs;
452
453 // Process ranges. [t1min, t1max] and [t2min, t2max] are approximated by line
454 // segments.
455 if (t1min > 0) {
456 // Flatten the Bezier up until the first inflection point's approximation
457 // point.
458 SplitBezier(aControlPoints, &prevCPs,
459 &remainingCP, t1min);
460 FlattenBezierCurveSegment(prevCPs, aSink, aTolerance);
461 }
462 if (t1max >= 0 && t1max < 1.0 && (count == 1 || t2min > t1max)) {
463 // The second inflection point's approximation range begins after the end
464 // of the first, approximate the first inflection point by a line and
465 // subsequently flatten up until the end or the next inflection point.
466 SplitBezier(aControlPoints, nullptr, &nextCPs, t1max);
467
468 aSink->LineTo(nextCPs.mCP1);
469
470 if (count == 1 || (count > 1 && t2min >= 1.0)) {
471 // No more inflection points to deal with, flatten the rest of the curve.
472 FlattenBezierCurveSegment(nextCPs, aSink, aTolerance);
473 }
474 } else if (count > 1 && t2min > 1.0) {
475 // We've already concluded t2min <= t1max, so if this is true the
476 // approximation range for the first inflection point runs past the
477 // end of the curve, draw a line to the end and we're done.
478 aSink->LineTo(aControlPoints.mCP4);
479 return;
480 }
481
482 if (count > 1 && t2min < 1.0 && t2max > 0) {
483 if (t2min > 0 && t2min < t1max) {
484 // In this case the t2 approximation range starts inside the t1
485 // approximation range.
486 SplitBezier(aControlPoints, nullptr, &nextCPs, t1max);
487 aSink->LineTo(nextCPs.mCP1);
488 } else if (t2min > 0 && t1max > 0) {
489 SplitBezier(aControlPoints, nullptr, &nextCPs, t1max);
490
491 // Find a control points describing the portion of the curve between t1max and t2min.
492 Float t2mina = (t2min - t1max) / (1 - t1max);
493 SplitBezier(nextCPs, &prevCPs, &nextCPs, t2mina);
494 FlattenBezierCurveSegment(prevCPs, aSink, aTolerance);
495 } else if (t2min > 0) {
496 // We have nothing interesting before t2min, find that bit and flatten it.
497 SplitBezier(aControlPoints, &prevCPs, &nextCPs, t2min);
498 FlattenBezierCurveSegment(prevCPs, aSink, aTolerance);
499 }
500 if (t2max < 1.0) {
501 // Flatten the portion of the curve after t2max
502 SplitBezier(aControlPoints, nullptr, &nextCPs, t2max);
503
504 // Draw a line to the start, this is the approximation between t2min and
505 // t2max.
506 aSink->LineTo(nextCPs.mCP1);
507 FlattenBezierCurveSegment(nextCPs, aSink, aTolerance);
508 } else {
509 // Our approximation range extends beyond the end of the curve.
510 aSink->LineTo(aControlPoints.mCP4);
511 return;
512 }
513 }
514 }
515
516 }
517 }

mercurial