| /* | |
| * Copyright 2020 Google LLC | |
| * | |
| * 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/SkPathBuilder.h" | |
| #include "include/core/SkPathMeasure.h" | |
| #include "include/core/SkPathUtils.h" | |
| #include "include/utils/SkParsePath.h" | |
| #include "src/core/SkGeometry.h" | |
| #include "tools/viewer/ClickHandlerSlide.h" | |
| #include <array> | |
| #include <stack> | |
| #include <vector> | |
| 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<float>& 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<float>(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<float> 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<float> ZeroSet(const ScalarBezCurve& curve) { | |
| constexpr float kTol = 0.001f; | |
| std::vector<float> 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<float>(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<float>(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<float>& 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<float>* 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<float> 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); | |
| } | |
| SkPathBuilder builder; | |
| builder.moveTo(fPreviousPoint); | |
| switch (verb) { | |
| case SkPath::kLine_Verb: | |
| builder.lineTo(pts[1]); | |
| break; | |
| case SkPath::kQuad_Verb: | |
| builder.quadTo(pts[1], pts[2]); | |
| break; | |
| case SkPath::kCubic_Verb: | |
| builder.cubicTo(pts[1], pts[2], pts[3]); | |
| break; | |
| case SkPath::kConic_Verb: | |
| builder.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 = builder.detach(); | |
| fPreviousPoint = fCurrVerb.getLastPt().value_or(SkPoint{0, 0}); | |
| fMeas.setPath(&fCurrVerb, false); | |
| } | |
| ////////////////////////////////////////////////////////////////////////////// | |
| // Several debug-only visualization helpers | |
| namespace viz { | |
| std::unique_ptr<ScalarBezCurve> 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<SkPoint, 4> fPoints; | |
| }; | |
| struct OffsetSegments { | |
| std::vector<PathSegment> fInner; | |
| std::vector<PathSegment> 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<PathSegment> 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, SkPathBuilder* 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; | |
| SkPathBuilder 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.reset(); | |
| fOuter.reset(); | |
| 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.snapshot(), &fOuter); | |
| endcap(CapLocation::Start); | |
| return fOuter.snapshot(); | |
| } | |
| SkVarWidthStroker::OffsetSegments SkVarWidthStroker::strokeSegment( | |
| const PathSegment& segment, | |
| const ScalarBezCurve& varWidth, | |
| const ScalarBezCurve& varWidthInner, | |
| bool needsMove) { | |
| viz::outerErr.reset(nullptr); | |
| std::vector<PathSegment> outer = strokeSegment(segment, varWidth); | |
| std::vector<PathSegment> inner = strokeSegment(segment, varWidthInner); | |
| return {inner, outer}; | |
| } | |
| std::vector<SkVarWidthStroker::PathSegment> 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<Item> stack; | |
| stack.push(Item(seg, distanceFunc, ScalarBezCurve::Mul(distanceFunc, distanceFunc))); | |
| std::vector<PathSegment> 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<ScalarBezCurve>(E); | |
| outerFirstApprox = SkPathBuilder() | |
| .moveTo(quadApprox.fPoints[0]) | |
| .quadTo(quadApprox.fPoints[1], quadApprox.fPoints[2]) | |
| .detach(); | |
| } | |
| // 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().value_or(SkPoint{0, 0}); | |
| 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) { | |
| SkPathBuilder* 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, SkPathBuilder* result) { | |
| SkSpan<const SkPoint> points = path.points(); | |
| SkSpan<const SkPathVerb> verbs = path.verbs(); | |
| const int numPoints = SkToInt(points.size()); | |
| const int numVerbs = SkToInt(verbs.size()); | |
| for (int i = numVerbs - 1, j = numPoints; i >= 0; i--) { | |
| auto verb = verbs[i]; | |
| switch (verb) { | |
| case SkPathVerb::kLine: { | |
| j -= 1; | |
| SkASSERT(j >= 1); | |
| result->lineTo(points[j - 1]); | |
| break; | |
| } | |
| case SkPathVerb::kQuad: { | |
| j -= 1; | |
| SkASSERT(j >= 2); | |
| result->quadTo(points[j - 1], points[j - 2]); | |
| j -= 1; | |
| break; | |
| } | |
| case SkPathVerb::kMove: | |
| // Ignore | |
| break; | |
| default: | |
| SkASSERT(false); | |
| break; | |
| } | |
| } | |
| } | |
| int SkVarWidthStroker::segmentDegree(const PathSegment& seg) { | |
| static constexpr auto lut = std::to_array<int>({ | |
| -1, // move, | |
| 1, // line | |
| 2, // quad | |
| -1, // conic | |
| 3, // cubic | |
| -1 // done | |
| }); | |
| const int deg = lut[static_cast<uint8_t>(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 VariableWidthStrokerSlide : public ClickHandlerSlide { | |
| public: | |
| VariableWidthStrokerSlide() | |
| : 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); | |
| fName = "VariableWidthStroker"; | |
| } | |
| void load(SkScalar w, SkScalar h) override { fWinSize = {w, h}; } | |
| void resize(SkScalar w, SkScalar h) override { fWinSize = {w, h}; } | |
| 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 draw(SkCanvas* canvas) override { | |
| canvas->drawColor(0xFFEEEEEE); | |
| SkPath path = this->makePath(); | |
| 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, 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); | |
| } | |
| } | |
| protected: | |
| 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; | |
| } | |
| bool onClick(ClickHandlerSlide::Click *) override { return false; } | |
| private: | |
| /** Selectable menu item for choosing distance functions */ | |
| struct DistFncMenuItem { | |
| std::string fName; | |
| int fDegree; | |
| bool fSelected; | |
| std::vector<float> fWeights; | |
| DistFncMenuItem(const std::string& name, int degree, bool selected) { | |
| fName = name; | |
| fDegree = degree; | |
| fSelected = selected; | |
| fWeights.resize(degree + 1, 1.0f); | |
| } | |
| }; | |
| 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; | |
| } | |
| SkPath makePath() { | |
| return SkPathBuilder() | |
| .moveTo(fPathPts[0]) | |
| .quadTo(fPathPts[1], fPathPts[2]) | |
| .quadTo(fPathPts[3], fPathPts[4]) | |
| .detach(); | |
| } | |
| static ScalarBezCurve makeDistFnc(const std::vector<DistFncMenuItem>& 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 drawStrokePoints(SkCanvas* canvas, const SkPath& fillPath) { | |
| SkPath::Iter it(fillPath, false); | |
| SkPoint points[4]; | |
| SkPath::Verb verb; | |
| std::vector<SkPoint> 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, fStrokePointsPaint); | |
| fStrokePointsPaint.setColor(SK_ColorBLUE); | |
| fStrokePointsPaint.setStrokeWidth(3); | |
| canvas->drawPoints(SkCanvas::kPoints_PointMode, ctrlPts, 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(); | |
| SkPathBuilder builder; | |
| 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) { | |
| builder.moveTo(x, -rr[0]); | |
| } | |
| builder.lineTo(x + dx, -rr[deg]); | |
| } | |
| SkPath path = builder.detach(); | |
| 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 = SkPathBuilder() | |
| .moveTo(0, 0) | |
| .lineTo(pathBounds.width(), 0) | |
| .moveTo(0, -yAxisMax) | |
| .lineTo(0, yAxisMax) | |
| .detach(); | |
| 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<DistFncMenuItem>& 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<std::pair<std::string, SkVarWidthStroker::LengthMetric>, 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<SkPoint, kNPts> fPathPts; | |
| SkSize fWinSize; | |
| SkVarWidthStroker::LengthMetric fLengthMetric; | |
| const std::vector<DistFncMenuItem> 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<DistFncMenuItem> fDistFncs = fDefaultsDistFncs; | |
| std::vector<DistFncMenuItem> fDistFncsInner = fDefaultsDistFncs; | |
| }; | |
| DEF_SLIDE(return new VariableWidthStrokerSlide;) |