[skottie] Stroke dash support

AE supports dashing all strokes.  Dashes are specified as an arbitrary
number of intervals (alternating dash/gap) plus a start offset.

All values can be animated independently (but of course!).

  - implement a SkSG dash effect (based on SkDashPathEffect)
  - expand the shape builder logic to allow local geometry adjustments
    (kind of a bummer that dashing is a stroke/paint property as opposed
    to a geometry effect in AE)

Change-Id: Ic9ff35f2f9a552a3c26f9e1596ce58ad81f7ced5
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/274550
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/src/layers/shapelayer/FillStroke.cpp b/modules/skottie/src/layers/shapelayer/FillStroke.cpp
index d7301df..aa64499 100644
--- a/modules/skottie/src/layers/shapelayer/FillStroke.cpp
+++ b/modules/skottie/src/layers/shapelayer/FillStroke.cpp
@@ -10,6 +10,7 @@
 #include "modules/skottie/src/SkottiePriv.h"
 #include "modules/skottie/src/SkottieValue.h"
 #include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGDashEffect.h"
 #include "modules/sksg/include/SkSGPaint.h"
 
 namespace skottie {
@@ -87,6 +88,41 @@
     using INHERITED = DiscardableAdapterBase<FillStrokeAdapter, sksg::PaintNode>;
 };
 
+class DashAdapter final : public DiscardableAdapterBase<DashAdapter, sksg::DashEffect> {
+public:
+    DashAdapter(const skjson::ArrayValue& jdash,
+                const AnimationBuilder& abuilder,
+                sk_sp<sksg::GeometryNode> geo)
+        : INHERITED(sksg::DashEffect::Make(std::move(geo))) {
+        SkASSERT(jdash.size() > 1);
+
+        // The dash is encoded as an arbitrary number of intervals (alternating dash/gap),
+        // plus a single trailing offset.  Each value can be animated independently.
+        const auto interval_count = jdash.size() - 1;
+        fIntervals.resize(interval_count, 0);
+
+        for (size_t i = 0; i < jdash.size(); ++i) {
+            if (const skjson::ObjectValue* jint = jdash[i]) {
+                auto* target = i < interval_count
+                        ? &fIntervals[i]
+                        : &fOffset;
+                this->bind(abuilder, (*jint)["v"], target);
+            }
+        }
+    }
+
+private:
+    void onSync() override {
+        this->node()->setPhase(fOffset);
+        this->node()->setIntervals(fIntervals);
+    }
+
+    std::vector<ScalarValue> fIntervals;
+    ScalarValue              fOffset = 0;
+
+    using INHERITED = DiscardableAdapterBase<DashAdapter, sksg::DashEffect>;
+};
+
 } // namespace
 
 sk_sp<sksg::PaintNode> ShapeBuilder::AttachFill(const skjson::ObjectValue& jpaint,
@@ -129,5 +165,21 @@
     return AttachStroke(jpaint, abuilder, std::move(color_node));
 }
 
+std::vector<sk_sp<sksg::GeometryNode>> ShapeBuilder::AdjustStrokeGeometry(
+        const skjson::ObjectValue& jstroke,
+        const AnimationBuilder* abuilder,
+        std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
+
+    const skjson::ArrayValue* jdash = jstroke["d"];
+    if (jdash && jdash->size() > 1) {
+        for (size_t i = 0; i < geos.size(); ++i) {
+            geos[i] = abuilder->attachDiscardableAdapter<DashAdapter, sk_sp<sksg::GeometryNode>>(
+                          *jdash, *abuilder, std::move(geos[i]));
+        }
+    }
+
+    return std::move(geos);
+}
+
 } // namespace internal
 } // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/ShapeLayer.cpp b/modules/skottie/src/layers/shapelayer/ShapeLayer.cpp
index 73d5def..3ab9394 100644
--- a/modules/skottie/src/layers/shapelayer/ShapeLayer.cpp
+++ b/modules/skottie/src/layers/shapelayer/ShapeLayer.cpp
@@ -41,15 +41,6 @@
     ShapeBuilder::AttachPolystarGeometry,
 };
 
