Path.cpp (18290B)
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 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 25 PointD() = default; 26 PointD(double aX, double aY) : Super(aX, aY) {} 27 MOZ_IMPLICIT PointD(const Point& aPoint) : Super(aPoint.x, aPoint.y) {} 28 29 Point ToPoint() const { 30 return Point(static_cast<Float>(x), static_cast<Float>(y)); 31 } 32 }; 33 34 struct BezierControlPoints { 35 BezierControlPoints() = default; 36 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 50 Float Path::ComputeLength() { 51 EnsureFlattenedPath(); 52 return mFlattenedPath->ComputeLength(); 53 } 54 55 Point Path::ComputePointAtLength(Float aLength, Point* aTangent) { 56 EnsureFlattenedPath(); 57 return mFlattenedPath->ComputePointAtLength(aLength, aTangent); 58 } 59 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 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 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 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 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 110 void FlattenedPath::Close() { 111 MOZ_ASSERT(!mCalculatedLength); 112 LineTo(mBeginPoint); 113 } 114 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 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 140 Point FlattenedPath::ComputePointAtLength(Float aLength, Point* aTangent) { 141 if (aLength < mCursor.mLength) { 142 // If cursor is beyond the target length, reset to the beginning. 143 mCursor.Reset(); 144 } else { 145 // Adjust aLength to account for the position where we'll start searching. 146 aLength -= mCursor.mLength; 147 } 148 149 while (mCursor.mIndex < mPathOps.size()) { 150 const auto& op = mPathOps[mCursor.mIndex]; 151 if (op.mType == FlatPathOp::OP_MOVETO) { 152 if (Distance(mCursor.mCurrentPoint, op.mPoint) > 0.0f) { 153 mCursor.mLastPointSinceMove = mCursor.mCurrentPoint; 154 } 155 mCursor.mCurrentPoint = op.mPoint; 156 } else { 157 Float segmentLength = Distance(mCursor.mCurrentPoint, op.mPoint); 158 159 if (segmentLength) { 160 mCursor.mLastPointSinceMove = mCursor.mCurrentPoint; 161 if (segmentLength > aLength) { 162 Point currentVector = op.mPoint - mCursor.mCurrentPoint; 163 Point tangent = currentVector / segmentLength; 164 if (aTangent) { 165 *aTangent = tangent; 166 } 167 return mCursor.mCurrentPoint + tangent * aLength; 168 } 169 } 170 171 aLength -= segmentLength; 172 mCursor.mLength += segmentLength; 173 mCursor.mCurrentPoint = op.mPoint; 174 } 175 mCursor.mIndex++; 176 } 177 178 if (aTangent) { 179 Point currentVector = mCursor.mCurrentPoint - mCursor.mLastPointSinceMove; 180 if (auto h = hypotf(currentVector.x, currentVector.y)) { 181 *aTangent = currentVector / h; 182 } else { 183 *aTangent = Point(); 184 } 185 } 186 return mCursor.mCurrentPoint; 187 } 188 189 // This function explicitly permits aControlPoints to refer to the same object 190 // as either of the other arguments. 191 static void SplitBezier(const BezierControlPoints& aControlPoints, 192 BezierControlPoints* aFirstSegmentControlPoints, 193 BezierControlPoints* aSecondSegmentControlPoints, 194 double t) { 195 MOZ_ASSERT(aSecondSegmentControlPoints); 196 197 *aSecondSegmentControlPoints = aControlPoints; 198 199 PointD cp1a = 200 aControlPoints.mCP1 + (aControlPoints.mCP2 - aControlPoints.mCP1) * t; 201 PointD cp2a = 202 aControlPoints.mCP2 + (aControlPoints.mCP3 - aControlPoints.mCP2) * t; 203 PointD cp1aa = cp1a + (cp2a - cp1a) * t; 204 PointD cp3a = 205 aControlPoints.mCP3 + (aControlPoints.mCP4 - aControlPoints.mCP3) * t; 206 PointD cp2aa = cp2a + (cp3a - cp2a) * t; 207 PointD cp1aaa = cp1aa + (cp2aa - cp1aa) * t; 208 aSecondSegmentControlPoints->mCP4 = aControlPoints.mCP4; 209 210 if (aFirstSegmentControlPoints) { 211 aFirstSegmentControlPoints->mCP1 = aControlPoints.mCP1; 212 aFirstSegmentControlPoints->mCP2 = cp1a; 213 aFirstSegmentControlPoints->mCP3 = cp1aa; 214 aFirstSegmentControlPoints->mCP4 = cp1aaa; 215 } 216 aSecondSegmentControlPoints->mCP1 = cp1aaa; 217 aSecondSegmentControlPoints->mCP2 = cp2aa; 218 aSecondSegmentControlPoints->mCP3 = cp3a; 219 } 220 221 static void FlattenBezierCurveSegment(const BezierControlPoints& aControlPoints, 222 PathSink* aSink, double aTolerance) { 223 /* The algorithm implemented here is based on: 224 * http://cis.usouthal.edu/~hain/general/Publications/Bezier/Bezier%20Offset%20Curves.pdf 225 * 226 * The basic premise is that for a small t the third order term in the 227 * equation of a cubic bezier curve is insignificantly small. This can 228 * then be approximated by a quadratic equation for which the maximum 229 * difference from a linear approximation can be much more easily determined. 230 */ 231 BezierControlPoints currentCP = aControlPoints; 232 233 double t = 0; 234 double currentTolerance = aTolerance; 235 while (t < 1.0) { 236 PointD cp21 = currentCP.mCP2 - currentCP.mCP1; 237 PointD cp31 = currentCP.mCP3 - currentCP.mCP1; 238 239 /* To remove divisions and check for divide-by-zero, this is optimized from: 240 * Float s3 = (cp31.x * cp21.y - cp31.y * cp21.x) / hypotf(cp21.x, cp21.y); 241 * t = 2 * Float(sqrt(aTolerance / (3. * std::abs(s3)))); 242 */ 243 double cp21x31 = cp31.x * cp21.y - cp31.y * cp21.x; 244 double h = hypot(cp21.x, cp21.y); 245 if (cp21x31 * h == 0) { 246 break; 247 } 248 249 double s3inv = h / cp21x31; 250 t = 2 * sqrt(currentTolerance * std::abs(s3inv) / 3.); 251 currentTolerance *= 1 + aTolerance; 252 // Increase tolerance every iteration to prevent this loop from executing 253 // too many times. This approximates the length of large curves more 254 // roughly. In practice, aTolerance is the constant kFlatteningTolerance 255 // which has value 0.0001. With this value, it takes 6,932 splits to double 256 // currentTolerance (to 0.0002) and 23,028 splits to increase 257 // currentTolerance by an order of magnitude (to 0.001). 258 if (t >= 1.0) { 259 break; 260 } 261 262 SplitBezier(currentCP, nullptr, ¤tCP, t); 263 264 aSink->LineTo(currentCP.mCP1.ToPoint()); 265 } 266 267 aSink->LineTo(currentCP.mCP4.ToPoint()); 268 } 269 270 static inline void FindInflectionApproximationRange( 271 BezierControlPoints aControlPoints, double* aMin, double* aMax, double aT, 272 double aTolerance) { 273 SplitBezier(aControlPoints, nullptr, &aControlPoints, aT); 274 275 PointD cp21 = aControlPoints.mCP2 - aControlPoints.mCP1; 276 PointD cp41 = aControlPoints.mCP4 - aControlPoints.mCP1; 277 278 if (cp21.x == 0. && cp21.y == 0.) { 279 cp21 = aControlPoints.mCP3 - aControlPoints.mCP1; 280 } 281 282 if (cp21.x == 0. && cp21.y == 0.) { 283 // In this case s3 becomes lim[n->0] (cp41.x * n) / n - (cp41.y * n) / n = 284 // cp41.x - cp41.y. 285 double s3 = cp41.x - cp41.y; 286 287 // Use the absolute value so that Min and Max will correspond with the 288 // minimum and maximum of the range. 289 if (s3 == 0) { 290 *aMin = -1.0; 291 *aMax = 2.0; 292 } else { 293 double r = CubicRoot(std::abs(aTolerance / s3)); 294 *aMin = aT - r; 295 *aMax = aT + r; 296 } 297 return; 298 } 299 300 double s3 = (cp41.x * cp21.y - cp41.y * cp21.x) / hypot(cp21.x, cp21.y); 301 302 if (s3 == 0) { 303 // This means within the precision we have it can be approximated 304 // infinitely by a linear segment. Deal with this by specifying the 305 // approximation range as extending beyond the entire curve. 306 *aMin = -1.0; 307 *aMax = 2.0; 308 return; 309 } 310 311 double tf = CubicRoot(std::abs(aTolerance / s3)); 312 313 *aMin = aT - tf * (1 - aT); 314 *aMax = aT + tf * (1 - aT); 315 } 316 317 /* Find the inflection points of a bezier curve. Will return false if the 318 * curve is degenerate in such a way that it is best approximated by a straight 319 * line. 320 * 321 * The below algorithm was written by Jeff Muizelaar <jmuizelaar@mozilla.com>, 322 * explanation follows: 323 * 324 * The lower inflection point is returned in aT1, the higher one in aT2. In the 325 * case of a single inflection point this will be in aT1. 326 * 327 * The method is inspired by the algorithm in "analysis of in?ection points for 328 * planar cubic bezier curve" 329 * 330 * Here are some differences between this algorithm and versions discussed 331 * elsewhere in the literature: 332 * 333 * zhang et. al compute a0, d0 and e0 incrementally using the follow formula: 334 * 335 * Point a0 = CP2 - CP1 336 * Point a1 = CP3 - CP2 337 * Point a2 = CP4 - CP1 338 * 339 * Point d0 = a1 - a0 340 * Point d1 = a2 - a1 341 342 * Point e0 = d1 - d0 343 * 344 * this avoids any multiplications and may or may not be faster than the 345 * approach take below. 346 * 347 * "fast, precise flattening of cubic bezier path and ofset curves" by hain et. 348 * al 349 * Point a = CP1 + 3 * CP2 - 3 * CP3 + CP4 350 * Point b = 3 * CP1 - 6 * CP2 + 3 * CP3 351 * Point c = -3 * CP1 + 3 * CP2 352 * Point d = CP1 353 * the a, b, c, d can be expressed in terms of a0, d0 and e0 defined above as: 354 * c = 3 * a0 355 * b = 3 * d0 356 * a = e0 357 * 358 * 359 * a = 3a = a.y * b.x - a.x * b.y 360 * b = 3b = a.y * c.x - a.x * c.y 361 * c = 9c = b.y * c.x - b.x * c.y 362 * 363 * The additional multiples of 3 cancel each other out as show below: 364 * 365 * x = (-b + sqrt(b * b - 4 * a * c)) / (2 * a) 366 * x = (-3 * b + sqrt(3 * b * 3 * b - 4 * a * 3 * 9 * c / 3)) / (2 * 3 * a) 367 * x = 3 * (-b + sqrt(b * b - 4 * a * c)) / (2 * 3 * a) 368 * x = (-b + sqrt(b * b - 4 * a * c)) / (2 * a) 369 * 370 * I haven't looked into whether the formulation of the quadratic formula in 371 * hain has any numerical advantages over the one used below. 372 */ 373 static inline void FindInflectionPoints( 374 const BezierControlPoints& aControlPoints, double* aT1, double* aT2, 375 uint32_t* aCount) { 376 // Find inflection points. 377 // See www.faculty.idc.ac.il/arik/quality/appendixa.html for an explanation 378 // of this approach. 379 PointD A = aControlPoints.mCP2 - aControlPoints.mCP1; 380 PointD B = 381 aControlPoints.mCP3 - (aControlPoints.mCP2 * 2) + aControlPoints.mCP1; 382 PointD C = aControlPoints.mCP4 - (aControlPoints.mCP3 * 3) + 383 (aControlPoints.mCP2 * 3) - aControlPoints.mCP1; 384 385 double a = B.x * C.y - B.y * C.x; 386 double b = A.x * C.y - A.y * C.x; 387 double c = A.x * B.y - A.y * B.x; 388 389 if (a == 0) { 390 // Not a quadratic equation. 391 if (b == 0) { 392 // Instead of a linear acceleration change we have a constant 393 // acceleration change. This means the equation has no solution 394 // and there are no inflection points, unless the constant is 0. 395 // In that case the curve is a straight line, essentially that means 396 // the easiest way to deal with is is by saying there's an inflection 397 // point at t == 0. The inflection point approximation range found will 398 // automatically extend into infinity. 399 if (c == 0) { 400 *aCount = 1; 401 *aT1 = 0; 402 return; 403 } 404 *aCount = 0; 405 return; 406 } 407 *aT1 = -c / b; 408 *aCount = 1; 409 return; 410 } 411 412 double discriminant = b * b - 4 * a * c; 413 414 if (discriminant < 0) { 415 // No inflection points. 416 *aCount = 0; 417 } else if (discriminant == 0) { 418 *aCount = 1; 419 *aT1 = -b / (2 * a); 420 } else { 421 /* Use the following formula for computing the roots: 422 * 423 * q = -1/2 * (b + sign(b) * sqrt(b^2 - 4ac)) 424 * t1 = q / a 425 * t2 = c / q 426 */ 427 double q = sqrt(discriminant); 428 if (b < 0) { 429 q = b - q; 430 } else { 431 q = b + q; 432 } 433 q *= -1. / 2; 434 435 *aT1 = q / a; 436 *aT2 = c / q; 437 if (*aT1 > *aT2) { 438 std::swap(*aT1, *aT2); 439 } 440 *aCount = 2; 441 } 442 } 443 444 void FlattenBezier(const BezierControlPoints& aControlPoints, PathSink* aSink, 445 double aTolerance) { 446 double t1; 447 double t2; 448 uint32_t count; 449 450 FindInflectionPoints(aControlPoints, &t1, &t2, &count); 451 452 // Check that at least one of the inflection points is inside [0..1] 453 if (count == 0 || 454 ((t1 < 0.0 || t1 >= 1.0) && (count == 1 || (t2 < 0.0 || t2 >= 1.0)))) { 455 FlattenBezierCurveSegment(aControlPoints, aSink, aTolerance); 456 return; 457 } 458 459 double t1min = t1, t1max = t1, t2min = t2, t2max = t2; 460 461 BezierControlPoints remainingCP = aControlPoints; 462 463 // For both inflection points, calulate the range where they can be linearly 464 // approximated if they are positioned within [0,1] 465 if (count > 0 && t1 >= 0 && t1 < 1.0) { 466 FindInflectionApproximationRange(aControlPoints, &t1min, &t1max, t1, 467 aTolerance); 468 } 469 if (count > 1 && t2 >= 0 && t2 < 1.0) { 470 FindInflectionApproximationRange(aControlPoints, &t2min, &t2max, t2, 471 aTolerance); 472 } 473 BezierControlPoints nextCPs = aControlPoints; 474 BezierControlPoints prevCPs; 475 476 // Process ranges. [t1min, t1max] and [t2min, t2max] are approximated by line 477 // segments. 478 if (count == 1 && t1min <= 0 && t1max >= 1.0) { 479 // The whole range can be approximated by a line segment. 480 aSink->LineTo(aControlPoints.mCP4.ToPoint()); 481 return; 482 } 483 484 if (t1min > 0) { 485 // Flatten the Bezier up until the first inflection point's approximation 486 // point. 487 SplitBezier(aControlPoints, &prevCPs, &remainingCP, t1min); 488 FlattenBezierCurveSegment(prevCPs, aSink, aTolerance); 489 } 490 if (t1max >= 0 && t1max < 1.0 && (count == 1 || t2min > t1max)) { 491 // The second inflection point's approximation range begins after the end 492 // of the first, approximate the first inflection point by a line and 493 // subsequently flatten up until the end or the next inflection point. 494 SplitBezier(aControlPoints, nullptr, &nextCPs, t1max); 495 496 aSink->LineTo(nextCPs.mCP1.ToPoint()); 497 498 if (count == 1 || (count > 1 && t2min >= 1.0)) { 499 // No more inflection points to deal with, flatten the rest of the curve. 500 FlattenBezierCurveSegment(nextCPs, aSink, aTolerance); 501 } 502 } else if (count > 1 && t2min > 1.0) { 503 // We've already concluded t2min <= t1max, so if this is true the 504 // approximation range for the first inflection point runs past the 505 // end of the curve, draw a line to the end and we're done. 506 aSink->LineTo(aControlPoints.mCP4.ToPoint()); 507 return; 508 } 509 510 if (count > 1 && t2min < 1.0 && t2max > 0) { 511 if (t2min > 0 && t2min < t1max) { 512 // In this case the t2 approximation range starts inside the t1 513 // approximation range. 514 SplitBezier(aControlPoints, nullptr, &nextCPs, t1max); 515 aSink->LineTo(nextCPs.mCP1.ToPoint()); 516 } else if (t2min > 0 && t1max > 0) { 517 SplitBezier(aControlPoints, nullptr, &nextCPs, t1max); 518 519 // Find a control points describing the portion of the curve between t1max 520 // and t2min. 521 double t2mina = (t2min - t1max) / (1 - t1max); 522 SplitBezier(nextCPs, &prevCPs, &nextCPs, t2mina); 523 FlattenBezierCurveSegment(prevCPs, aSink, aTolerance); 524 } else if (t2min > 0) { 525 // We have nothing interesting before t2min, find that bit and flatten it. 526 SplitBezier(aControlPoints, &prevCPs, &nextCPs, t2min); 527 FlattenBezierCurveSegment(prevCPs, aSink, aTolerance); 528 } 529 if (t2max < 1.0) { 530 // Flatten the portion of the curve after t2max 531 SplitBezier(aControlPoints, nullptr, &nextCPs, t2max); 532 533 // Draw a line to the start, this is the approximation between t2min and 534 // t2max. 535 aSink->LineTo(nextCPs.mCP1.ToPoint()); 536 FlattenBezierCurveSegment(nextCPs, aSink, aTolerance); 537 } else { 538 // Our approximation range extends beyond the end of the curve. 539 aSink->LineTo(aControlPoints.mCP4.ToPoint()); 540 return; 541 } 542 } 543 } 544 545 Rect Path::GetFastBounds(const Matrix& aTransform, 546 const StrokeOptions* aStrokeOptions) const { 547 return aStrokeOptions ? GetStrokedBounds(*aStrokeOptions, aTransform) 548 : GetBounds(aTransform); 549 } 550 551 } // namespace gfx 552 } // namespace mozilla