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),