-using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const skjson::ObjectValue&,
-                                                  const AnimationBuilder*);
-static constexpr PaintAttacherT gPaintAttachers[] = {
-    ShapeBuilder::AttachColorFill,
-    ShapeBuilder::AttachColorStroke,
-    ShapeBuilder::AttachGradientFill,
-    ShapeBuilder::AttachGradientStroke,
-};
-
 using GeometryEffectAttacherT =
     std::vector<sk_sp<sksg::GeometryNode>> (*)(const skjson::ObjectValue&,
                                                const AnimationBuilder*,
@@ -60,6 +51,24 @@
     ShapeBuilder::AttachRoundGeometryEffect,
 };
 
+using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const skjson::ObjectValue&,
+                                                  const AnimationBuilder*);
+static constexpr PaintAttacherT gPaintAttachers[] = {
+    ShapeBuilder::AttachColorFill,
+    ShapeBuilder::AttachColorStroke,
+    ShapeBuilder::AttachGradientFill,
+    ShapeBuilder::AttachGradientStroke,
+};
+
+// Some paint types (looking at you dashed-stroke) mess with the local geometry.
+static constexpr GeometryEffectAttacherT gPaintGeometryAdjusters[] = {
+    nullptr,                             // color fill
+    ShapeBuilder::AdjustStrokeGeometry,  // color stroke
+    nullptr,                             // gradient fill
+    ShapeBuilder::AdjustStrokeGeometry,  // gradient stroke
+};
+static_assert(SK_ARRAY_COUNT(gPaintGeometryAdjusters) == SK_ARRAY_COUNT(gPaintAttachers), "");
+
 using DrawEffectAttacherT =
     std::vector<sk_sp<sksg::RenderNode>> (*)(const skjson::ObjectValue&,
                                              const AnimationBuilder*,
@@ -258,6 +267,12 @@
                 drawGeos = it->fAttach(it->fJson, this, std::move(drawGeos));
             }
 
+            // Apply local paint geometry adjustments (e.g. dashing).
+            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintGeometryAdjusters));
+            if (const auto adjuster = gPaintGeometryAdjusters[rec->fInfo.fAttacherIndex]) {
+                drawGeos = adjuster(rec->fJson, this, std::move(drawGeos));
+            }
+
             // If we still have multiple geos, reduce using 'merge'.
             auto geo = drawGeos.size() > 1
                 ? ShapeBuilder::MergeGeometry(std::move(drawGeos), sksg::Merge::Mode::kMerge)
diff --git a/modules/skottie/src/layers/shapelayer/ShapeLayer.h b/modules/skottie/src/layers/shapelayer/ShapeLayer.h
index 6a89efa..30ee12c 100644
--- a/modules/skottie/src/layers/shapelayer/ShapeLayer.h
+++ b/modules/skottie/src/layers/shapelayer/ShapeLayer.h
@@ -67,6 +67,9 @@
     static std::vector<sk_sp<sksg::GeometryNode>> AttachRoundGeometryEffect(
             const skjson::ObjectValue&, const AnimationBuilder*,
             std::vector<sk_sp<sksg::GeometryNode>>&&);
