Pass CTM to path effects (experimental)

Add an overload to SkPathEffect that can be used when the CTM is known
at the callsite. GPU callsites are not handled here, that will be
tackled in a separate CL.

Path effects must implement the filterPath virtual that accepts the CTM,
although they are not obligated to use it. If a path effect does
use the CTM, the output geometry must be in the original coordinate
space, not device space.

Bug: skia:11957
Change-Id: I01615985599fe2736de954bb10dac881b0554ae7
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/420239
Commit-Queue: Tyler Denniston <tdenniston@google.com>
Reviewed-by: Mike Reed <reed@google.com>
diff --git a/gm/patheffects.cpp b/gm/patheffects.cpp
index aabe3de..801ef71 100644
--- a/gm/patheffects.cpp
+++ b/gm/patheffects.cpp
@@ -288,3 +288,124 @@
         canvas->translate(0, 150);
     }
 }
+
+//////////////////////////////////////////////////////////////////////////////
+
+#include "include/core/SkStrokeRec.h"
+#include "src/core/SkPathEffectBase.h"
+
+namespace {
+/**
+ * Example path effect using CTM. This "strokes" a single line segment with some stroke width,
+ * and then inflates the result by some number of pixels.
+ */
+class StrokeLineInflated : public SkPathEffectBase {
+public:
+    StrokeLineInflated(float strokeWidth, float pxInflate)
+            : fRadius(strokeWidth / 2.f), fPxInflate(pxInflate) {}
+
+    bool onNeedsCTM() const final { return true; }
+
+    bool onFilterPath(SkPath* dst,
+                      const SkPath& src,
+                      SkStrokeRec* rec,
+                      const SkRect* cullR,
+                      const SkMatrix& ctm) const final {
+        SkASSERT(src.countPoints() == 2);
+        const SkPoint pts[2] = {src.getPoint(0), src.getPoint(1)};
+
+        SkMatrix invCtm;
+        if (!ctm.invert(&invCtm)) {
+            return false;
+        }
+
+        // For a line segment, we can just map the (scaled) normal vector to pixel-space,
+        // increase its length by the desired number of pixels, and then map back to canvas space.
+        SkPoint n = {pts[0].fY - pts[1].fY, pts[1].fX - pts[0].fX};
+        if (!n.setLength(fRadius)) {
+            return false;
+        }
+
+        SkPoint mappedN = ctm.mapVector(n.fX, n.fY);
+        if (!mappedN.setLength(mappedN.length() + fPxInflate)) {
+            return false;
+        }
+        n = invCtm.mapVector(mappedN.fX, mappedN.fY);
+
+        dst->moveTo(pts[0] + n);
+        dst->lineTo(pts[1] + n);
+        dst->lineTo(pts[1] - n);
+        dst->lineTo(pts[0] - n);
+        dst->close();
+
+        rec->setFillStyle();
+
+        return true;
+    }
+
+protected:
+    void flatten(SkWriteBuffer&) const final {}
+
+private:
+    SK_FLATTENABLE_HOOKS(StrokeLineInflated)
+
+    bool computeFastBounds(SkRect* bounds) const final { return false; }
+
+    const float fRadius;
+    const float fPxInflate;
+};
+
+sk_sp<SkFlattenable> StrokeLineInflated::CreateProc(SkReadBuffer&) { return nullptr; }
+
+}  // namespace
+
+class CTMPathEffectGM : public skiagm::GM {
+protected:
+    SkString onShortName() override { return SkString("ctmpatheffect"); }
+
+    SkISize onISize() override { return SkISize::Make(800, 600); }
+
+    // TODO: ctm-aware path effects are currently CPU only
+    DrawResult onGpuSetup(GrDirectContext* dctx, SkString*) override {
+        return dctx == nullptr ? DrawResult::kOk : DrawResult::kSkip;
+    }
+
+    void onDraw(SkCanvas* canvas) override {
+        const float strokeWidth = 16;
+        const float pxInflate = 0.5f;
+        sk_sp<SkPathEffect> pathEffect(new StrokeLineInflated(strokeWidth, pxInflate));
+
+        SkPath path;
+        path.moveTo(100, 100);
+        path.lineTo(200, 200);
+
+        // Draw the inflated path, and a scaled version, in blue.
+        SkPaint paint;
+        paint.setAntiAlias(true);
+        paint.setColor(SkColorSetA(SK_ColorBLUE, 0xff));
+        paint.setPathEffect(pathEffect);
+        canvas->drawPath(path, paint);
+        canvas->save();
+        canvas->translate(150, 0);
+        canvas->scale(2.5, 0.5f);
+        canvas->drawPath(path, paint);
+        canvas->restore();
+
+        // Draw the regular stroked version on top in green.
+        // The inflated version should be visible underneath as a blue "border".
+        paint.setPathEffect(nullptr);
+        paint.setStyle(SkPaint::kStroke_Style);
+        paint.setStrokeWidth(strokeWidth);
+        paint.setColor(SkColorSetA(SK_ColorGREEN, 0xff));
+        canvas->drawPath(path, paint);
+        canvas->save();
+        canvas->translate(150, 0);
+        canvas->scale(2.5, 0.5f);
+        canvas->drawPath(path, paint);
+        canvas->restore();
+    }
+
+private:
+    using INHERITED = GM;
+};
+DEF_GM(return new CTMPathEffectGM;)