/* * Copyright 2020 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "imgui.h" #include "include/core/SkBitmap.h" #include "include/core/SkCanvas.h" #include "include/core/SkPath.h" #include "include/core/SkPathMeasure.h" #include "include/utils/SkParsePath.h" #include "samplecode/Sample.h" #include "src/core/SkGeometry.h" #include namespace { ////////////////////////////////////////////////////////////////////////////// constexpr inline SkPoint rotate90(const SkPoint& p) { return {p.fY, -p.fX}; } inline SkPoint rotate180(const SkPoint& p) { return p * -1; } inline bool isClockwise(const SkPoint& a, const SkPoint& b) { return a.cross(b) > 0; } static SkPoint checkSetLength(SkPoint p, float len, const char* file, int line) { if (!p.setLength(len)) { SkDebugf("%s:%d: Failed to set point length\n", file, line); } return p; } /** Version of setLength that prints debug msg on failure to help catch edge cases */ #define setLength(p, len) checkSetLength(p, len, __FILE__, __LINE__) constexpr uint64_t choose(uint64_t n, uint64_t k) { SkASSERT(n >= k); uint64_t result = 1; for (uint64_t i = 1; i <= k; i++) { result *= (n + 1 - i); result /= i; } return result; } ////////////////////////////////////////////////////////////////////////////// /** * A scalar (float-valued weights) Bezier curve of arbitrary degree. */ class ScalarBezCurve { public: inline static constexpr int kDegreeInvalid = -1; /** Creates an empty curve with invalid degree. */ ScalarBezCurve() : fDegree(kDegreeInvalid) {} /** Creates a curve of the specified degree with weights initialized to 0. */ explicit ScalarBezCurve(int degree) : fDegree(degree) { SkASSERT(degree >= 0); fWeights.resize(degree + 1, {0}); } /** Creates a curve of specified degree with the given weights. */ ScalarBezCurve(int degree, const std::vector& weights) : ScalarBezCurve(degree) { SkASSERT(degree >= 0); SkASSERT(weights.size() == (size_t)degree + 1); fWeights.insert(fWeights.begin(), weights.begin(), weights.end()); } /** Returns the extreme-valued weight */ float extremumWeight() const { float f = 0; int sign = 1; for (float w : fWeights) { if (std::abs(w) > f) { f = std::abs(w); sign = w >= 0 ? 1 : -1; } } return sign * f; } /** Evaluates the curve at t */ float eval(float t) const { return Eval(*this, t); } /** Evaluates the curve at t */ static float Eval(const ScalarBezCurve& curve, float t) { // Set up starting point of recursion (k=0) ScalarBezCurve result = curve; for (int k = 1; k <= curve.fDegree; k++) { // k is level of recursion, k-1 has previous level's values. for (int i = curve.fDegree; i >= k; i--) { result.fWeights[i] = result.fWeights[i - 1] * (1 - t) + result.fWeights[i] * t; } } return result.fWeights[curve.fDegree]; } /** Splits this curve at t into two halves (of the same degree) */ void split(float t, ScalarBezCurve* left, ScalarBezCurve* right) const { Split(*this, t, left, right); } /** Splits this curve into the subinterval [tmin,tmax]. */ void split(float tmin, float tmax, ScalarBezCurve* result) const { // TODO: I believe there's a more efficient algorithm for this const float tRel = tmin / tmax; ScalarBezCurve ll, rl, rr; this->split(tmax, &rl, &rr); rl.split(tRel, &ll, result); } /** Splits the curve at t into two halves (of the same degree) */ static void Split(const ScalarBezCurve& curve, float t, ScalarBezCurve* left, ScalarBezCurve* right) { // Set up starting point of recursion (k=0) const int degree = curve.fDegree; ScalarBezCurve result = curve; *left = ScalarBezCurve(degree); *right = ScalarBezCurve(degree); left->fWeights[0] = curve.fWeights[0]; right->fWeights[degree] = curve.fWeights[degree]; for (int k = 1; k <= degree; k++) { // k is level of recursion, k-1 has previous level's values. for (int i = degree; i >= k; i--) { result.fWeights[i] = result.fWeights[i - 1] * (1 - t) + result.fWeights[i] * t; } left->fWeights[k] = result.fWeights[k]; right->fWeights[degree - k] = result.fWeights[degree]; } } /** * Increases the degree of the curve to the given degree. Has no effect if the * degree is already equal to the given degree. * * This process is always exact (NB the reverse, degree reduction, is not exact). */ void elevateDegree(int newDegree) { if (newDegree == fDegree) { return; } fWeights = ElevateDegree(*this, newDegree).fWeights; fDegree = newDegree; } /** * Increases the degree of the curve to the given degree. Has no effect if the * degree is already equal to the given degree. * * This process is always exact (NB the reverse, degree reduction, is not exact). */ static ScalarBezCurve ElevateDegree(const ScalarBezCurve& curve, int newDegree) { SkASSERT(newDegree >= curve.degree()); if (newDegree == curve.degree()) { return curve; } // From Farouki, Rajan, "Algorithms for polynomials in Bernstein form" 1988. ScalarBezCurve elevated(newDegree); const int r = newDegree - curve.fDegree; const int n = curve.fDegree; for (int i = 0; i <= n + r; i++) { elevated.fWeights[i] = 0; for (int j = std::max(0, i - r); j <= std::min(n, i); j++) { const float f = (choose(n, j) * choose(r, i - j)) / static_cast(choose(n + r, i)); elevated.fWeights[i] += curve.fWeights[j] * f; } } return elevated; } /** * Returns the zero-set of this curve, which is a list of t values where the curve crosses 0. */ std::vector zeroSet() const { return ZeroSet(*this); } /** * Returns the zero-set of the curve, which is a list of t values where the curve crosses 0. */ static std::vector ZeroSet(const ScalarBezCurve& curve) { constexpr float kTol = 0.001f; std::vector result; ZeroSetRec(curve, 0, 1, kTol, &result); return result; } /** Multiplies the curve's weights by a constant value */ static ScalarBezCurve Mul(const ScalarBezCurve& curve, float f) { ScalarBezCurve result = curve; for (int k = 0; k <= curve.fDegree; k++) { result.fWeights[k] *= f; } return result; } /** * Multiplies the two curves and returns the result. * * Degree of resulting curve is the sum of the degrees of the input curves. */ static ScalarBezCurve Mul(const ScalarBezCurve& a, const ScalarBezCurve& b) { // From G. Elber, "Free form surface analysis using a hybrid of symbolic and numeric // computation". PhD thesis, 1992. p.11. const int n = a.degree(), m = b.degree(); const int newDegree = n + m; ScalarBezCurve result(newDegree); for (int k = 0; k <= newDegree; k++) { result.fWeights[k] = 0; for (int i = std::max(0, k - n); i <= std::min(k, m); i++) { const float f = (choose(m, i) * choose(n, k - i)) / static_cast(choose(m + n, k)); result.fWeights[k] += a.fWeights[i] * b.fWeights[k - i] * f; } } return result; } /** Returns a^2 + b^2. This is a specialized method because the loops are easily fused. */ static ScalarBezCurve AddSquares(const ScalarBezCurve& a, const ScalarBezCurve& b) { const int n = a.degree(), m = b.degree(); const int newDegree = n + m; ScalarBezCurve result(newDegree); for (int k = 0; k <= newDegree; k++) { float aSq = 0, bSq = 0; for (int i = std::max(0, k - n); i <= std::min(k, m); i++) { const float f = (choose(m, i) * choose(n, k - i)) / static_cast(choose(m + n, k)); aSq += a.fWeights[i] * a.fWeights[k - i] * f; bSq += b.fWeights[i] * b.fWeights[k - i] * f; } result.fWeights[k] = aSq + bSq; } return result; } /** Returns a - b. */ static ScalarBezCurve Sub(const ScalarBezCurve& a, const ScalarBezCurve& b) { ScalarBezCurve result = a; result.sub(b); return result; } /** Subtracts the other curve from this curve */ void sub(const ScalarBezCurve& other) { SkASSERT(other.fDegree == fDegree); for (int k = 0; k <= fDegree; k++) { fWeights[k] -= other.fWeights[k]; } } /** Subtracts a constant from this curve */ void sub(float f) { for (int k = 0; k <= fDegree; k++) { fWeights[k] -= f; } } /** Returns the curve degree */ int degree() const { return fDegree; } /** Returns the curve weights */ const std::vector& weights() const { return fWeights; } float operator[](size_t i) const { return fWeights[i]; } float& operator[](size_t i) { return fWeights[i]; } private: /** Recursive helper for ZeroSet */ static void ZeroSetRec(const ScalarBezCurve& curve, float tmin, float tmax, float tol, std::vector* result) { float lenP = 0; bool allPos = curve.fWeights[0] >= 0, allNeg = curve.fWeights[0] < 0; for (int i = 1; i <= curve.fDegree; i++) { lenP += std::abs(curve.fWeights[i] - curve.fWeights[i - 1]); allPos &= curve.fWeights[i] >= 0; allNeg &= curve.fWeights[i] < 0; } if (lenP <= tol) { result->push_back((tmin + tmax) * 0.5); return; } else if (allPos || allNeg) { // No zero crossings possible if the coefficients don't change sign (convex hull // property) return; } else if (SkScalarNearlyZero(tmax - tmin)) { return; } else { ScalarBezCurve left(curve.fDegree), right(curve.fDegree); Split(curve, 0.5f, &left, &right); const float tmid = (tmin + tmax) * 0.5; ZeroSetRec(left, tmin, tmid, tol, result); ZeroSetRec(right, tmid, tmax, tol, result); } } int fDegree; std::vector fWeights; }; ////////////////////////////////////////////////////////////////////////////// /** Helper class that measures per-verb path lengths. */ class PathVerbMeasure { public: explicit PathVerbMeasure(const SkPath& path) : fPath(path), fIter(path, false) { nextVerb(); } SkScalar totalLength() const; SkScalar currentVerbLength() { return fMeas.getLength(); } void nextVerb(); private: const SkPath& fPath; SkPoint fFirstPointInContour; SkPoint fPreviousPoint; SkPath fCurrVerb; SkPath::Iter fIter; SkPathMeasure fMeas; }; SkScalar PathVerbMeasure::totalLength() const { SkPathMeasure meas(fPath, false); return meas.getLength(); } void PathVerbMeasure::nextVerb() { SkPoint pts[4]; SkPath::Verb verb = fIter.next(pts); while (verb == SkPath::kMove_Verb || verb == SkPath::kClose_Verb) { if (verb == SkPath::kMove_Verb) { fFirstPointInContour = pts[0]; fPreviousPoint = fFirstPointInContour; } verb = fIter.next(pts); } fCurrVerb.rewind(); fCurrVerb.moveTo(fPreviousPoint); switch (verb) { case SkPath::kLine_Verb: fCurrVerb.lineTo(pts[1]); break; case SkPath::kQuad_Verb: fCurrVerb.quadTo(pts[1], pts[2]); break; case SkPath::kCubic_Verb: fCurrVerb.cubicTo(pts[1], pts[2], pts[3]); break; case SkPath::kConic_Verb: fCurrVerb.conicTo(pts[1], pts[2], fIter.conicWeight()); break; case SkPath::kDone_Verb: break; case SkPath::kClose_Verb: case SkPath::kMove_Verb: SkASSERT(false); break; } fCurrVerb.getLastPt(&fPreviousPoint); fMeas.setPath(&fCurrVerb, false); } ////////////////////////////////////////////////////////////////////////////// // Several debug-only visualization helpers namespace viz { std::unique_ptr outerErr; SkPath outerFirstApprox; } // namespace viz /** * Prototype variable-width path stroker. * * Takes as input a path to be stroked, and two distance functions (inside and outside). * Produces a fill path with the stroked path geometry. * * The algorithms in use here are from: * * G. Elber, E. Cohen. "Error bounded variable distance offset operator for free form curves and * surfaces." International Journal of Computational Geometry & Applications 1, no. 01 (1991) * * G. Elber. "Free form surface analysis using a hybrid of symbolic and numeric computation." * PhD diss., Dept. of Computer Science, University of Utah, 1992. */ class SkVarWidthStroker { public: /** Metric to use for interpolation of distance function across path segments. */ enum class LengthMetric { /** Each path segment gets an equal interval of t in [0,1] */ kNumSegments, /** Each path segment gets t interval equal to its percent of the total path length */ kPathLength, }; /** * Strokes the path with a fixed-width distance function. This produces a traditional stroked * path. */ SkPath getFillPath(const SkPath& path, const SkPaint& paint) { return getFillPath(path, paint, identityVarWidth(paint.getStrokeWidth()), identityVarWidth(paint.getStrokeWidth())); } /** * Strokes the given path using the two given distance functions for inner and outer offsets. */ SkPath getFillPath(const SkPath& path, const SkPaint& paint, const ScalarBezCurve& varWidth, const ScalarBezCurve& varWidthInner, LengthMetric lengthMetric = LengthMetric::kNumSegments); private: /** Helper struct referring to a single segment of an SkPath */ struct PathSegment { SkPath::Verb fVerb; std::array fPoints; }; struct OffsetSegments { std::vector fInner; std::vector fOuter; }; /** Initialize stroker state */ void initForPath(const SkPath& path, const SkPaint& paint); /** Strokes a path segment */ OffsetSegments strokeSegment(const PathSegment& segment, const ScalarBezCurve& varWidth, const ScalarBezCurve& varWidthInner, bool needsMove); /** * Strokes the given segment using the given distance function. * * Returns a list of quad segments that approximate the offset curve. * TODO: no reason this needs to return a vector of quads, can just append to the path */ std::vector strokeSegment(const PathSegment& seg, const ScalarBezCurve& distanceFunc) const; /** Adds an endcap to fOuter */ enum class CapLocation { Start, End }; void endcap(CapLocation loc); /** Adds a join between the two segments */ void join(const SkPoint& common, float innerRadius, float outerRadius, const OffsetSegments& prev, const OffsetSegments& curr); /** Appends path in reverse to result */ static void appendPathReversed(const SkPath& path, SkPath* result); /** Returns the segment unit normal and unit tangent if not nullptr */ static SkPoint unitNormal(const PathSegment& seg, float t, SkPoint* tangentOut); /** Returns the degree of a segment curve */ static int segmentDegree(const PathSegment& seg); /** Splits a path segment at t */ static void splitSegment(const PathSegment& seg, float t, PathSegment* segA, PathSegment* segB); /** * Returns a quadratic segment that approximates the given segment using the given distance * function. */ static void approximateSegment(const PathSegment& seg, const ScalarBezCurve& distFnc, PathSegment* approxQuad); /** Returns a constant (deg 0) distance function for the given stroke width */ static ScalarBezCurve identityVarWidth(float strokeWidth) { return ScalarBezCurve(0, {strokeWidth / 2.0f}); } float fRadius; SkPaint::Cap fCap; SkPaint::Join fJoin; SkPath fInner, fOuter; ScalarBezCurve fVarWidth, fVarWidthInner; float fCurrT; }; void SkVarWidthStroker::initForPath(const SkPath& path, const SkPaint& paint) { fRadius = paint.getStrokeWidth() / 2; fCap = paint.getStrokeCap(); fJoin = paint.getStrokeJoin(); fInner.rewind(); fOuter.rewind(); fCurrT = 0; } SkPath SkVarWidthStroker::getFillPath(const SkPath& path, const SkPaint& paint, const ScalarBezCurve& varWidth, const ScalarBezCurve& varWidthInner, LengthMetric lengthMetric) { const auto appendStrokes = [this](const OffsetSegments& strokes, bool needsMove) { if (needsMove) { fOuter.moveTo(strokes.fOuter.front().fPoints[0]); fInner.moveTo(strokes.fInner.front().fPoints[0]); } for (const PathSegment& seg : strokes.fOuter) { fOuter.quadTo(seg.fPoints[1], seg.fPoints[2]); } for (const PathSegment& seg : strokes.fInner) { fInner.quadTo(seg.fPoints[1], seg.fPoints[2]); } }; initForPath(path, paint); fVarWidth = varWidth; fVarWidthInner = varWidthInner; // TODO: this assumes one contour: PathVerbMeasure meas(path); const float totalPathLength = lengthMetric == LengthMetric::kPathLength ? meas.totalLength() : (path.countVerbs() - 1); // Trace the inner and outer paths simultaneously. Inner will therefore be // recorded in reverse from how we trace the outline. SkPath::Iter it(path, false); PathSegment segment, prevSegment; OffsetSegments offsetSegs, prevOffsetSegs; bool firstSegment = true, prevWasFirst = false; float lenTraveled = 0; while ((segment.fVerb = it.next(&segment.fPoints[0])) != SkPath::kDone_Verb) { const float verbLength = lengthMetric == LengthMetric::kPathLength ? (meas.currentVerbLength() / totalPathLength) : (1.0f / totalPathLength); const float tmin = lenTraveled; const float tmax = lenTraveled + verbLength; // Subset the distance function for the current interval. ScalarBezCurve partVarWidth, partVarWidthInner; fVarWidth.split(tmin, tmax, &partVarWidth); fVarWidthInner.split(tmin, tmax, &partVarWidthInner); partVarWidthInner = ScalarBezCurve::Mul(partVarWidthInner, -1); // Stroke the current segment switch (segment.fVerb) { case SkPath::kLine_Verb: case SkPath::kQuad_Verb: case SkPath::kCubic_Verb: offsetSegs = strokeSegment(segment, partVarWidth, partVarWidthInner, firstSegment); break; case SkPath::kMove_Verb: // Don't care about multiple contours currently continue; default: SkDebugf("Unhandled path verb %d\n", segment.fVerb); SkASSERT(false); break; } // Join to the previous segment if (!firstSegment) { // Append prev inner and outer strokes appendStrokes(prevOffsetSegs, prevWasFirst); // Append the join const float innerRadius = varWidthInner.eval(tmin); const float outerRadius = varWidth.eval(tmin); join(segment.fPoints[0], innerRadius, outerRadius, prevOffsetSegs, offsetSegs); } std::swap(segment, prevSegment); std::swap(offsetSegs, prevOffsetSegs); prevWasFirst = firstSegment; firstSegment = false; lenTraveled += verbLength; meas.nextVerb(); } // Finish appending final offset segments appendStrokes(prevOffsetSegs, prevWasFirst); // Open contour => endcap at the end const bool isClosed = path.isLastContourClosed(); if (isClosed) { SkDebugf("Unhandled closed contour\n"); SkASSERT(false); } else { endcap(CapLocation::End); } // Walk inner path in reverse, appending to result appendPathReversed(fInner, &fOuter); endcap(CapLocation::Start); return fOuter; } SkVarWidthStroker::OffsetSegments SkVarWidthStroker::strokeSegment( const PathSegment& segment, const ScalarBezCurve& varWidth, const ScalarBezCurve& varWidthInner, bool needsMove) { viz::outerErr.reset(nullptr); std::vector outer = strokeSegment(segment, varWidth); std::vector inner = strokeSegment(segment, varWidthInner); return {inner, outer}; } std::vector SkVarWidthStroker::strokeSegment( const PathSegment& seg, const ScalarBezCurve& distanceFunc) const { // Work item for the recursive splitting stack. struct Item { PathSegment fSeg; ScalarBezCurve fDistFnc, fDistFncSqd; ScalarBezCurve fSegX, fSegY; Item(const PathSegment& seg, const ScalarBezCurve& distFnc, const ScalarBezCurve& distFncSqd) : fSeg(seg), fDistFnc(distFnc), fDistFncSqd(distFncSqd) { const int segDegree = segmentDegree(seg); fSegX = ScalarBezCurve(segDegree); fSegY = ScalarBezCurve(segDegree); for (int i = 0; i <= segDegree; i++) { fSegX[i] = seg.fPoints[i].fX; fSegY[i] = seg.fPoints[i].fY; } } }; // Push the initial segment and distance function std::stack stack; stack.push(Item(seg, distanceFunc, ScalarBezCurve::Mul(distanceFunc, distanceFunc))); std::vector result; constexpr int kMaxIters = 5000; /** TODO: this is completely arbitrary */ int iter = 0; while (!stack.empty()) { if (iter++ >= kMaxIters) break; const Item item = stack.top(); stack.pop(); const ScalarBezCurve& distFnc = item.fDistFnc; ScalarBezCurve distFncSqd = item.fDistFncSqd; const float kTol = std::abs(0.5f * distFnc.extremumWeight()); // Compute a quad that approximates stroke outline PathSegment quadApprox; approximateSegment(item.fSeg, distFnc, &quadApprox); ScalarBezCurve quadApproxX(2), quadApproxY(2); for (int i = 0; i < 3; i++) { quadApproxX[i] = quadApprox.fPoints[i].fX; quadApproxY[i] = quadApprox.fPoints[i].fY; } // Compute control polygon for the delta(t) curve. First must elevate to a common degree. const int deltaDegree = std::max(quadApproxX.degree(), item.fSegX.degree()); ScalarBezCurve segX = item.fSegX, segY = item.fSegY; segX.elevateDegree(deltaDegree); segY.elevateDegree(deltaDegree); quadApproxX.elevateDegree(deltaDegree); quadApproxY.elevateDegree(deltaDegree); ScalarBezCurve deltaX = ScalarBezCurve::Sub(quadApproxX, segX); ScalarBezCurve deltaY = ScalarBezCurve::Sub(quadApproxY, segY); // Compute psi(t) = delta_x(t)^2 + delta_y(t)^2. ScalarBezCurve E = ScalarBezCurve::AddSquares(deltaX, deltaY); // Promote E and d(t)^2 to a common degree. const int commonDeg = std::max(distFncSqd.degree(), E.degree()); distFncSqd.elevateDegree(commonDeg); E.elevateDegree(commonDeg); // Subtract dist squared curve from E, resulting in: // eps(t) = delta_x(t)^2 + delta_y(t)^2 - d(t)^2 E.sub(distFncSqd); // Purely for debugging/testing, save the first approximation and error function: if (viz::outerErr == nullptr) { using namespace viz; outerErr = std::make_unique(E); outerFirstApprox.rewind(); outerFirstApprox.moveTo(quadApprox.fPoints[0]); outerFirstApprox.quadTo(quadApprox.fPoints[1], quadApprox.fPoints[2]); } // Compute maxErr, which is just the max coefficient of eps (using convex hull property // of bez curves) float maxAbsErr = std::abs(E.extremumWeight()); if (maxAbsErr > kTol) { PathSegment left, right; splitSegment(item.fSeg, 0.5f, &left, &right); ScalarBezCurve distFncL, distFncR; distFnc.split(0.5f, &distFncL, &distFncR); ScalarBezCurve distFncSqdL, distFncSqdR; distFncSqd.split(0.5f, &distFncSqdL, &distFncSqdR); stack.push(Item(right, distFncR, distFncSqdR)); stack.push(Item(left, distFncL, distFncSqdL)); } else { // Approximation is good enough. quadApprox.fVerb = SkPath::kQuad_Verb; result.push_back(quadApprox); } } SkASSERT(!result.empty()); return result; } void SkVarWidthStroker::endcap(CapLocation loc) { const auto buttCap = [this](CapLocation loc) { if (loc == CapLocation::Start) { // Back at the start of the path: just close the stroked outline fOuter.close(); } else { // Inner last pt == first pt when appending in reverse SkPoint innerLastPt; fInner.getLastPt(&innerLastPt); fOuter.lineTo(innerLastPt); } }; switch (fCap) { case SkPaint::kButt_Cap: buttCap(loc); break; default: SkDebugf("Unhandled endcap %d\n", fCap); buttCap(loc); break; } } void SkVarWidthStroker::join(const SkPoint& common, float innerRadius, float outerRadius, const OffsetSegments& prev, const OffsetSegments& curr) { const auto miterJoin = [this](const SkPoint& common, float leftRadius, float rightRadius, const OffsetSegments& prev, const OffsetSegments& curr) { // With variable-width stroke you can actually have a situation where both sides // need an "inner" or an "outer" join. So we call the two sides "left" and // "right" and they can each independently get an inner or outer join. const auto makeJoin = [this, &common, &prev, &curr](bool left, float radius) { SkPath* path = left ? &fOuter : &fInner; const auto& prevSegs = left ? prev.fOuter : prev.fInner; const auto& currSegs = left ? curr.fOuter : curr.fInner; SkASSERT(!prevSegs.empty()); SkASSERT(!currSegs.empty()); const SkPoint afterEndpt = currSegs.front().fPoints[0]; SkPoint before = unitNormal(prevSegs.back(), 1, nullptr); SkPoint after = unitNormal(currSegs.front(), 0, nullptr); // Don't create any join geometry if the normals are nearly identical. const float cosTheta = before.dot(after); if (!SkScalarNearlyZero(1 - cosTheta)) { bool outerJoin; if (left) { outerJoin = isClockwise(before, after); } else { before = rotate180(before); after = rotate180(after); outerJoin = !isClockwise(before, after); } if (outerJoin) { // Before and after have the same origin and magnitude, so before+after is the // diagonal of their rhombus. Origin of this vector is the midpoint of the miter // line. SkPoint miterVec = before + after; // Note the relationship (draw a right triangle with the miter line as its // hypoteneuse): // sin(theta/2) = strokeWidth / miterLength // so miterLength = strokeWidth / sin(theta/2) // where miterLength is the length of the miter from outer point to inner // corner. miterVec's origin is the midpoint of the miter line, so we use // strokeWidth/2. Sqrt is just an application of half-angle identities. const float sinHalfTheta = sqrtf(0.5 * (1 + cosTheta)); const float halfMiterLength = radius / sinHalfTheta; // TODO: miter length limit miterVec = setLength(miterVec, halfMiterLength); // Outer join: connect to the miter point, and then to t=0 of next segment. path->lineTo(common + miterVec); path->lineTo(afterEndpt); } else { // Connect to the miter midpoint (common path endpoint of the two segments), // and then to t=0 of the next segment. This adds an interior "loop" // of geometry that handles edge cases where segment lengths are shorter than // the stroke width. path->lineTo(common); path->lineTo(afterEndpt); } } }; makeJoin(true, leftRadius); makeJoin(false, rightRadius); }; switch (fJoin) { case SkPaint::kMiter_Join: miterJoin(common, innerRadius, outerRadius, prev, curr); break; default: SkDebugf("Unhandled join %d\n", fJoin); miterJoin(common, innerRadius, outerRadius, prev, curr); break; } } void SkVarWidthStroker::appendPathReversed(const SkPath& path, SkPath* result) { const int numVerbs = path.countVerbs(); const int numPoints = path.countPoints(); std::vector verbs; std::vector points; verbs.resize(numVerbs); points.resize(numPoints); path.getVerbs(verbs.data(), numVerbs); path.getPoints(points.data(), numPoints); for (int i = numVerbs - 1, j = numPoints; i >= 0; i--) { auto verb = static_cast(verbs[i]); switch (verb) { case SkPath::kLine_Verb: { j -= 1; SkASSERT(j >= 1); result->lineTo(points[j - 1]); break; } case SkPath::kQuad_Verb: { j -= 1; SkASSERT(j >= 2); result->quadTo(points[j - 1], points[j - 2]); j -= 1; break; } case SkPath::kMove_Verb: // Ignore break; default: SkASSERT(false); break; } } } int SkVarWidthStroker::segmentDegree(const PathSegment& seg) { static constexpr int lut[] = { -1, // move, 1, // line 2, // quad -1, // conic 3, // cubic -1 // done }; const int deg = lut[static_cast(seg.fVerb)]; SkASSERT(deg > 0); return deg; } void SkVarWidthStroker::splitSegment(const PathSegment& seg, float t, PathSegment* segA, PathSegment* segB) { // TODO: although general, this is a pretty slow way to do this const int degree = segmentDegree(seg); ScalarBezCurve x(degree), y(degree); for (int i = 0; i <= degree; i++) { x[i] = seg.fPoints[i].fX; y[i] = seg.fPoints[i].fY; } ScalarBezCurve leftX(degree), rightX(degree), leftY(degree), rightY(degree); x.split(t, &leftX, &rightX); y.split(t, &leftY, &rightY); segA->fVerb = segB->fVerb = seg.fVerb; for (int i = 0; i <= degree; i++) { segA->fPoints[i] = {leftX[i], leftY[i]}; segB->fPoints[i] = {rightX[i], rightY[i]}; } } void SkVarWidthStroker::approximateSegment(const PathSegment& seg, const ScalarBezCurve& distFnc, PathSegment* approxQuad) { // This is a simple control polygon transformation. // From F. Yzerman. "Precise offsetting of quadratic Bezier curves". 2019. // TODO: detect and handle more degenerate cases (e.g. linear) // TODO: Tiller-Hanson works better in many cases but does not generalize well SkPoint tangentStart, tangentEnd; SkPoint offsetStart = unitNormal(seg, 0, &tangentStart); SkPoint offsetEnd = unitNormal(seg, 1, &tangentEnd); SkPoint offsetMid = offsetStart + offsetEnd; const float radiusStart = distFnc.eval(0); const float radiusMid = distFnc.eval(0.5f); const float radiusEnd = distFnc.eval(1); offsetStart = radiusStart == 0 ? SkPoint::Make(0, 0) : setLength(offsetStart, radiusStart); offsetMid = radiusMid == 0 ? SkPoint::Make(0, 0) : setLength(offsetMid, radiusMid); offsetEnd = radiusEnd == 0 ? SkPoint::Make(0, 0) : setLength(offsetEnd, radiusEnd); SkPoint start, mid, end; switch (segmentDegree(seg)) { case 1: start = seg.fPoints[0]; end = seg.fPoints[1]; mid = (start + end) * 0.5f; break; case 2: start = seg.fPoints[0]; mid = seg.fPoints[1]; end = seg.fPoints[2]; break; case 3: start = seg.fPoints[0]; mid = (seg.fPoints[1] + seg.fPoints[2]) * 0.5f; end = seg.fPoints[3]; break; default: SkDebugf("Unhandled degree for segment approximation"); SkASSERT(false); break; } approxQuad->fPoints[0] = start + offsetStart; approxQuad->fPoints[1] = mid + offsetMid; approxQuad->fPoints[2] = end + offsetEnd; } SkPoint SkVarWidthStroker::unitNormal(const PathSegment& seg, float t, SkPoint* tangentOut) { switch (seg.fVerb) { case SkPath::kLine_Verb: { const SkPoint tangent = setLength(seg.fPoints[1] - seg.fPoints[0], 1); const SkPoint normal = rotate90(tangent); if (tangentOut) { *tangentOut = tangent; } return normal; } case SkPath::kQuad_Verb: { SkPoint tangent; if (t == 0) { tangent = seg.fPoints[1] - seg.fPoints[0]; } else if (t == 1) { tangent = seg.fPoints[2] - seg.fPoints[1]; } else { tangent = ((seg.fPoints[1] - seg.fPoints[0]) * (1 - t) + (seg.fPoints[2] - seg.fPoints[1]) * t) * 2; } if (!tangent.normalize()) { SkDebugf("Failed to normalize quad tangent\n"); SkASSERT(false); } if (tangentOut) { *tangentOut = tangent; } return rotate90(tangent); } case SkPath::kCubic_Verb: { SkPoint tangent; SkEvalCubicAt(seg.fPoints.data(), t, nullptr, &tangent, nullptr); if (!tangent.normalize()) { SkDebugf("Failed to normalize cubic tangent\n"); SkASSERT(false); } if (tangentOut) { *tangentOut = tangent; } return rotate90(tangent); } default: SkDebugf("Unhandled verb for unit normal %d\n", seg.fVerb); SkASSERT(false); return {}; } } } // namespace ////////////////////////////////////////////////////////////////////////////// class VariableWidthStroker : public Sample { public: VariableWidthStroker() : fShowHidden(true) , fShowSkeleton(true) , fShowStrokePoints(false) , fShowUI(false) , fDifferentInnerFunc(false) , fShowErrorCurve(false) { resetToDefaults(); fPtsPaint.setAntiAlias(true); fPtsPaint.setStrokeWidth(10); fPtsPaint.setStrokeCap(SkPaint::kRound_Cap); fStrokePointsPaint.setAntiAlias(true); fStrokePointsPaint.setStrokeWidth(5); fStrokePointsPaint.setStrokeCap(SkPaint::kRound_Cap); fStrokePaint.setAntiAlias(true); fStrokePaint.setStyle(SkPaint::kStroke_Style); fStrokePaint.setColor(0x80FF0000); fNewFillPaint.setAntiAlias(true); fNewFillPaint.setColor(0x8000FF00); fHiddenPaint.setAntiAlias(true); fHiddenPaint.setStyle(SkPaint::kStroke_Style); fHiddenPaint.setColor(0xFF0000FF); fSkeletonPaint.setAntiAlias(true); fSkeletonPaint.setStyle(SkPaint::kStroke_Style); fSkeletonPaint.setColor(SK_ColorRED); } private: /** Selectable menu item for choosing distance functions */ struct DistFncMenuItem { std::string fName; int fDegree; bool fSelected; std::vector fWeights; DistFncMenuItem(const std::string& name, int degree, bool selected) { fName = name; fDegree = degree; fSelected = selected; fWeights.resize(degree + 1, 1.0f); } }; SkString name() override { return SkString("VariableWidthStroker"); } void onSizeChange() override { fWinSize = SkSize::Make(this->width(), this->height()); INHERITED::onSizeChange(); } bool onChar(SkUnichar uni) override { switch (uni) { case '0': this->toggle(fShowUI); return true; case '1': this->toggle(fShowSkeleton); return true; case '2': this->toggle(fShowHidden); return true; case '3': this->toggle(fShowStrokePoints); return true; case '4': this->toggle(fShowErrorCurve); return true; case '5': this->toggle(fLengthMetric); return true; case 'x': resetToDefaults(); return true; case '-': fWidth -= 5; return true; case '=': fWidth += 5; return true; default: break; } return false; } void toggle(bool& value) { value = !value; } void toggle(SkVarWidthStroker::LengthMetric& value) { value = value == SkVarWidthStroker::LengthMetric::kPathLength ? SkVarWidthStroker::LengthMetric::kNumSegments : SkVarWidthStroker::LengthMetric::kPathLength; } void resetToDefaults() { fPathPts[0] = {300, 400}; fPathPts[1] = {500, 400}; fPathPts[2] = {700, 400}; fPathPts[3] = {900, 400}; fPathPts[4] = {1100, 400}; fWidth = 175; fLengthMetric = SkVarWidthStroker::LengthMetric::kPathLength; fDistFncs = fDefaultsDistFncs; fDistFncsInner = fDefaultsDistFncs; } void makePath(SkPath* path) { path->moveTo(fPathPts[0]); path->quadTo(fPathPts[1], fPathPts[2]); path->quadTo(fPathPts[3], fPathPts[4]); } static ScalarBezCurve makeDistFnc(const std::vector& fncs, float strokeWidth) { const float radius = strokeWidth / 2; for (const auto& df : fncs) { if (df.fSelected) { return ScalarBezCurve::Mul(ScalarBezCurve(df.fDegree, df.fWeights), radius); } } SkASSERT(false); return ScalarBezCurve(0, {radius}); } void onDrawContent(SkCanvas* canvas) override { canvas->drawColor(0xFFEEEEEE); SkPath path; this->makePath(&path); fStrokePaint.setStrokeWidth(fWidth); // Elber-Cohen stroker result ScalarBezCurve distFnc = makeDistFnc(fDistFncs, fWidth); ScalarBezCurve distFncInner = fDifferentInnerFunc ? makeDistFnc(fDistFncsInner, fWidth) : distFnc; SkVarWidthStroker stroker; SkPath fillPath = stroker.getFillPath(path, fStrokePaint, distFnc, distFncInner, fLengthMetric); fillPath.setFillType(SkPathFillType::kWinding); canvas->drawPath(fillPath, fNewFillPaint); if (fShowHidden) { canvas->drawPath(fillPath, fHiddenPaint); } if (fShowSkeleton) { canvas->drawPath(path, fSkeletonPaint); canvas->drawPoints(SkCanvas::kPoints_PointMode, fPathPts.size(), fPathPts.data(), fPtsPaint); } if (fShowStrokePoints) { drawStrokePoints(canvas, fillPath); } if (fShowUI) { drawUI(); } if (fShowErrorCurve && viz::outerErr != nullptr) { SkPaint firstApproxPaint; firstApproxPaint.setStrokeWidth(4); firstApproxPaint.setStyle(SkPaint::kStroke_Style); firstApproxPaint.setColor(SK_ColorRED); canvas->drawPath(viz::outerFirstApprox, firstApproxPaint); drawErrorCurve(canvas, *viz::outerErr); } } Sample::Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey modi) override { const SkScalar tol = 4; const SkRect r = SkRect::MakeXYWH(x - tol, y - tol, tol * 2, tol * 2); for (size_t i = 0; i < fPathPts.size(); ++i) { if (r.intersects(SkRect::MakeXYWH(fPathPts[i].fX, fPathPts[i].fY, 1, 1))) { return new Click([this, i](Click* c) { fPathPts[i] = c->fCurr; return true; }); } } return nullptr; } void drawStrokePoints(SkCanvas* canvas, const SkPath& fillPath) { SkPath::Iter it(fillPath, false); SkPoint points[4]; SkPath::Verb verb; std::vector pointsVec, ctrlPts; while ((verb = it.next(&points[0])) != SkPath::kDone_Verb) { switch (verb) { case SkPath::kLine_Verb: pointsVec.push_back(points[1]); break; case SkPath::kQuad_Verb: ctrlPts.push_back(points[1]); pointsVec.push_back(points[2]); break; case SkPath::kMove_Verb: pointsVec.push_back(points[0]); break; case SkPath::kClose_Verb: break; default: SkDebugf("Unhandled path verb %d for stroke points\n", verb); SkASSERT(false); break; } } canvas->drawPoints(SkCanvas::kPoints_PointMode, pointsVec.size(), pointsVec.data(), fStrokePointsPaint); fStrokePointsPaint.setColor(SK_ColorBLUE); fStrokePointsPaint.setStrokeWidth(3); canvas->drawPoints(SkCanvas::kPoints_PointMode, ctrlPts.size(), ctrlPts.data(), fStrokePointsPaint); fStrokePointsPaint.setColor(SK_ColorBLACK); fStrokePointsPaint.setStrokeWidth(5); } void drawErrorCurve(SkCanvas* canvas, const ScalarBezCurve& E) { const float winW = fWinSize.width() * 0.75f, winH = fWinSize.height() * 0.25f; const float padding = 25; const SkRect box = SkRect::MakeXYWH(padding, fWinSize.height() - winH - padding, winW - 2 * padding, winH); constexpr int nsegs = 100; constexpr float dt = 1.0f / nsegs; constexpr float dx = 10.0f; const int deg = E.degree(); SkPath path; for (int i = 0; i < nsegs; i++) { const float tmin = i * dt, tmax = (i + 1) * dt; ScalarBezCurve left(deg), right(deg); E.split(tmax, &left, &right); const float tRel = tmin / tmax; ScalarBezCurve rl(deg), rr(deg); left.split(tRel, &rl, &rr); const float x = i * dx; if (i == 0) { path.moveTo(x, -rr[0]); } path.lineTo(x + dx, -rr[deg]); } SkPaint paint; paint.setStyle(SkPaint::kStroke_Style); paint.setAntiAlias(true); paint.setStrokeWidth(0); paint.setColor(SK_ColorRED); const SkRect pathBounds = path.computeTightBounds(); constexpr float yAxisMax = 8000; const float sx = box.width() / pathBounds.width(); const float sy = box.height() / (2 * yAxisMax); canvas->save(); canvas->translate(box.left(), box.top() + box.height() / 2); canvas->scale(sx, sy); canvas->drawPath(path, paint); SkPath axes; axes.moveTo(0, 0); axes.lineTo(pathBounds.width(), 0); axes.moveTo(0, -yAxisMax); axes.lineTo(0, yAxisMax); paint.setColor(SK_ColorBLACK); paint.setAntiAlias(false); canvas->drawPath(axes, paint); canvas->restore(); } void drawUI() { static constexpr auto kUIOpacity = 0.35f; static constexpr float kUIWidth = 200.0f, kUIHeight = 400.0f; ImGui::SetNextWindowBgAlpha(kUIOpacity); if (ImGui::Begin("E-C Controls", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { const SkRect uiArea = SkRect::MakeXYWH(10, 10, kUIWidth, kUIHeight); ImGui::SetWindowPos(ImVec2(uiArea.x(), uiArea.y())); ImGui::SetWindowSize(ImVec2(uiArea.width(), uiArea.height())); const auto drawControls = [](std::vector& distFncs, const std::string& menuPfx, const std::string& ptPfx) { std::string degreeMenuLabel = menuPfx + ": "; for (const auto& df : distFncs) { if (df.fSelected) { degreeMenuLabel += df.fName; break; } } if (ImGui::BeginMenu(degreeMenuLabel.c_str())) { for (size_t i = 0; i < distFncs.size(); i++) { if (ImGui::MenuItem(distFncs[i].fName.c_str(), nullptr, distFncs[i].fSelected)) { for (size_t j = 0; j < distFncs.size(); j++) { distFncs[j].fSelected = j == i; } } } ImGui::EndMenu(); } for (auto& df : distFncs) { if (df.fSelected) { for (int i = 0; i <= df.fDegree; i++) { const std::string label = ptPfx + std::to_string(i); ImGui::SliderFloat(label.c_str(), &(df.fWeights[i]), 0, 1); } } } }; const std::array, 2> metrics = { std::make_pair("% path length", SkVarWidthStroker::LengthMetric::kPathLength), std::make_pair("% segment count", SkVarWidthStroker::LengthMetric::kNumSegments), }; if (ImGui::BeginMenu("Interpolation metric:")) { for (const auto& metric : metrics) { if (ImGui::MenuItem(metric.first.c_str(), nullptr, fLengthMetric == metric.second)) { fLengthMetric = metric.second; } } ImGui::EndMenu(); } drawControls(fDistFncs, "Degree", "P"); if (ImGui::CollapsingHeader("Inner stroke", true)) { fDifferentInnerFunc = true; drawControls(fDistFncsInner, "Degree (inner)", "Q"); } else { fDifferentInnerFunc = false; } } ImGui::End(); } bool fShowHidden, fShowSkeleton, fShowStrokePoints, fShowUI, fDifferentInnerFunc, fShowErrorCurve; float fWidth = 175; SkPaint fPtsPaint, fStrokePaint, fNewFillPaint, fHiddenPaint, fSkeletonPaint, fStrokePointsPaint; inline static constexpr int kNPts = 5; std::array fPathPts; SkSize fWinSize; SkVarWidthStroker::LengthMetric fLengthMetric; const std::vector fDefaultsDistFncs = { DistFncMenuItem("Linear", 1, true), DistFncMenuItem("Quadratic", 2, false), DistFncMenuItem("Cubic", 3, false), DistFncMenuItem("One Louder (11)", 11, false), DistFncMenuItem("30?!", 30, false)}; std::vector fDistFncs = fDefaultsDistFncs; std::vector fDistFncsInner = fDefaultsDistFncs; using INHERITED = Sample; }; DEF_SAMPLE(return new VariableWidthStroker;)