[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