tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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, &currentCP, 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