Add support for arc-length metric in variable width stroke sample

I left in the ability to swap between using % arc length versus dividing
t evenly up across path segments so we can easily visually compare the
effect.

Change-Id: Id83792aa9e22fd5464e956092bac0baec389ffed
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/330103
Reviewed-by: Chris Dalton <csmartdalton@google.com>
Commit-Queue: Tyler Denniston <tdenniston@google.com>
diff --git a/samplecode/SampleVariableWidthStroker.cpp b/samplecode/SampleVariableWidthStroker.cpp
index b09257a..6f5c07c 100644
--- a/samplecode/SampleVariableWidthStroker.cpp
+++ b/samplecode/SampleVariableWidthStroker.cpp
@@ -9,6 +9,7 @@
 #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"
 
@@ -317,6 +318,74 @@
     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);
+    }
+
+    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<ScalarBezCurve> outerErr;
@@ -339,6 +408,14 @@
  */
 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.
@@ -354,7 +431,8 @@
     SkPath getFillPath(const SkPath& path,
                        const SkPaint& paint,
                        const ScalarBezCurve& varWidth,
-                       const ScalarBezCurve& varWidthInner);
+                       const ScalarBezCurve& varWidthInner,
+                       LengthMetric lengthMetric = LengthMetric::kNumSegments);
 
 private:
     /** Helper struct referring to a single segment of an SkPath */
@@ -448,7 +526,8 @@
 SkPath SkVarWidthStroker::getFillPath(const SkPath& path,
                                       const SkPaint& paint,
                                       const ScalarBezCurve& varWidth,
-                                      const ScalarBezCurve& varWidthInner) {
+                                      const ScalarBezCurve& varWidthInner,
+                                      LengthMetric lengthMetric) {
     const auto appendStrokes = [this](const OffsetSegments& strokes, bool needsMove) {
         if (needsMove) {
             fOuter.moveTo(strokes.fOuter.front().fPoints[0]);
@@ -468,8 +547,11 @@
     fVarWidth = varWidth;
     fVarWidthInner = varWidthInner;
 
-    // TODO: this assumes one contour: one move + some number of segs.
-    const int numSegs = path.countVerbs() - 1;
+    // 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.
@@ -478,13 +560,15 @@
     OffsetSegments offsetSegs, prevOffsetSegs;
     bool firstSegment = true, prevWasFirst = false;
 
-    const float dtDist = 1.0f / numSegs;
-    float tDist = 0;
+    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.
-        // TODO: Currently each path segment gets an even portion of the distance function,
-        //       but we could investigate using arc-length proportions instead.
-        const float tmin = tDist, tmax = tDist + dtDist;
         ScalarBezCurve partVarWidth, partVarWidthInner;
         fVarWidth.split(tmin, tmax, &partVarWidth);
         fVarWidthInner.split(tmin, tmax, &partVarWidthInner);
@@ -522,7 +606,8 @@
         std::swap(offsetSegs, prevOffsetSegs);
         prevWasFirst = firstSegment;
         firstSegment = false;
-        tDist += dtDist;
+        lenTraveled += verbLength;
+        meas.nextVerb();
     }
 
     // Finish appending final offset segments
@@ -1006,6 +1091,9 @@
             case '4':
                 this->toggle(fShowErrorCurve);
                 return true;
+            case '5':
+                this->toggle(fLengthMetric);
+                return true;
             case 'x':
                 resetToDefaults();
                 return true;
@@ -1022,6 +1110,11 @@
     }
 
     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};
@@ -1032,6 +1125,7 @@
 
         fWidth = 175;
 
+        fLengthMetric = SkVarWidthStroker::LengthMetric::kPathLength;
         fDistFncs = fDefaultsDistFncs;
         fDistFncsInner = fDefaultsDistFncs;
     }
@@ -1066,7 +1160,8 @@
         ScalarBezCurve distFncInner =
                 fDifferentInnerFunc ? makeDistFnc(fDistFncsInner, fWidth) : distFnc;
         SkVarWidthStroker stroker;
-        SkPath fillPath = stroker.getFillPath(path, fStrokePaint, distFnc, distFncInner);
+        SkPath fillPath =
+                stroker.getFillPath(path, fStrokePaint, distFnc, distFncInner, fLengthMetric);
         fillPath.setFillType(SkPathFillType::kWinding);
         canvas->drawPath(fillPath, fNewFillPaint);
 
@@ -1243,9 +1338,24 @@
                 }
             };
 
+            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("Demo part 2", true)) {
+            if (ImGui::CollapsingHeader("Inner stroke", true)) {
                 fDifferentInnerFunc = true;
                 drawControls(fDistFncsInner, "Degree (inner)", "Q");
             } else {
@@ -1263,6 +1373,7 @@
     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),