+    static std::vector<sk_sp<sksg::GeometryNode>> AdjustStrokeGeometry(
+            const skjson::ObjectValue&, const AnimationBuilder*,
+            std::vector<sk_sp<sksg::GeometryNode>>&&);
 
     static std::vector<sk_sp<sksg::RenderNode>> AttachRepeaterDrawEffect(
             const skjson::ObjectValue&,
diff --git a/modules/sksg/include/SkSGDashEffect.h b/modules/sksg/include/SkSGDashEffect.h
new file mode 100644
index 0000000..341e71e
--- /dev/null
+++ b/modules/sksg/include/SkSGDashEffect.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGDashEffect_DEFINED
+#define SkSGDashEffect_DEFINED
+
+#include "include/core/SkPath.h"
+#include "modules/sksg/include/SkSGGeometryNode.h"
+
+#include <vector>
+
+namespace  sksg {
+
+/**
+ * Apply a dash effect to the child geometry.
+ *
+ * Follows the same semantics as SkDashPathEffect, with one minor tweak: when the number of
+ * intervals is odd, they are repeated once more to attain an even sequence (same as SVG
+ * stroke-dasharray: https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty).
+ */
+class DashEffect final : public GeometryNode {
+public:
+    static sk_sp<DashEffect> Make(sk_sp<GeometryNode> child) {
+        return child ? sk_sp<DashEffect>(new DashEffect(std::move(child))) : nullptr;
+    }
+
+    ~DashEffect() override;
+
+    SG_ATTRIBUTE(Intervals, std::vector<float>, fIntervals)
+    SG_ATTRIBUTE(Phase,                 float , fPhase    )
+
+protected:
+    void onClip(SkCanvas*, bool antiAlias) const override;
+    void onDraw(SkCanvas*, const SkPaint&) const override;
+    bool onContains(const SkPoint&)        const override;
+
+    SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
+    SkPath onAsPath() const override;
+
+private:
+    explicit DashEffect(sk_sp<GeometryNode>);
+
+    const sk_sp<GeometryNode> fChild;
+
+    SkPath fDashedPath; // cache
+
+    std::vector<float> fIntervals;
+    float              fPhase;
+};
+
+} // namespace sksg
+
+#endif // SkSGDashEffect_DEFINED
diff --git a/modules/sksg/sksg.gni b/modules/sksg/sksg.gni
index b9d222d..739941b 100644
--- a/modules/sksg/sksg.gni
+++ b/modules/sksg/sksg.gni
@@ -9,6 +9,7 @@
 skia_sksg_sources = [
   "$_src/SkSGClipEffect.cpp",
   "$_src/SkSGColorFilter.cpp",
+  "$_src/SkSGDashEffect.cpp",
   "$_src/SkSGDraw.cpp",
   "$_src/SkSGEffectNode.cpp",
   "$_src/SkSGGeometryNode.cpp",
diff --git a/modules/sksg/src/SkSGDashEffect.cpp b/modules/sksg/src/SkSGDashEffect.cpp
new file mode 100644
index 0000000..2b4002c
--- /dev/null
+++ b/modules/sksg/src/SkSGDashEffect.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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 "modules/sksg/include/SkSGDashEffect.h"
+
+#include "include/core/SkCanvas.h"
+#include "include/core/SkStrokeRec.h"
+#include "include/effects/SkDashPathEffect.h"
+
+#include <algorithm>
+
+namespace sksg {
+
+namespace  {
+
+sk_sp<SkPathEffect> make_dash(const std::vector<float> intervals, float phase) {
+    if (intervals.empty()) {
+        return nullptr;
+    }
+
+    const auto* intervals_ptr   = intervals.data();
+    auto        intervals_count = intervals.size();
+
+    SkSTArray<32, float, true> storage;
+    if (intervals_count & 1) {
+        intervals_count *= 2;
+        storage.resize(intervals_count);
+        intervals_ptr = storage.data();
+
+        std::copy(intervals.begin(), intervals.end(), storage.begin());
+        std::copy(intervals.begin(), intervals.end(), storage.begin() + intervals.size());
+    }
+
+    return SkDashPathEffect::Make(intervals_ptr, SkToInt(intervals_count), phase);
+}
+
+} // namespace
+
+DashEffect::DashEffect(sk_sp<GeometryNode> child)
+    : fChild(std::move(child)) {
+    this->observeInval(fChild);
+}
+
+DashEffect::~DashEffect() {
+    this->unobserveInval(fChild);
+}
+
+void DashEffect::onClip(SkCanvas* canvas, bool antiAlias) const {
+    canvas->clipPath(fDashedPath, SkClipOp::kIntersect, antiAlias);
+}
+
+void DashEffect::onDraw(SkCanvas* canvas, const SkPaint& paint) const {
+    canvas->drawPath(fDashedPath, paint);
+}
+
+bool DashEffect::onContains(const SkPoint& p) const {
+    return fDashedPath.contains(p.x(), p.y());
+}
+
+SkPath DashEffect::onAsPath() const {
+    return fDashedPath;
+}
+
+SkRect DashEffect::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
+    SkASSERT(this->hasInval());
+
+    const auto child_bounds = fChild->revalidate(ic, ctm);
+    const auto child_path   = fChild->asPath();
+
+    fDashedPath.reset();
+
+    auto dash_patheffect = make_dash(fIntervals, fPhase);
+    SkStrokeRec rec(SkStrokeRec::kHairline_InitStyle);
+
+    if (!dash_patheffect ||
+        !dash_patheffect->filterPath(&fDashedPath, child_path, &rec, &child_bounds)) {
+        fDashedPath = std::move(child_path);
+    }
+    fDashedPath.shrinkToFit();
+
+    return fDashedPath.computeTightBounds();
+}
+
+} // namespace sksg