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