[skottie] Cleanup: convert shape layer adapters to new pattern

Split off most shape layer components into own CUs (naming convention
following AE), and convert to new DiscardableAdapter pattern.

TBR=
Change-Id: Iba7800cff1998d3d7cf81dfd89b4193d02b59559
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/265147
Reviewed-by: Florin Malita <fmalita@chromium.org>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/skottie.gni b/modules/skottie/skottie.gni
index 8d65f41..b9dade6 100644
--- a/modules/skottie/skottie.gni
+++ b/modules/skottie/skottie.gni
@@ -13,6 +13,7 @@
 ]
 
 skia_skottie_sources = [
+  "$_src/Adapter.h",
   "$_src/Animator.cpp",
   "$_src/Animator.h",
   "$_src/Camera.cpp",
@@ -22,7 +23,6 @@
   "$_src/Layer.cpp",
   "$_src/Layer.h",
   "$_src/Skottie.cpp",
-  "$_src/SkottieAdapter.cpp",
   "$_src/SkottieAdapter.h",
   "$_src/SkottieAnimator.cpp",
   "$_src/SkottieJson.cpp",
@@ -55,9 +55,18 @@
   "$_src/layers/ImageLayer.cpp",
   "$_src/layers/NullLayer.cpp",
   "$_src/layers/PrecompLayer.cpp",
-  "$_src/layers/ShapeLayer.cpp",
   "$_src/layers/SolidLayer.cpp",
   "$_src/layers/TextLayer.cpp",
+  "$_src/layers/shapelayer/Ellipse.cpp",
+  "$_src/layers/shapelayer/Gradient.cpp",
+  "$_src/layers/shapelayer/MergePaths.cpp",
+  "$_src/layers/shapelayer/Polystar.cpp",
+  "$_src/layers/shapelayer/Rectangle.cpp",
+  "$_src/layers/shapelayer/Repeater.cpp",
+  "$_src/layers/shapelayer/RoundCorners.cpp",
+  "$_src/layers/shapelayer/ShapeLayer.cpp",
+  "$_src/layers/shapelayer/ShapeLayer.h",
+  "$_src/layers/shapelayer/TrimPaths.cpp",
   "$_src/text/RangeSelector.cpp",
   "$_src/text/RangeSelector.h",
   "$_src/text/SkottieShaper.cpp",
diff --git a/modules/skottie/src/Adapter.h b/modules/skottie/src/Adapter.h
new file mode 100644
index 0000000..dd75129
--- /dev/null
+++ b/modules/skottie/src/Adapter.h
@@ -0,0 +1,40 @@
+/*
+ * 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 SkottieAdapter_DEFINED
+#define SkottieAdapter_DEFINED
+
+#include "modules/skottie/src/Animator.h"
+
+namespace skottie {
+namespace internal {
+
+template <typename AdapterT, typename T>
+class DiscardableAdapterBase : public AnimatablePropertyContainer {
+public:
+    template <typename... Args>
+    static sk_sp<AdapterT> Make(Args&&... args) {
+        return sk_sp<AdapterT>(new AdapterT(std::forward<Args>(args)...));
+    }
+
+    const sk_sp<T>& node() const { return fNode; }
+
+protected:
+    DiscardableAdapterBase()
+        : fNode(T::Make()) {}
+
+    explicit DiscardableAdapterBase(sk_sp<T> node)
+        : fNode(std::move(node)) {}
+
+private:
+    const sk_sp<T> fNode;
+};
+
+} // namespace internal
+} // namespace skottie
+
+#endif // SkottieAdapter_DEFINED
diff --git a/modules/skottie/src/Animator.h b/modules/skottie/src/Animator.h
index 9061113..9470591 100644
--- a/modules/skottie/src/Animator.h
+++ b/modules/skottie/src/Animator.h
@@ -29,6 +29,11 @@
     template <typename T>
     bool bind(const AnimationBuilder&, const skjson::ObjectValue*, T*);
 
+    template <typename T>
+    bool bind(const AnimationBuilder& abuilder, const skjson::ObjectValue* jobject, T& v) {
+        return this->bind<T>(abuilder, jobject, &v);
+    }
+
     bool isStatic() const { return fAnimators.empty(); }
 
 protected:
diff --git a/modules/skottie/src/SkottieAdapter.cpp b/modules/skottie/src/SkottieAdapter.cpp
deleted file mode 100644
index fae0432..0000000
--- a/modules/skottie/src/SkottieAdapter.cpp
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright 2018 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "modules/skottie/src/SkottieAdapter.h"
-
-#include "include/core/SkFont.h"
-#include "include/core/SkMatrix.h"
-#include "include/core/SkMatrix44.h"
-#include "include/core/SkPath.h"
-#include "include/core/SkRRect.h"
-#include "include/private/SkTo.h"
-#include "include/utils/Sk3D.h"
-#include "modules/skottie/src/SkottieValue.h"
-#include "modules/skottie/src/Transform.h"
-#include "modules/sksg/include/SkSGDraw.h"
-#include "modules/sksg/include/SkSGGradient.h"
-#include "modules/sksg/include/SkSGGroup.h"
-#include "modules/sksg/include/SkSGPaint.h"
-#include "modules/sksg/include/SkSGPath.h"
-#include "modules/sksg/include/SkSGRect.h"
-#include "modules/sksg/include/SkSGTransform.h"
-#include "modules/sksg/include/SkSGTrimEffect.h"
-
-#include <cmath>
-#include <utility>
-
-namespace skottie {
-
-RRectAdapter::RRectAdapter(sk_sp<sksg::RRect> wrapped_node)
-    : fRRectNode(std::move(wrapped_node)) {}
-
-RRectAdapter::~RRectAdapter() = default;
-
-void RRectAdapter::apply() {
-    // BM "position" == "center position"
-    auto rr = SkRRect::MakeRectXY(SkRect::MakeXYWH(fPosition.x() - fSize.width() / 2,
-                                                   fPosition.y() - fSize.height() / 2,
-                                                   fSize.width(), fSize.height()),
-                                  fRadius.width(),
-                                  fRadius.height());
-   fRRectNode->setRRect(rr);
-}
-
-RepeaterAdapter::RepeaterAdapter(sk_sp<sksg::RenderNode> repeater_node, Composite composite)
-    : fRepeaterNode(repeater_node)
-    , fComposite(composite)
-    , fRoot(sksg::Group::Make()) {}
-
-RepeaterAdapter::~RepeaterAdapter() = default;
-
-void RepeaterAdapter::apply() {
-    static constexpr SkScalar kMaxCount = 512;
-    const auto count = static_cast<size_t>(SkTPin(fCount, 0.0f, kMaxCount) + 0.5f);
-
-    const auto& compute_transform = [this] (size_t index) {
-        const auto t = fOffset + index;
-
-        // Position, scale & rotation are "scaled" by index/offset.
-        SkMatrix m = SkMatrix::MakeTrans(-fAnchorPoint.x(),
-                                         -fAnchorPoint.y());
-        m.postScale(std::pow(fScale.x() * .01f, fOffset),
-                    std::pow(fScale.y() * .01f, fOffset));
-        m.postRotate(t * fRotation);
-        m.postTranslate(t * fPosition.x() + fAnchorPoint.x(),
-                        t * fPosition.y() + fAnchorPoint.y());
-
-        return m;
-    };
-
-    // TODO: start/end opacity support.
-
-    // TODO: we can avoid rebuilding all the fragments in most cases.
-    fRoot->clear();
-    for (size_t i = 0; i < count; ++i) {
-        const auto insert_index = (fComposite == Composite::kAbove) ? i : count - i - 1;
-        fRoot->addChild(sksg::TransformEffect::Make(fRepeaterNode,
-                                                    compute_transform(insert_index)));
-    }
-}
-
-PolyStarAdapter::PolyStarAdapter(sk_sp<sksg::Path> wrapped_node, Type t)
-    : fPathNode(std::move(wrapped_node))
-    , fType(t) {}
-
-PolyStarAdapter::~PolyStarAdapter() = default;
-
-void PolyStarAdapter::apply() {
-    static constexpr int kMaxPointCount = 100000;
-    const auto count = SkToUInt(SkTPin(SkScalarRoundToInt(fPointCount), 0, kMaxPointCount));
-    const auto arc   = sk_ieee_float_divide(SK_ScalarPI * 2, count);
-
-    const auto pt_on_circle = [](const SkPoint& c, SkScalar r, SkScalar a) {
-        return SkPoint::Make(c.x() + r * std::cos(a),
-                             c.y() + r * std::sin(a));
-    };
-
-    // TODO: inner/outer "roundness"?
-
-    SkPath poly;
-
-    auto angle = SkDegreesToRadians(fRotation - 90);
-    poly.moveTo(pt_on_circle(fPosition, fOuterRadius, angle));
-    poly.incReserve(fType == Type::kStar ? count * 2 : count);
-
-    for (unsigned i = 0; i < count; ++i) {
-        if (fType == Type::kStar) {
-            poly.lineTo(pt_on_circle(fPosition, fInnerRadius, angle + arc * 0.5f));
-        }
-        angle += arc;
-        poly.lineTo(pt_on_circle(fPosition, fOuterRadius, angle));
-    }
-
-    poly.close();
-    fPathNode->setPath(poly);
-}
-
-GradientAdapter::GradientAdapter(sk_sp<sksg::Gradient> grad, size_t colorStopCount)
-    : fGradient(std::move(grad))
-    , fColorStopCount(colorStopCount) {}
-
-template <typename T>
-static inline const T* next_rec(const T* rec, const T* end_rec) {
-    if (!rec) return nullptr;
-
-    SkASSERT(rec < end_rec);
-    rec++;
-
-    return rec < end_rec ? rec : nullptr;
-};
-
-void GradientAdapter::apply() {
-    this->onApply();
-
-    // Gradient color stops are specified as a consolidated float vector holding:
-    //
-    //   a) an (optional) array of color/RGB stop records (t, r, g, b)
-    //
-    // followed by
-    //
-    //   b) an (optional) array of opacity/alpha stop records (t, a)
-    //
-    struct   ColorRec { float t, r, g, b; };
-    struct OpacityRec { float t, a;       };
-
-    // The number of color records is explicit (fColorStopCount),
-    // while the number of opacity stops is implicit (based on the size of fStops).
-    //
-    // |fStops| holds ColorRec x |fColorStopCount| + OpacityRec x N
-    const auto c_count = fColorStopCount,
-               c_size  = c_count * 4,
-               o_count = (fStops.size() - c_size) / 2;
-    if (fStops.size() < c_size || fStops.size() != (c_count * 4 + o_count * 2)) {
-        // apply() may get called before the stops are set, so only log when we have some stops.
-        if (!fStops.empty()) {
-            SkDebugf("!! Invalid gradient stop array size: %zu\n", fStops.size());
-        }
-        return;
-    }
-
-    const auto* c_rec = c_count > 0 ? reinterpret_cast<const ColorRec*>(fStops.data())
-                                    : nullptr;
-    const auto* o_rec = o_count > 0 ? reinterpret_cast<const OpacityRec*>(fStops.data() + c_size)
-                                    : nullptr;
-    const auto* c_end = c_rec + c_count;
-    const auto* o_end = o_rec + o_count;
-
-    sksg::Gradient::ColorStop current_stop = {
-        0.0f, {
-            c_rec ? c_rec->r : 0,
-            c_rec ? c_rec->g : 0,
-            c_rec ? c_rec->b : 0,
-            o_rec ? o_rec->a : 1,
-    }};
-
-    std::vector<sksg::Gradient::ColorStop> stops;
-    stops.reserve(c_count);
-
-    // Merge-sort the color and opacity stops, LERP-ing intermediate channel values as needed.
-    while (c_rec || o_rec) {
-        // After exhausting one of color recs / opacity recs, continue propagating the last
-        // computed values (as if they were specified at the current position).
-        const auto& cs = c_rec
-                ? *c_rec
-                : ColorRec{ o_rec->t,
-                            current_stop.fColor.fR,
-                            current_stop.fColor.fG,
-                            current_stop.fColor.fB };
-        const auto& os = o_rec
-                ? *o_rec
-                : OpacityRec{ c_rec->t, current_stop.fColor.fA };
-
-        // Compute component lerp coefficients based on the relative position of the stops
-        // being considered. The idea is to select the smaller-pos stop, use its own properties
-        // as specified (lerp with t == 1), and lerp (with t < 1) the properties from the
-        // larger-pos stop against the previously computed gradient stop values.
-        const auto     c_pos = std::max(cs.t, current_stop.fPosition),
-                       o_pos = std::max(os.t, current_stop.fPosition),
-                   c_pos_rel = c_pos - current_stop.fPosition,
-                   o_pos_rel = o_pos - current_stop.fPosition,
-                         t_c = SkTPin(sk_ieee_float_divide(o_pos_rel, c_pos_rel), 0.0f, 1.0f),
-                         t_o = SkTPin(sk_ieee_float_divide(c_pos_rel, o_pos_rel), 0.0f, 1.0f);
-
-        auto lerp = [](float a, float b, float t) { return a + t * (b - a); };
-
-        current_stop = {
-                std::min(c_pos, o_pos),
-                {
-                    lerp(current_stop.fColor.fR, cs.r, t_c ),
-                    lerp(current_stop.fColor.fG, cs.g, t_c ),
-                    lerp(current_stop.fColor.fB, cs.b, t_c ),
-                    lerp(current_stop.fColor.fA, os.a, t_o)
-                }
-        };
-        stops.push_back(current_stop);
-
-        // Consume one of, or both (for coincident positions) color/opacity stops.
-        if (c_pos <= o_pos) {
-            c_rec = next_rec<ColorRec>(c_rec, c_end);
-        }
-        if (o_pos <= c_pos) {
-            o_rec = next_rec<OpacityRec>(o_rec, o_end);
-        }
-    }
-
-    stops.shrink_to_fit();
-    fGradient->setColorStops(std::move(stops));
-}
-
-LinearGradientAdapter::LinearGradientAdapter(sk_sp<sksg::LinearGradient> grad, size_t stopCount)
-    : INHERITED(std::move(grad), stopCount) {}
-
-void LinearGradientAdapter::onApply() {
-    auto* grad = static_cast<sksg::LinearGradient*>(fGradient.get());
-    grad->setStartPoint(this->startPoint());
-    grad->setEndPoint(this->endPoint());
-}
-
-RadialGradientAdapter::RadialGradientAdapter(sk_sp<sksg::RadialGradient> grad, size_t stopCount)
-    : INHERITED(std::move(grad), stopCount) {}
-
-void RadialGradientAdapter::onApply() {
-    auto* grad = static_cast<sksg::RadialGradient*>(fGradient.get());
-    grad->setStartCenter(this->startPoint());
-    grad->setEndCenter(this->startPoint());
-    grad->setStartRadius(0);
-    grad->setEndRadius(SkPoint::Distance(this->startPoint(), this->endPoint()));
-}
-
-TrimEffectAdapter::TrimEffectAdapter(sk_sp<sksg::TrimEffect> trimEffect)
-    : fTrimEffect(std::move(trimEffect)) {
-    SkASSERT(fTrimEffect);
-}
-
-TrimEffectAdapter::~TrimEffectAdapter() = default;
-
-void TrimEffectAdapter::apply() {
-    // BM semantics: start/end are percentages, offset is "degrees" (?!).
-    const auto  start = fStart  / 100,
-                  end = fEnd    / 100,
-               offset = fOffset / 360;
-
-    auto startT = SkTMin(start, end) + offset,
-          stopT = SkTMax(start, end) + offset;
-    auto   mode = SkTrimPathEffect::Mode::kNormal;
-
-    if (stopT - startT < 1) {
-        startT -= SkScalarFloorToScalar(startT);
-        stopT  -= SkScalarFloorToScalar(stopT);
-
-        if (startT > stopT) {
-            using std::swap;
-            swap(startT, stopT);
-            mode = SkTrimPathEffect::Mode::kInverted;
-        }
-    } else {
-        startT = 0;
-        stopT  = 1;
-    }
-
-    fTrimEffect->setStart(startT);
-    fTrimEffect->setStop(stopT);
-    fTrimEffect->setMode(mode);
-}
-
-} // namespace skottie
diff --git a/modules/skottie/src/SkottieAdapter.h b/modules/skottie/src/SkottieAdapter.h
index 85e2647..7d1b549 100644
--- a/modules/skottie/src/SkottieAdapter.h
+++ b/modules/skottie/src/SkottieAdapter.h
@@ -5,8 +5,8 @@
  * found in the LICENSE file.
  */
 
-#ifndef SkottieAdapter_DEFINED
-#define SkottieAdapter_DEFINED
+#ifndef SkottieAdapter__DEFINED
+#define SkottieAdapter__DEFINED
 
 #include "include/core/SkPoint.h"
 #include "include/core/SkRefCnt.h"
@@ -56,131 +56,6 @@
     p_type f##p_name = p_default;                   \
   public:
 
-class RRectAdapter final : public SkNVRefCnt<RRectAdapter> {
-public:
-    explicit RRectAdapter(sk_sp<sksg::RRect>);
-    ~RRectAdapter();
-
-    ADAPTER_PROPERTY(Position, SkPoint , SkPoint::Make(0, 0))
-    ADAPTER_PROPERTY(Size    , SkSize  ,  SkSize::Make(0, 0))
-    ADAPTER_PROPERTY(Radius  , SkSize  ,  SkSize::Make(0, 0))
-
-private:
-    void apply();
-
-    sk_sp<sksg::RRect> fRRectNode;
-};
-
-class PolyStarAdapter final : public SkNVRefCnt<PolyStarAdapter> {
-public:
-    enum class Type {
-        kStar, kPoly,
-    };
-
-    PolyStarAdapter(sk_sp<sksg::Path>, Type);
-    ~PolyStarAdapter();
-
-    ADAPTER_PROPERTY(Position      , SkPoint , SkPoint::Make(0, 0))
-    ADAPTER_PROPERTY(PointCount    , SkScalar, 0)
-    ADAPTER_PROPERTY(InnerRadius   , SkScalar, 0)
-    ADAPTER_PROPERTY(OuterRadius   , SkScalar, 0)
-    ADAPTER_PROPERTY(InnerRoundness, SkScalar, 0)
-    ADAPTER_PROPERTY(OuterRoundness, SkScalar, 0)
-    ADAPTER_PROPERTY(Rotation      , SkScalar, 0)
-
-private:
-    void apply();
-
-    sk_sp<sksg::Path> fPathNode;
-    Type              fType;
-};
-
-class RepeaterAdapter final : public SkNVRefCnt<RepeaterAdapter> {
-public:
-    enum class Composite { kAbove, kBelow };
-
-    RepeaterAdapter(sk_sp<sksg::RenderNode>, Composite);
-    ~RepeaterAdapter();
-
-    // Repeater props
-    ADAPTER_PROPERTY(Count       , SkScalar, 0)
-    ADAPTER_PROPERTY(Offset      , SkScalar, 0)
-
-    // Transform props
-    ADAPTER_PROPERTY(AnchorPoint , SkPoint , SkPoint::Make(0, 0))
-    ADAPTER_PROPERTY(Position    , SkPoint , SkPoint::Make(0, 0))
-    ADAPTER_PROPERTY(Scale       , SkVector, SkPoint::Make(100, 100))
-    ADAPTER_PROPERTY(Rotation    , SkScalar, 0)
-    ADAPTER_PROPERTY(StartOpacity, SkScalar, 100)
-    ADAPTER_PROPERTY(EndOpacity  , SkScalar, 100)
-
-    const sk_sp<sksg::Group>& root() const { return fRoot; }
-
-private:
-    void apply();
-
-    const sk_sp<sksg::RenderNode> fRepeaterNode;
-    const Composite               fComposite;
-
-    sk_sp<sksg::Group>            fRoot;
-};
-
-class GradientAdapter : public SkRefCnt {
-public:
-    ADAPTER_PROPERTY(StartPoint, SkPoint        , SkPoint::Make(0, 0)   )
-    ADAPTER_PROPERTY(EndPoint  , SkPoint        , SkPoint::Make(0, 0)   )
-    ADAPTER_PROPERTY(Stops     , VectorValue    , VectorValue()         )
-
-protected:
-    GradientAdapter(sk_sp<sksg::Gradient>, size_t colorStopCount);
-
-    const SkPoint& startPoint() const { return fStartPoint; }
-    const SkPoint& endPoint()   const { return fEndPoint;   }
-
-    sk_sp<sksg::Gradient> fGradient;
-    size_t                fColorStopCount;
-
-    virtual void onApply() = 0;
-
-private:
-    void apply();
-};
-
-class LinearGradientAdapter final : public GradientAdapter {
-public:
-    LinearGradientAdapter(sk_sp<sksg::LinearGradient>, size_t stopCount);
-
-private:
-    void onApply() override;
-
-    using INHERITED = GradientAdapter;
-};
-
-class RadialGradientAdapter final : public GradientAdapter {
-public:
-    RadialGradientAdapter(sk_sp<sksg::RadialGradient>, size_t stopCount);
-
-private:
-    void onApply() override;
-
-    using INHERITED = GradientAdapter;
-};
-
-class TrimEffectAdapter final : public SkNVRefCnt<TrimEffectAdapter> {
-public:
-    explicit TrimEffectAdapter(sk_sp<sksg::TrimEffect>);
-    ~TrimEffectAdapter();
-
-    ADAPTER_PROPERTY(Start , SkScalar,   0)
-    ADAPTER_PROPERTY(End   , SkScalar, 100)
-    ADAPTER_PROPERTY(Offset, SkScalar,   0)
-
-private:
-    void apply();
-
-    sk_sp<sksg::TrimEffect> fTrimEffect;
-};
-
 } // namespace skottie
 
-#endif // SkottieAdapter_DEFINED
+#endif // SkottieAdapter__DEFINED
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index 50b5908..dbe127a 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -115,10 +115,10 @@
         AnimatorScope*          fPrevScope;
     };
 
-    template <typename T,  typename... Args>
-    sk_sp<sksg::RenderNode> attachDiscardableAdapter(Args&&... args) const {
+    template <typename T,  typename NodeType = sk_sp<sksg::RenderNode>, typename... Args>
+    NodeType attachDiscardableAdapter(Args&&... args) const {
         if (auto adapter = T::Make(std::forward<Args>(args)...)) {
-            sk_sp<sksg::RenderNode> node = adapter->renderNode();
+            auto node = adapter->node();
             if (adapter->isStatic()) {
                 // Fire off a synthetic tick to force a single SG sync before discarding.
                 adapter->tick(0);
diff --git a/modules/skottie/src/effects/DropShadowEffect.cpp b/modules/skottie/src/effects/DropShadowEffect.cpp
index d376699..565a0ea 100644
--- a/modules/skottie/src/effects/DropShadowEffect.cpp
+++ b/modules/skottie/src/effects/DropShadowEffect.cpp
@@ -25,7 +25,7 @@
         return sk_sp<DropShadowAdapter>(new DropShadowAdapter(jprops, std::move(layer), abuilder));
     }
 
-    const sk_sp<sksg::RenderNode>& renderNode() const { return fImageFilterEffect; }
+    const sk_sp<sksg::RenderNode>& node() const { return fImageFilterEffect; }
 
 private:
     DropShadowAdapter(const skjson::ArrayValue& jprops,
diff --git a/modules/skottie/src/effects/Effects.h b/modules/skottie/src/effects/Effects.h
index 6470a84..b6c0b15 100644
--- a/modules/skottie/src/effects/Effects.h
+++ b/modules/skottie/src/effects/Effects.h
@@ -77,7 +77,7 @@
  */
 class MaskFilterEffectBase : public AnimatablePropertyContainer {
 public:
-    const sk_sp<sksg::MaskFilterEffect>& renderNode() const { return fMaskEffectNode; }
+    const sk_sp<sksg::MaskFilterEffect>& node() const { return fMaskEffectNode; }
 
 protected:
     MaskFilterEffectBase(sk_sp<sksg::RenderNode>, const SkSize&);
diff --git a/modules/skottie/src/effects/GaussianBlurEffect.cpp b/modules/skottie/src/effects/GaussianBlurEffect.cpp
index ffee42b..cfa0854 100644
--- a/modules/skottie/src/effects/GaussianBlurEffect.cpp
+++ b/modules/skottie/src/effects/GaussianBlurEffect.cpp
@@ -27,7 +27,7 @@
                                                                               abuilder));
     }
 
-    const sk_sp<sksg::RenderNode>& renderNode() const { return fImageFilterEffect; }
+    const sk_sp<sksg::RenderNode>& node() const { return fImageFilterEffect; }
 
 private:
     GaussianBlurEffectAdapter(const skjson::ArrayValue& jprops,
diff --git a/modules/skottie/src/effects/GradientEffect.cpp b/modules/skottie/src/effects/GradientEffect.cpp
index 7812041..7fbc9a7 100644
--- a/modules/skottie/src/effects/GradientEffect.cpp
+++ b/modules/skottie/src/effects/GradientEffect.cpp
@@ -28,7 +28,7 @@
                                                                               abuilder));
     }
 
-    sk_sp<sksg::RenderNode> renderNode() const { return fShaderEffect; }
+    sk_sp<sksg::RenderNode> node() const { return fShaderEffect; }
 
 private:
     GradientRampEffectAdapter(const skjson::ArrayValue& jprops,
diff --git a/modules/skottie/src/effects/HueSaturationEffect.cpp b/modules/skottie/src/effects/HueSaturationEffect.cpp
index acf540f..f80954b 100644
--- a/modules/skottie/src/effects/HueSaturationEffect.cpp
+++ b/modules/skottie/src/effects/HueSaturationEffect.cpp
@@ -28,7 +28,7 @@
                     new HueSaturationEffectAdapter(jprops, std::move(layer), abuilder));
     }
 
-    const sk_sp<sksg::ExternalColorFilter>& renderNode() const { return fColorFilter; }
+    const sk_sp<sksg::ExternalColorFilter>& node() const { return fColorFilter; }
 
 private:
     HueSaturationEffectAdapter(const skjson::ArrayValue& jprops,
diff --git a/modules/skottie/src/effects/InvertEffect.cpp b/modules/skottie/src/effects/InvertEffect.cpp
index 41a64a8..3851aba 100644
--- a/modules/skottie/src/effects/InvertEffect.cpp
+++ b/modules/skottie/src/effects/InvertEffect.cpp
@@ -26,7 +26,7 @@
                     new InvertEffectAdapter(jprops, std::move(layer), abuilder));
     }
 
-    const sk_sp<sksg::ExternalColorFilter>& renderNode() const { return fColorFilter; }
+    const sk_sp<sksg::ExternalColorFilter>& node() const { return fColorFilter; }
 
 private:
     InvertEffectAdapter(const skjson::ArrayValue& jprops,
diff --git a/modules/skottie/src/effects/LevelsEffect.cpp b/modules/skottie/src/effects/LevelsEffect.cpp
index 367a157..c44e1a4 100644
--- a/modules/skottie/src/effects/LevelsEffect.cpp
+++ b/modules/skottie/src/effects/LevelsEffect.cpp
@@ -43,7 +43,7 @@
                                                                   abuilder));
     }
 
-    sk_sp<sksg::RenderNode> renderNode() const { return fEffect; }
+    sk_sp<sksg::RenderNode> node() const { return fEffect; }
 
 private:
     LevelsEffectAdapter(const skjson::ArrayValue& jprops,
diff --git a/modules/skottie/src/effects/ShiftChannelsEffect.cpp b/modules/skottie/src/effects/ShiftChannelsEffect.cpp
index 668d356..4711193 100644
--- a/modules/skottie/src/effects/ShiftChannelsEffect.cpp
+++ b/modules/skottie/src/effects/ShiftChannelsEffect.cpp
@@ -35,7 +35,7 @@
                     new ShiftChannelsEffectAdapter(jprops, std::move(layer), abuilder));
     }
 
-    const sk_sp<sksg::ExternalColorFilter>& renderNode() const { return fColorFilter; }
+    const sk_sp<sksg::ExternalColorFilter>& node() const { return fColorFilter; }
 
 private:
     ShiftChannelsEffectAdapter(const skjson::ArrayValue& jprops,
diff --git a/modules/skottie/src/layers/ShapeLayer.cpp b/modules/skottie/src/layers/ShapeLayer.cpp
deleted file mode 100644
index 97a96da..0000000
--- a/modules/skottie/src/layers/ShapeLayer.cpp
+++ /dev/null
@@ -1,727 +0,0 @@
-/*
- * Copyright 2018 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "modules/skottie/src/SkottiePriv.h"
-
-#include "include/core/SkPath.h"
-#include "modules/skottie/src/SkottieAdapter.h"
-#include "modules/skottie/src/SkottieJson.h"
-#include "modules/skottie/src/SkottieValue.h"
-#include "modules/sksg/include/SkSGDraw.h"
-#include "modules/sksg/include/SkSGGeometryTransform.h"
-#include "modules/sksg/include/SkSGGradient.h"
-#include "modules/sksg/include/SkSGGroup.h"
-#include "modules/sksg/include/SkSGMerge.h"
-#include "modules/sksg/include/SkSGPaint.h"
-#include "modules/sksg/include/SkSGPath.h"
-#include "modules/sksg/include/SkSGRect.h"
-#include "modules/sksg/include/SkSGRenderEffect.h"
-#include "modules/sksg/include/SkSGRoundEffect.h"
-#include "modules/sksg/include/SkSGTransform.h"
-#include "modules/sksg/include/SkSGTrimEffect.h"
-#include "src/utils/SkJSON.h"
-
-#include <algorithm>
-#include <iterator>
-
-namespace skottie {
-namespace internal {
-
-namespace {
-
-sk_sp<sksg::GeometryNode> AttachPathGeometry(const skjson::ObjectValue& jpath,
-                                             const AnimationBuilder* abuilder) {
-    return abuilder->attachPath(jpath["ks"]);
-}
-
-sk_sp<sksg::GeometryNode> AttachRRectGeometry(const skjson::ObjectValue& jrect,
-                                              const AnimationBuilder* abuilder) {
-    auto rect_node = sksg::RRect::Make();
-    rect_node->setDirection(ParseDefault(jrect["d"], -1) == 3 ? SkPathDirection::kCCW
-                                                              : SkPathDirection::kCW);
-    rect_node->setInitialPointIndex(2); // starting point: (Right, Top - radius.y)
-
-    auto adapter = sk_make_sp<RRectAdapter>(rect_node);
-
-    auto p_attached = abuilder->bindProperty<VectorValue>(jrect["p"],
-        [adapter](const VectorValue& p) {
-            adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
-        });
-    auto s_attached = abuilder->bindProperty<VectorValue>(jrect["s"],
-        [adapter](const VectorValue& s) {
-            adapter->setSize(ValueTraits<VectorValue>::As<SkSize>(s));
-        });
-    auto r_attached = abuilder->bindProperty<ScalarValue>(jrect["r"],
-        [adapter](const ScalarValue& r) {
-            adapter->setRadius(SkSize::Make(r, r));
-        });
-
-    if (!p_attached && !s_attached && !r_attached) {
-        return nullptr;
-    }
-
-    return rect_node;
-}
-
-sk_sp<sksg::GeometryNode> AttachEllipseGeometry(const skjson::ObjectValue& jellipse,
-                                                const AnimationBuilder* abuilder) {
-    auto rect_node = sksg::RRect::Make();
-    rect_node->setDirection(ParseDefault(jellipse["d"], -1) == 3 ? SkPathDirection::kCCW
-                                                                 : SkPathDirection::kCW);
-    rect_node->setInitialPointIndex(1); // starting point: (Center, Top)
-
-    auto adapter = sk_make_sp<RRectAdapter>(rect_node);
-
-    auto p_attached = abuilder->bindProperty<VectorValue>(jellipse["p"],
-        [adapter](const VectorValue& p) {
-            adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
-        });
-    auto s_attached = abuilder->bindProperty<VectorValue>(jellipse["s"],
-        [adapter](const VectorValue& s) {
-            const auto sz = ValueTraits<VectorValue>::As<SkSize>(s);
-            adapter->setSize(sz);
-            adapter->setRadius(SkSize::Make(sz.width() / 2, sz.height() / 2));
-        });
-
-    if (!p_attached && !s_attached) {
-        return nullptr;
-    }
-
-    return rect_node;
-}
-
-sk_sp<sksg::GeometryNode> AttachPolystarGeometry(const skjson::ObjectValue& jstar,
-                                                 const AnimationBuilder* abuilder) {
-    static constexpr PolyStarAdapter::Type gTypes[] = {
-        PolyStarAdapter::Type::kStar, // "sy": 1
-        PolyStarAdapter::Type::kPoly, // "sy": 2
-    };
-
-    const auto type = ParseDefault<size_t>(jstar["sy"], 0) - 1;
-    if (type >= SK_ARRAY_COUNT(gTypes)) {
-        abuilder->log(Logger::Level::kError, &jstar, "Unknown polystar type.");
-        return nullptr;
-    }
-
-    auto path_node = sksg::Path::Make();
-    auto adapter = sk_make_sp<PolyStarAdapter>(path_node, gTypes[type]);
-
-    abuilder->bindProperty<VectorValue>(jstar["p"],
-        [adapter](const VectorValue& p) {
-            adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
-        });
-    abuilder->bindProperty<ScalarValue>(jstar["pt"],
-        [adapter](const ScalarValue& pt) {
-            adapter->setPointCount(pt);
-        });
-    abuilder->bindProperty<ScalarValue>(jstar["ir"],
-        [adapter](const ScalarValue& ir) {
-            adapter->setInnerRadius(ir);
-        });
-    abuilder->bindProperty<ScalarValue>(jstar["or"],
-        [adapter](const ScalarValue& otr) {
-            adapter->setOuterRadius(otr);
-        });
-    abuilder->bindProperty<ScalarValue>(jstar["is"],
-        [adapter](const ScalarValue& is) {
-            adapter->setInnerRoundness(is);
-        });
-    abuilder->bindProperty<ScalarValue>(jstar["os"],
-        [adapter](const ScalarValue& os) {
-            adapter->setOuterRoundness(os);
-        });
-    abuilder->bindProperty<ScalarValue>(jstar["r"],
-        [adapter](const ScalarValue& r) {
-            adapter->setRotation(r);
-        });
-
-    return path_node;
-}
-
-sk_sp<sksg::ShaderPaint> AttachGradient(const skjson::ObjectValue& jgrad,
-                                        const AnimationBuilder* abuilder) {
-    const skjson::ObjectValue* stops = jgrad["g"];
-    if (!stops)
-        return nullptr;
-
-    const auto stopCount = ParseDefault<int>((*stops)["p"], -1);
-    if (stopCount < 0)
-        return nullptr;
-
-    sk_sp<sksg::Gradient> gradient_node;
-    sk_sp<GradientAdapter> adapter;
-
-    if (ParseDefault<int>(jgrad["t"], 1) == 1) {
-        auto linear_node = sksg::LinearGradient::Make();
-        adapter = sk_make_sp<LinearGradientAdapter>(linear_node, stopCount);
-        gradient_node = std::move(linear_node);
-    } else {
-        auto radial_node = sksg::RadialGradient::Make();
-        adapter = sk_make_sp<RadialGradientAdapter>(radial_node, stopCount);
-
-        // TODO: highlight, angle
-        gradient_node = std::move(radial_node);
-    }
-
-    abuilder->bindProperty<VectorValue>((*stops)["k"],
-        [adapter](const VectorValue& stops) {
-            adapter->setStops(stops);
-        });
-    abuilder->bindProperty<VectorValue>(jgrad["s"],
-        [adapter](const VectorValue& s) {
-            adapter->setStartPoint(ValueTraits<VectorValue>::As<SkPoint>(s));
-        });
-    abuilder->bindProperty<VectorValue>(jgrad["e"],
-        [adapter](const VectorValue& e) {
-            adapter->setEndPoint(ValueTraits<VectorValue>::As<SkPoint>(e));
-        });
-
-    return sksg::ShaderPaint::Make(std::move(gradient_node));
-}
-
-sk_sp<sksg::PaintNode> AttachPaint(const skjson::ObjectValue& jpaint,
-                                   const AnimationBuilder* abuilder,
-                                   sk_sp<sksg::PaintNode> paint_node) {
-    if (paint_node) {
-        paint_node->setAntiAlias(true);
-
-        abuilder->bindProperty<ScalarValue>(jpaint["o"],
-            [paint_node](const ScalarValue& o) {
-                // BM opacity is [0..100]
-                paint_node->setOpacity(o * 0.01f);
-            });
-    }
-
-    return paint_node;
-}
-
-sk_sp<sksg::PaintNode> AttachStroke(const skjson::ObjectValue& jstroke,
-                                    const AnimationBuilder* abuilder,
-                                    sk_sp<sksg::PaintNode> stroke_node) {
-    if (!stroke_node)
-        return nullptr;
-
-    stroke_node->setStyle(SkPaint::kStroke_Style);
-
-    abuilder->bindProperty<ScalarValue>(jstroke["w"],
-        [stroke_node](const ScalarValue& w) {
-            stroke_node->setStrokeWidth(w);
-        });
-
-    stroke_node->setStrokeMiter(ParseDefault<SkScalar>(jstroke["ml"], 4.0f));
-
-    static constexpr SkPaint::Join gJoins[] = {
-        SkPaint::kMiter_Join,
-        SkPaint::kRound_Join,
-        SkPaint::kBevel_Join,
-    };
-    stroke_node->setStrokeJoin(gJoins[SkTMin<size_t>(ParseDefault<size_t>(jstroke["lj"], 1) - 1,
-                                                     SK_ARRAY_COUNT(gJoins) - 1)]);
-
-    static constexpr SkPaint::Cap gCaps[] = {
-        SkPaint::kButt_Cap,
-        SkPaint::kRound_Cap,
-        SkPaint::kSquare_Cap,
-    };
-    stroke_node->setStrokeCap(gCaps[SkTMin<size_t>(ParseDefault<size_t>(jstroke["lc"], 1) - 1,
-                                                   SK_ARRAY_COUNT(gCaps) - 1)]);
-
-    return stroke_node;
-}
-
-sk_sp<sksg::PaintNode> AttachColorFill(const skjson::ObjectValue& jfill,
-                                       const AnimationBuilder* abuilder) {
-    return AttachPaint(jfill, abuilder, abuilder->attachColor(jfill, "c"));
-}
-
-sk_sp<sksg::PaintNode> AttachGradientFill(const skjson::ObjectValue& jfill,
-                                          const AnimationBuilder* abuilder) {
-    return AttachPaint(jfill, abuilder, AttachGradient(jfill, abuilder));
-}
-
-sk_sp<sksg::PaintNode> AttachColorStroke(const skjson::ObjectValue& jstroke,
-                                         const AnimationBuilder* abuilder) {
-    return AttachStroke(jstroke, abuilder, AttachPaint(jstroke, abuilder,
-                                                       abuilder->attachColor(jstroke, "c")));
-}
-
-sk_sp<sksg::PaintNode> AttachGradientStroke(const skjson::ObjectValue& jstroke,
-                                            const AnimationBuilder* abuilder) {
-    return AttachStroke(jstroke, abuilder, AttachPaint(jstroke, abuilder,
-                                                       AttachGradient(jstroke, abuilder)));
-}
-
-sk_sp<sksg::Merge> Merge(std::vector<sk_sp<sksg::GeometryNode>>&& geos, sksg::Merge::Mode mode) {
-    std::vector<sksg::Merge::Rec> merge_recs;
-    merge_recs.reserve(geos.size());
-
-    for (auto& geo : geos) {
-        merge_recs.push_back(
-            {std::move(geo), merge_recs.empty() ? sksg::Merge::Mode::kMerge : mode});
-    }
-
-    return sksg::Merge::Make(std::move(merge_recs));
-}
-
-std::vector<sk_sp<sksg::GeometryNode>> AttachMergeGeometryEffect(
-        const skjson::ObjectValue& jmerge, const AnimationBuilder*,
-        std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
-    static constexpr sksg::Merge::Mode gModes[] = {
-        sksg::Merge::Mode::kMerge,      // "mm": 1
-        sksg::Merge::Mode::kUnion,      // "mm": 2
-        sksg::Merge::Mode::kDifference, // "mm": 3
-        sksg::Merge::Mode::kIntersect,  // "mm": 4
-        sksg::Merge::Mode::kXOR      ,  // "mm": 5
-    };
-
-    const auto mode = gModes[SkTMin<size_t>(ParseDefault<size_t>(jmerge["mm"], 1) - 1,
-                                            SK_ARRAY_COUNT(gModes) - 1)];
-
-    std::vector<sk_sp<sksg::GeometryNode>> merged;
-    merged.push_back(Merge(std::move(geos), mode));
-
-    return merged;
-}
-
-std::vector<sk_sp<sksg::GeometryNode>> AttachTrimGeometryEffect(
-        const skjson::ObjectValue& jtrim, const AnimationBuilder* abuilder,
-        std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
-
-    enum class Mode {
-        kParallel, // "m": 1 (Trim Multiple Shapes: Simultaneously)
-        kSerial,   // "m": 2 (Trim Multiple Shapes: Individually)
-    } gModes[] = { Mode::kParallel, Mode::kSerial};
-
-    const auto mode = gModes[SkTMin<size_t>(ParseDefault<size_t>(jtrim["m"], 1) - 1,
-                                            SK_ARRAY_COUNT(gModes) - 1)];
-
-    std::vector<sk_sp<sksg::GeometryNode>> inputs;
-    if (mode == Mode::kSerial) {
-        inputs.push_back(Merge(std::move(geos), sksg::Merge::Mode::kMerge));
-    } else {
-        inputs = std::move(geos);
-    }
-
-    std::vector<sk_sp<sksg::GeometryNode>> trimmed;
-    trimmed.reserve(inputs.size());
-    for (const auto& i : inputs) {
-        auto trimEffect = sksg::TrimEffect::Make(i);
-        trimmed.push_back(trimEffect);
-
-        const auto adapter = sk_make_sp<TrimEffectAdapter>(std::move(trimEffect));
-        abuilder->bindProperty<ScalarValue>(jtrim["s"],
-            [adapter](const ScalarValue& s) {
-                adapter->setStart(s);
-            });
-        abuilder->bindProperty<ScalarValue>(jtrim["e"],
-            [adapter](const ScalarValue& e) {
-                adapter->setEnd(e);
-            });
-        abuilder->bindProperty<ScalarValue>(jtrim["o"],
-            [adapter](const ScalarValue& o) {
-                adapter->setOffset(o);
-            });
-    }
-
-    return trimmed;
-}
-
-std::vector<sk_sp<sksg::GeometryNode>> AttachRoundGeometryEffect(
-        const skjson::ObjectValue& jtrim, const AnimationBuilder* abuilder,
-        std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
-
-    std::vector<sk_sp<sksg::GeometryNode>> rounded;
-    rounded.reserve(geos.size());
-
-    for (auto& g : geos) {
-        const auto roundEffect = sksg::RoundEffect::Make(std::move(g));
-        rounded.push_back(roundEffect);
-
-        abuilder->bindProperty<ScalarValue>(jtrim["r"],
-            [roundEffect](const ScalarValue& r) {
-                roundEffect->setRadius(r);
-            });
-    }
-
-    return rounded;
-}
-
-std::vector<sk_sp<sksg::RenderNode>> AttachRepeaterDrawEffect(
-        const skjson::ObjectValue& jrepeater,
-        const AnimationBuilder* abuilder,
-        std::vector<sk_sp<sksg::RenderNode>>&& draws) {
-
-    std::vector<sk_sp<sksg::RenderNode>> repeater_draws;
-
-    if (const skjson::ObjectValue* jtransform = jrepeater["tr"]) {
-        sk_sp<sksg::RenderNode> repeater_node;
-        if (draws.size() > 1) {
-            repeater_node = sksg::Group::Make(std::move(draws));
-        } else {
-            repeater_node = std::move(draws[0]);
-        }
-
-        const auto repeater_composite = (ParseDefault(jrepeater["m"], 1) == 1)
-                ? RepeaterAdapter::Composite::kAbove
-                : RepeaterAdapter::Composite::kBelow;
-
-        auto adapter = sk_make_sp<RepeaterAdapter>(std::move(repeater_node),
-                                                   repeater_composite);
-
-        abuilder->bindProperty<ScalarValue>(jrepeater["c"],
-            [adapter](const ScalarValue& c) {
-                adapter->setCount(c);
-            });
-        abuilder->bindProperty<ScalarValue>(jrepeater["o"],
-            [adapter](const ScalarValue& o) {
-                adapter->setOffset(o);
-            });
-        abuilder->bindProperty<VectorValue>((*jtransform)["a"],
-            [adapter](const VectorValue& a) {
-                adapter->setAnchorPoint(ValueTraits<VectorValue>::As<SkPoint>(a));
-            });
-        abuilder->bindProperty<VectorValue>((*jtransform)["p"],
-            [adapter](const VectorValue& p) {
-                adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
-            });
-        abuilder->bindProperty<VectorValue>((*jtransform)["s"],
-            [adapter](const VectorValue& s) {
-                adapter->setScale(ValueTraits<VectorValue>::As<SkVector>(s));
-            });
-        abuilder->bindProperty<ScalarValue>((*jtransform)["r"],
-            [adapter](const ScalarValue& r) {
-                adapter->setRotation(r);
-            });
-        abuilder->bindProperty<ScalarValue>((*jtransform)["so"],
-            [adapter](const ScalarValue& so) {
-                adapter->setStartOpacity(so);
-            });
-        abuilder->bindProperty<ScalarValue>((*jtransform)["eo"],
-            [adapter](const ScalarValue& eo) {
-                adapter->setEndOpacity(eo);
-            });
-
-        repeater_draws.reserve(1);
-        repeater_draws.push_back(adapter->root());
-    } else {
-        repeater_draws = std::move(draws);
-    }
-
-    return repeater_draws;
-}
-
-using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const skjson::ObjectValue&,
-                                                        const AnimationBuilder*);
-static constexpr GeometryAttacherT gGeometryAttachers[] = {
-    AttachPathGeometry,
-    AttachRRectGeometry,
-    AttachEllipseGeometry,
-    AttachPolystarGeometry,
-};
-
-using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const skjson::ObjectValue&,
-                                                  const AnimationBuilder*);
-static constexpr PaintAttacherT gPaintAttachers[] = {
-    AttachColorFill,
-    AttachColorStroke,
-    AttachGradientFill,
-    AttachGradientStroke,
-};
-
-using GeometryEffectAttacherT =
-    std::vector<sk_sp<sksg::GeometryNode>> (*)(const skjson::ObjectValue&,
-                                               const AnimationBuilder*,
-                                               std::vector<sk_sp<sksg::GeometryNode>>&&);
-static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = {
-    AttachMergeGeometryEffect,
-    AttachTrimGeometryEffect,
-    AttachRoundGeometryEffect,
-};
-
-using DrawEffectAttacherT =
-    std::vector<sk_sp<sksg::RenderNode>> (*)(const skjson::ObjectValue&,
-                                             const AnimationBuilder*,
-                                             std::vector<sk_sp<sksg::RenderNode>>&&);
-
-static constexpr DrawEffectAttacherT gDrawEffectAttachers[] = {
-    AttachRepeaterDrawEffect,
-};
-
-enum class ShapeType {
-    kGeometry,
-    kGeometryEffect,
-    kPaint,
-    kGroup,
-    kTransform,
-    kDrawEffect,
-};
-
-struct ShapeInfo {
-    const char* fTypeString;
-    ShapeType   fShapeType;
-    uint32_t    fAttacherIndex; // index into respective attacher tables
-};
-
-const ShapeInfo* FindShapeInfo(const skjson::ObjectValue& jshape) {
-    static constexpr ShapeInfo gShapeInfo[] = {
-        { "el", ShapeType::kGeometry      , 2 }, // ellipse   -> AttachEllipseGeometry
-        { "fl", ShapeType::kPaint         , 0 }, // fill      -> AttachColorFill
-        { "gf", ShapeType::kPaint         , 2 }, // gfill     -> AttachGradientFill
-        { "gr", ShapeType::kGroup         , 0 }, // group     -> Inline handler
-        { "gs", ShapeType::kPaint         , 3 }, // gstroke   -> AttachGradientStroke
-        { "mm", ShapeType::kGeometryEffect, 0 }, // merge     -> AttachMergeGeometryEffect
-        { "rc", ShapeType::kGeometry      , 1 }, // rrect     -> AttachRRectGeometry
-        { "rd", ShapeType::kGeometryEffect, 2 }, // round     -> AttachRoundGeometryEffect
-        { "rp", ShapeType::kDrawEffect    , 0 }, // repeater  -> AttachRepeaterDrawEffect
-        { "sh", ShapeType::kGeometry      , 0 }, // shape     -> AttachPathGeometry
-        { "sr", ShapeType::kGeometry      , 3 }, // polystar  -> AttachPolyStarGeometry
-        { "st", ShapeType::kPaint         , 1 }, // stroke    -> AttachColorStroke
-        { "tm", ShapeType::kGeometryEffect, 1 }, // trim      -> AttachTrimGeometryEffect
-        { "tr", ShapeType::kTransform     , 0 }, // transform -> Inline handler
-    };
-
-    const skjson::StringValue* type = jshape["ty"];
-    if (!type) {
-        return nullptr;
-    }
-
-    const auto* info = bsearch(type->begin(),
-                               gShapeInfo,
-                               SK_ARRAY_COUNT(gShapeInfo),
-                               sizeof(ShapeInfo),
-                               [](const void* key, const void* info) {
-                                  return strcmp(static_cast<const char*>(key),
-                                                static_cast<const ShapeInfo*>(info)->fTypeString);
-                               });
-
-    return static_cast<const ShapeInfo*>(info);
-}
-
-struct GeometryEffectRec {
-    const skjson::ObjectValue& fJson;
-    GeometryEffectAttacherT    fAttach;
-};
-
-} // namespace
-
-struct AnimationBuilder::AttachShapeContext {
-    AttachShapeContext(std::vector<sk_sp<sksg::GeometryNode>>* geos,
-                       std::vector<GeometryEffectRec>* effects,
-                       size_t committedAnimators)
-        : fGeometryStack(geos)
-        , fGeometryEffectStack(effects)
-        , fCommittedAnimators(committedAnimators) {}
-
-    std::vector<sk_sp<sksg::GeometryNode>>* fGeometryStack;
-    std::vector<GeometryEffectRec>*         fGeometryEffectStack;
-    size_t                                  fCommittedAnimators;
-};
-
-sk_sp<sksg::RenderNode> AnimationBuilder::attachShape(const skjson::ArrayValue* jshape,
-                                                      AttachShapeContext* ctx) const {
-    if (!jshape)
-        return nullptr;
-
-    SkDEBUGCODE(const auto initialGeometryEffects = ctx->fGeometryEffectStack->size();)
-
-    const skjson::ObjectValue* jtransform = nullptr;
-
-    struct ShapeRec {
-        const skjson::ObjectValue& fJson;
-        const ShapeInfo&           fInfo;
-    };
-
-    // First pass (bottom->top):
-    //
-    //   * pick up the group transform and opacity
-    //   * push local geometry effects onto the stack
-    //   * store recs for next pass
-    //
-    std::vector<ShapeRec> recs;
-    for (size_t i = 0; i < jshape->size(); ++i) {
-        const skjson::ObjectValue* shape = (*jshape)[jshape->size() - 1 - i];
-        if (!shape) continue;
-
-        const auto* info = FindShapeInfo(*shape);
-        if (!info) {
-            this->log(Logger::Level::kError, &(*shape)["ty"], "Unknown shape.");
-            continue;
-        }
-
-        if (ParseDefault<bool>((*shape)["hd"], false)) {
-            // Ignore hidden shapes.
-            continue;
-        }
-
-        recs.push_back({ *shape, *info });
-
-        switch (info->fShapeType) {
-        case ShapeType::kTransform:
-            // Just track the transform property for now -- we'll deal with it later.
-            jtransform = shape;
-            break;
-        case ShapeType::kGeometryEffect:
-            SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
-            ctx->fGeometryEffectStack->push_back(
-                { *shape, gGeometryEffectAttachers[info->fAttacherIndex] });
-            break;
-        default:
-            break;
-        }
-    }
-
-    // Second pass (top -> bottom, after 2x reverse):
-    //
-    //   * track local geometry
-    //   * emit local paints
-    //
-    std::vector<sk_sp<sksg::GeometryNode>> geos;
-    std::vector<sk_sp<sksg::RenderNode  >> draws;
-
-    const auto add_draw = [this, &draws](sk_sp<sksg::RenderNode> draw, const ShapeRec& rec) {
-        // All draws can have an optional blend mode.
-        draws.push_back(this->attachBlendMode(rec.fJson, std::move(draw)));
-    };
-
-    for (auto rec = recs.rbegin(); rec != recs.rend(); ++rec) {
-        const AutoPropertyTracker apt(this, rec->fJson);
-
-        switch (rec->fInfo.fShapeType) {
-        case ShapeType::kGeometry: {
-            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers));
-            if (auto geo = gGeometryAttachers[rec->fInfo.fAttacherIndex](rec->fJson, this)) {
-                geos.push_back(std::move(geo));
-            }
-        } break;
-        case ShapeType::kGeometryEffect: {
-            // Apply the current effect and pop from the stack.
-            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
-            if (!geos.empty()) {
-                geos = gGeometryEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
-                                                                           this,
-                                                                           std::move(geos));
-            }
-
-            SkASSERT(&ctx->fGeometryEffectStack->back().fJson == &rec->fJson);
-            SkASSERT(ctx->fGeometryEffectStack->back().fAttach ==
-                     gGeometryEffectAttachers[rec->fInfo.fAttacherIndex]);
-            ctx->fGeometryEffectStack->pop_back();
-        } break;
-        case ShapeType::kGroup: {
-            AttachShapeContext groupShapeCtx(&geos,
-                                             ctx->fGeometryEffectStack,
-                                             ctx->fCommittedAnimators);
-            if (auto subgroup = this->attachShape(rec->fJson["it"], &groupShapeCtx)) {
-                add_draw(std::move(subgroup), *rec);
-                SkASSERT(groupShapeCtx.fCommittedAnimators >= ctx->fCommittedAnimators);
-                ctx->fCommittedAnimators = groupShapeCtx.fCommittedAnimators;
-            }
-        } break;
-        case ShapeType::kPaint: {
-            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers));
-            auto paint = gPaintAttachers[rec->fInfo.fAttacherIndex](rec->fJson, this);
-            if (!paint || geos.empty())
-                break;
-
-            auto drawGeos = geos;
-
-            // Apply all pending effects from the stack.
-            for (auto it = ctx->fGeometryEffectStack->rbegin();
-                 it != ctx->fGeometryEffectStack->rend(); ++it) {
-                drawGeos = it->fAttach(it->fJson, this, std::move(drawGeos));
-            }
-
-            // If we still have multiple geos, reduce using 'merge'.
-            auto geo = drawGeos.size() > 1
-                ? Merge(std::move(drawGeos), sksg::Merge::Mode::kMerge)
-                : drawGeos[0];
-
-            SkASSERT(geo);
-            add_draw(sksg::Draw::Make(std::move(geo), std::move(paint)), *rec);
-            ctx->fCommittedAnimators = fCurrentAnimatorScope->size();
-        } break;
-        case ShapeType::kDrawEffect: {
-            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gDrawEffectAttachers));
-            if (!draws.empty()) {
-                draws = gDrawEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
-                                                                        this,
-                                                                        std::move(draws));
-                ctx->fCommittedAnimators = fCurrentAnimatorScope->size();
-            }
-        } break;
-        default:
-            break;
-        }
-    }
-
-    // By now we should have popped all local geometry effects.
-    SkASSERT(ctx->fGeometryEffectStack->size() == initialGeometryEffects);
-
-    sk_sp<sksg::RenderNode> shape_wrapper;
-    if (draws.size() == 1) {
-        // For a single draw, we don't need a group.
-        shape_wrapper = std::move(draws.front());
-    } else if (!draws.empty()) {
-        // Emit local draws reversed (bottom->top, per spec).
-        std::reverse(draws.begin(), draws.end());
-        draws.shrink_to_fit();
-
-        // We need a group to dispatch multiple draws.
-        shape_wrapper = sksg::Group::Make(std::move(draws));
-    }
-
-    sk_sp<sksg::Transform> shape_transform;
-    if (jtransform) {
-        const AutoPropertyTracker apt(this, *jtransform);
-
-        // This is tricky due to the interaction with ctx->fCommittedAnimators: we want any
-        // animators related to tranform/opacity to be committed => they must be inserted in front
-        // of the dangling/uncommitted ones.
-        AutoScope ascope(this);
-
-        if ((shape_transform = this->attachMatrix2D(*jtransform, nullptr))) {
-            shape_wrapper = sksg::TransformEffect::Make(std::move(shape_wrapper), shape_transform);
-        }
-        shape_wrapper = this->attachOpacity(*jtransform, std::move(shape_wrapper));
-
-        auto local_scope = ascope.release();
-        fCurrentAnimatorScope->insert(fCurrentAnimatorScope->begin() + ctx->fCommittedAnimators,
-                                      std::make_move_iterator(local_scope.begin()),
-                                      std::make_move_iterator(local_scope.end()));
-        ctx->fCommittedAnimators += local_scope.size();
-    }
-
-    // Push transformed local geometries to parent list, for subsequent paints.
-    for (auto& geo : geos) {
-        ctx->fGeometryStack->push_back(shape_transform
-            ? sksg::GeometryTransform::Make(std::move(geo), shape_transform)
-            : std::move(geo));
-    }
-
-    return shape_wrapper;
-}
-
-sk_sp<sksg::RenderNode> AnimationBuilder::attachShapeLayer(const skjson::ObjectValue& layer,
-                                                           LayerInfo*) const {
-    std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
-    std::vector<GeometryEffectRec> geometryEffectStack;
-    AttachShapeContext shapeCtx(&geometryStack, &geometryEffectStack,
-                                fCurrentAnimatorScope->size());
-    auto shapeNode = this->attachShape(layer["shapes"], &shapeCtx);
-
-    // Trim uncommitted animators: AttachShape consumes effects on the fly, and greedily attaches
-    // geometries => at the end, we can end up with unused geometries, which are nevertheless alive
-    // due to attached animators.  To avoid this, we track committed animators and discard the
-    // orphans here.
-    SkASSERT(shapeCtx.fCommittedAnimators <= fCurrentAnimatorScope->size());
-    fCurrentAnimatorScope->resize(shapeCtx.fCommittedAnimators);
-
-    return shapeNode;
-}
-
-} // namespace internal
-} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/Ellipse.cpp b/modules/skottie/src/layers/shapelayer/Ellipse.cpp
new file mode 100644
index 0000000..469978e
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/Ellipse.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "include/core/SkRRect.h"
+#include "modules/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGRect.h"
+
+namespace skottie {
+namespace internal {
+
+namespace  {
+
+class EllipseGeometryAdapter final :
+        public DiscardableAdapterBase<EllipseGeometryAdapter, sksg::RRect> {
+public:
+    EllipseGeometryAdapter(const skjson::ObjectValue& jellipse,
+                           const AnimationBuilder* abuilder) {
+        this->node()->setDirection(ParseDefault(jellipse["d"], -1) == 3 ? SkPathDirection::kCCW
+                                                                        : SkPathDirection::kCW);
+        this->node()->setInitialPointIndex(1); // starting point: (Center, Top)
+
+        this->bind(*abuilder, jellipse["s"], &fSize);
+        this->bind(*abuilder, jellipse["p"], &fPosition);
+    }
+
+private:
+    void onSync() override {
+        const auto size   = ValueTraits<VectorValue>::As<SkSize >(fSize);
+        const auto center = ValueTraits<VectorValue>::As<SkPoint>(fPosition);
+
+        const auto bounds = SkRect::MakeXYWH(center.x() - size.width()  / 2,
+                                             center.y() - size.height() / 2,
+                                             size.width(), size.height());
+
+        this->node()->setRRect(SkRRect::MakeOval(bounds));
+    }
+
+    VectorValue fSize,
+                fPosition;
+};
+
+} // namespace
+
+sk_sp<sksg::GeometryNode> ShapeBuilder::AttachEllipseGeometry(const skjson::ObjectValue& jellipse,
+                                                              const AnimationBuilder* abuilder) {
+    return abuilder->attachDiscardableAdapter<EllipseGeometryAdapter, sk_sp<sksg::GeometryNode>>
+                (jellipse, abuilder);
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/Gradient.cpp b/modules/skottie/src/layers/shapelayer/Gradient.cpp
new file mode 100644
index 0000000..336b634
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/Gradient.cpp
@@ -0,0 +1,215 @@
+/*
+ * 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/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGGradient.h"
+#include "modules/sksg/include/SkSGPaint.h"
+
+namespace skottie {
+namespace internal {
+
+namespace  {
+
+class GradientAdapter final : public AnimatablePropertyContainer {
+public:
+    static sk_sp<GradientAdapter> Make(const skjson::ObjectValue& jgrad,
+                                       const AnimationBuilder& abuilder) {
+        const skjson::ObjectValue* jstops = jgrad["g"];
+        if (!jstops)
+            return nullptr;
+
+        const auto stopCount = ParseDefault<int>((*jstops)["p"], -1);
+        if (stopCount < 0)
+            return nullptr;
+
+        const auto type = (ParseDefault<int>(jgrad["t"], 1) == 1) ? Type::kLinear
+                                                                  : Type::kRadial;
+        auto gradient_node = (type == Type::kLinear)
+                ? sk_sp<sksg::Gradient>(sksg::LinearGradient::Make())
+                : sk_sp<sksg::Gradient>(sksg::RadialGradient::Make());
+
+        return sk_sp<GradientAdapter>(new GradientAdapter(std::move(gradient_node),
+                                                          type,
+                                                          SkToSizeT(stopCount),
+                                                          jgrad, *jstops, abuilder));
+    }
+
+    const sk_sp<sksg::Gradient>& node() const { return fGradient; }
+
+private:
+    enum class Type { kLinear, kRadial };
+
+    GradientAdapter(sk_sp<sksg::Gradient> gradient,
+                    Type type,
+                    size_t stop_count,
+                    const skjson::ObjectValue& jgrad,
+                    const skjson::ObjectValue& jstops,
+                    const AnimationBuilder& abuilder)
+        : fGradient(std::move(gradient))
+        , fType(type)
+        , fStopCount(stop_count) {
+        this->bind(abuilder,  jgrad["s"], &fStartPoint);
+        this->bind(abuilder,  jgrad["e"], &fEndPoint);
+        this->bind(abuilder, jstops["k"], &fStops);
+    }
+
+    void onSync() override {
+        const auto s_point = ValueTraits<VectorValue>::As<SkPoint>(this->fStartPoint),
+                   e_point = ValueTraits<VectorValue>::As<SkPoint>(this->fEndPoint);
+
+        switch (fType) {
+        case Type::kLinear: {
+            auto* grad = static_cast<sksg::LinearGradient*>(fGradient.get());
+            grad->setStartPoint(s_point);
+            grad->setEndPoint(e_point);
+
+            break;
+        }
+        case Type::kRadial: {
+            auto* grad = static_cast<sksg::RadialGradient*>(fGradient.get());
+            grad->setStartCenter(s_point);
+            grad->setEndCenter(s_point);
+            grad->setStartRadius(0);
+            grad->setEndRadius(SkPoint::Distance(s_point, e_point));
+
+            break;
+        }
+        }
+
+        // Gradient color stops are specified as a consolidated float vector holding:
+        //
+        //   a) an (optional) array of color/RGB stop records (t, r, g, b)
+        //
+        // followed by
+        //
+        //   b) an (optional) array of opacity/alpha stop records (t, a)
+        //
+        struct   ColorRec { float t, r, g, b; };
+        struct OpacityRec { float t, a;       };
+
+        // The number of color records is explicit (fColorStopCount),
+        // while the number of opacity stops is implicit (based on the size of fStops).
+        //
+        // |fStops| holds ColorRec x |fColorStopCount| + OpacityRec x N
+        const auto c_count = fStopCount,
+                   c_size  = c_count * 4,
+                   o_count = (fStops.size() - c_size) / 2;
+        if (fStops.size() < c_size || fStops.size() != (c_count * 4 + o_count * 2)) {
+            // apply() may get called before the stops are set, so only log when we have some stops.
+            if (!fStops.empty()) {
+                SkDebugf("!! Invalid gradient stop array size: %zu\n", fStops.size());
+            }
+            return;
+        }
+
+        const auto* c_rec = c_count > 0
+                ? reinterpret_cast<const ColorRec*>(fStops.data())
+                : nullptr;
+        const auto* o_rec = o_count > 0
+                ? reinterpret_cast<const OpacityRec*>(fStops.data() + c_size)
+                : nullptr;
+        const auto* c_end = c_rec + c_count;
+        const auto* o_end = o_rec + o_count;
+
+        sksg::Gradient::ColorStop current_stop = {
+            0.0f, {
+                c_rec ? c_rec->r : 0,
+                c_rec ? c_rec->g : 0,
+                c_rec ? c_rec->b : 0,
+                o_rec ? o_rec->a : 1,
+        }};
+
+        std::vector<sksg::Gradient::ColorStop> stops;
+        stops.reserve(c_count);
+
+        // Merge-sort the color and opacity stops, LERP-ing intermediate channel values as needed.
+        while (c_rec || o_rec) {
+            // After exhausting one of color recs / opacity recs, continue propagating the last
+            // computed values (as if they were specified at the current position).
+            const auto& cs = c_rec
+                    ? *c_rec
+                    : ColorRec{ o_rec->t,
+                                current_stop.fColor.fR,
+                                current_stop.fColor.fG,
+                                current_stop.fColor.fB };
+            const auto& os = o_rec
+                    ? *o_rec
+                    : OpacityRec{ c_rec->t, current_stop.fColor.fA };
+
+            // Compute component lerp coefficients based on the relative position of the stops
+            // being considered. The idea is to select the smaller-pos stop, use its own properties
+            // as specified (lerp with t == 1), and lerp (with t < 1) the properties from the
+            // larger-pos stop against the previously computed gradient stop values.
+            const auto     c_pos = std::max(cs.t, current_stop.fPosition),
+                           o_pos = std::max(os.t, current_stop.fPosition),
+                       c_pos_rel = c_pos - current_stop.fPosition,
+                       o_pos_rel = o_pos - current_stop.fPosition,
+                             t_c = SkTPin(sk_ieee_float_divide(o_pos_rel, c_pos_rel), 0.0f, 1.0f),
+                             t_o = SkTPin(sk_ieee_float_divide(c_pos_rel, o_pos_rel), 0.0f, 1.0f);
+
+            auto lerp = [](float a, float b, float t) { return a + t * (b - a); };
+
+            current_stop = {
+                    std::min(c_pos, o_pos),
+                    {
+                        lerp(current_stop.fColor.fR, cs.r, t_c ),
+                        lerp(current_stop.fColor.fG, cs.g, t_c ),
+                        lerp(current_stop.fColor.fB, cs.b, t_c ),
+                        lerp(current_stop.fColor.fA, os.a, t_o)
+                    }
+            };
+            stops.push_back(current_stop);
+
+            // Consume one of, or both (for coincident positions) color/opacity stops.
+            if (c_pos <= o_pos) {
+                c_rec = next_rec<ColorRec>(c_rec, c_end);
+            }
+            if (o_pos <= c_pos) {
+                o_rec = next_rec<OpacityRec>(o_rec, o_end);
+            }
+        }
+
+        stops.shrink_to_fit();
+        fGradient->setColorStops(std::move(stops));
+    }
+
+private:
+    template <typename T>
+    const T* next_rec(const T* rec, const T* end_rec) const {
+        if (!rec) return nullptr;
+
+        SkASSERT(rec < end_rec);
+        rec++;
+
+        return rec < end_rec ? rec : nullptr;
+    }
+
+    const sk_sp<sksg::Gradient> fGradient;
+    const Type                  fType;
+    const size_t                fStopCount;
+
+    VectorValue  fStartPoint,
+                 fEndPoint,
+                 fStops;
+};
+
+} // namespace
+
+sk_sp<sksg::PaintNode> ShapeBuilder::AttachGradient(const skjson::ObjectValue& jgrad,
+                                                    const AnimationBuilder* abuilder) {
+    auto gradient = abuilder->attachDiscardableAdapter<GradientAdapter, sk_sp<sksg::Gradient>>
+            (jgrad, *abuilder);
+
+    return sksg::ShaderPaint::Make(std::move(gradient));
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/MergePaths.cpp b/modules/skottie/src/layers/shapelayer/MergePaths.cpp
new file mode 100644
index 0000000..5ced51a
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/MergePaths.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+
+namespace skottie {
+namespace internal {
+
+sk_sp<sksg::Merge> ShapeBuilder::MergeGeometry(std::vector<sk_sp<sksg::GeometryNode>>&& geos,
+                                               sksg::Merge::Mode mode) {
+    std::vector<sksg::Merge::Rec> merge_recs;
+    merge_recs.reserve(geos.size());
+
+    for (auto& geo : geos) {
+        merge_recs.push_back(
+            {std::move(geo), merge_recs.empty() ? sksg::Merge::Mode::kMerge : mode});
+    }
+
+    return sksg::Merge::Make(std::move(merge_recs));
+}
+
+std::vector<sk_sp<sksg::GeometryNode>> ShapeBuilder::AttachMergeGeometryEffect(
+        const skjson::ObjectValue& jmerge, const AnimationBuilder*,
+        std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
+    static constexpr sksg::Merge::Mode gModes[] = {
+        sksg::Merge::Mode::kMerge,      // "mm": 1
+        sksg::Merge::Mode::kUnion,      // "mm": 2
+        sksg::Merge::Mode::kDifference, // "mm": 3
+        sksg::Merge::Mode::kIntersect,  // "mm": 4
+        sksg::Merge::Mode::kXOR      ,  // "mm": 5
+    };
+
+    const auto mode = gModes[SkTMin<size_t>(ParseDefault<size_t>(jmerge["mm"], 1) - 1,
+                                            SK_ARRAY_COUNT(gModes) - 1)];
+
+    std::vector<sk_sp<sksg::GeometryNode>> merged;
+    merged.push_back(ShapeBuilder::MergeGeometry(std::move(geos), mode));
+
+    return merged;
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/Polystar.cpp b/modules/skottie/src/layers/shapelayer/Polystar.cpp
new file mode 100644
index 0000000..4b1078b
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/Polystar.cpp
@@ -0,0 +1,102 @@
+/*
+ * 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/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGPath.h"
+
+namespace skottie {
+namespace internal {
+
+namespace  {
+
+class PolystarGeometryAdapter final :
+        public DiscardableAdapterBase<PolystarGeometryAdapter, sksg::Path> {
+public:
+    enum class Type {
+        kStar, kPoly,
+    };
+
+    PolystarGeometryAdapter(const skjson::ObjectValue& jstar,
+                            const AnimationBuilder* abuilder, Type t)
+        : fType(t) {
+        this->bind(*abuilder, jstar["pt"], &fPointCount);
+        this->bind(*abuilder, jstar["p" ], &fPosition);
+        this->bind(*abuilder, jstar["r" ], &fRotation);
+        this->bind(*abuilder, jstar["ir"], &fInnerRadius);
+        this->bind(*abuilder, jstar["or"], &fOuterRadius);
+        this->bind(*abuilder, jstar["is"], &fInnerRoundness);
+        this->bind(*abuilder, jstar["os"], &fOuterRoundness);
+    }
+
+private:
+    void onSync() override {
+        const auto pos   = ValueTraits<VectorValue>::As<SkPoint>(fPosition);
+        static constexpr int kMaxPointCount = 100000;
+        const auto count = SkToUInt(SkTPin(SkScalarRoundToInt(fPointCount), 0, kMaxPointCount));
+        const auto arc   = sk_ieee_float_divide(SK_ScalarPI * 2, count);
+
+        const auto pt_on_circle = [](const SkPoint& c, SkScalar r, SkScalar a) {
+            return SkPoint::Make(c.x() + r * std::cos(a),
+                                 c.y() + r * std::sin(a));
+        };
+
+        // TODO: inner/outer "roundness"?
+
+        SkPath poly;
+
+        auto angle = SkDegreesToRadians(fRotation - 90);
+        poly.moveTo(pt_on_circle(pos, fOuterRadius, angle));
+        poly.incReserve(fType == Type::kStar ? count * 2 : count);
+
+        for (unsigned i = 0; i < count; ++i) {
+            if (fType == Type::kStar) {
+                poly.lineTo(pt_on_circle(pos, fInnerRadius, angle + arc * 0.5f));
+            }
+            angle += arc;
+            poly.lineTo(pt_on_circle(pos, fOuterRadius, angle));
+        }
+
+        poly.close();
+        this->node()->setPath(poly);
+    }
+
+    const Type fType;
+
+    VectorValue fPosition;
+    ScalarValue fPointCount     = 0,
+                fRotation       = 0,
+                fInnerRadius    = 0,
+                fOuterRadius    = 0,
+                fInnerRoundness = 0,
+                fOuterRoundness = 0;
+};
+
+} // namespace
+
+sk_sp<sksg::GeometryNode> ShapeBuilder::AttachPolystarGeometry(const skjson::ObjectValue& jstar,
+                                                               const AnimationBuilder* abuilder) {
+    static constexpr PolystarGeometryAdapter::Type gTypes[] = {
+        PolystarGeometryAdapter::Type::kStar, // "sy": 1
+        PolystarGeometryAdapter::Type::kPoly, // "sy": 2
+    };
+
+    const auto type = ParseDefault<size_t>(jstar["sy"], 0) - 1;
+    if (type >= SK_ARRAY_COUNT(gTypes)) {
+        abuilder->log(Logger::Level::kError, &jstar, "Unknown polystar type.");
+        return nullptr;
+    }
+
+    return abuilder->attachDiscardableAdapter<PolystarGeometryAdapter, sk_sp<sksg::GeometryNode>>
+                (jstar, abuilder, gTypes[type]);
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/Rectangle.cpp b/modules/skottie/src/layers/shapelayer/Rectangle.cpp
new file mode 100644
index 0000000..804274d
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/Rectangle.cpp
@@ -0,0 +1,61 @@
+/*
+ * 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 "include/core/SkRRect.h"
+#include "modules/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGRect.h"
+
+namespace skottie {
+namespace internal {
+
+namespace  {
+
+class RectangleGeometryAdapter final :
+        public DiscardableAdapterBase<RectangleGeometryAdapter, sksg::RRect> {
+public:
+    RectangleGeometryAdapter(const skjson::ObjectValue& jrect,
+                             const AnimationBuilder* abuilder) {
+        this->node()->setDirection(ParseDefault(jrect["d"], -1) == 3 ? SkPathDirection::kCCW
+                                                                     : SkPathDirection::kCW);
+        this->node()->setInitialPointIndex(2); // starting point: (Right, Top - radius.y)
+
+        this->bind(*abuilder, jrect["s"], &fSize);
+        this->bind(*abuilder, jrect["p"], &fPosition);
+        this->bind(*abuilder, jrect["r"], &fRoundness);
+    }
+
+private:
+    void onSync() override {
+        const auto size   = ValueTraits<VectorValue>::As<SkSize >(fSize);
+        const auto center = ValueTraits<VectorValue>::As<SkPoint>(fPosition);
+
+        const auto bounds = SkRect::MakeXYWH(center.x() - size.width()  / 2,
+                                             center.y() - size.height() / 2,
+                                             size.width(), size.height());
+
+        this->node()->setRRect(SkRRect::MakeRectXY(bounds, fRoundness, fRoundness));
+    }
+
+    VectorValue fSize,
+                fPosition;
+    ScalarValue fRoundness = 0;
+};
+
+} // namespace
+
+sk_sp<sksg::GeometryNode> ShapeBuilder::AttachRRectGeometry(const skjson::ObjectValue& jrect,
+                                                            const AnimationBuilder* abuilder) {
+    return abuilder->attachDiscardableAdapter<RectangleGeometryAdapter, sk_sp<sksg::GeometryNode>>
+                (jrect, abuilder);
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/Repeater.cpp b/modules/skottie/src/layers/shapelayer/Repeater.cpp
new file mode 100644
index 0000000..c47c18c
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/Repeater.cpp
@@ -0,0 +1,124 @@
+/*
+ * 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/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGGroup.h"
+#include "modules/sksg/include/SkSGTransform.h"
+
+#include <vector>
+
+namespace skottie {
+namespace internal {
+
+namespace  {
+
+class RepeaterAdapter final : public DiscardableAdapterBase<RepeaterAdapter, sksg::Group> {
+public:
+    RepeaterAdapter(const skjson::ObjectValue& jrepeater,
+                    const skjson::ObjectValue& jtransform,
+                    const AnimationBuilder& abuilder,
+                    sk_sp<sksg::RenderNode> repeater_node)
+        : fRepeaterNode(std::move(repeater_node))
+        , fComposite((ParseDefault(jrepeater["m"], 1) == 1) ? Composite::kAbove
+                                                            : Composite::kBelow) {
+        this->bind(abuilder, jrepeater["c"], &fCount);
+        this->bind(abuilder, jrepeater["o"], &fOffset);
+
+        this->bind(abuilder, jtransform["a" ], &fAnchorPoint);
+        this->bind(abuilder, jtransform["p" ], &fPosition);
+        this->bind(abuilder, jtransform["s" ], &fScale);
+        this->bind(abuilder, jtransform["r" ], &fRotation);
+        this->bind(abuilder, jtransform["so"], &fStartOpacity);
+        this->bind(abuilder, jtransform["eo"], &fEndOpacity);
+    }
+
+private:
+    void onSync() override {
+        static constexpr SkScalar kMaxCount = 512;
+        const auto count = static_cast<size_t>(SkTPin(fCount, 0.0f, kMaxCount) + 0.5f);
+
+        const auto anchor_point = ValueTraits<VectorValue>::As<SkPoint>(fAnchorPoint),
+                   position     = ValueTraits<VectorValue>::As<SkPoint>(fPosition),
+                   scale        = ValueTraits<VectorValue>::As<SkVector>(fScale);
+
+        const auto& compute_transform = [&] (size_t index) {
+            const auto t = fOffset + index;
+
+            // Position, scale & rotation are "scaled" by index/offset.
+            SkMatrix m = SkMatrix::MakeTrans(-anchor_point.x(),
+                                             -anchor_point.y());
+            m.postScale(std::pow(scale.x() * .01f, fOffset),
+                        std::pow(scale.y() * .01f, fOffset));
+            m.postRotate(t * fRotation);
+            m.postTranslate(t * position.x() + anchor_point.x(),
+                            t * position.y() + anchor_point.y());
+
+            return m;
+        };
+
+        // TODO: start/end opacity support.
+
+        // TODO: we can avoid rebuilding all the fragments in most cases.
+        this->node()->clear();
+        for (size_t i = 0; i < count; ++i) {
+            const auto insert_index = (fComposite == Composite::kAbove) ? i : count - i - 1;
+            this->node()->addChild(sksg::TransformEffect::Make(fRepeaterNode,
+                                                               compute_transform(insert_index)));
+        }
+    }
+
+    enum class Composite { kAbove, kBelow };
+
+    const sk_sp<sksg::RenderNode> fRepeaterNode;
+    const Composite               fComposite;
+
+    // Repeater props
+    ScalarValue fCount  = 0,
+                fOffset = 0;
+
+    // Transform props
+    VectorValue fAnchorPoint,
+                fPosition,
+                fScale        = { 100, 100 };
+    ScalarValue fRotation     = 0,
+                fStartOpacity = 100,
+                fEndOpacity   = 100;
+};
+
+} // namespace
+
+std::vector<sk_sp<sksg::RenderNode>> ShapeBuilder::AttachRepeaterDrawEffect(
+        const skjson::ObjectValue& jrepeater,
+        const AnimationBuilder* abuilder,
+        std::vector<sk_sp<sksg::RenderNode>>&& draws) {
+    std::vector<sk_sp<sksg::RenderNode>> repeater_draws;
+
+    if (const skjson::ObjectValue* jtransform = jrepeater["tr"]) {
+        // We can skip the group if only one draw.
+        auto repeater_node = (draws.size() > 1) ? sksg::Group::Make(std::move(draws))
+                                                : std::move(draws[0]);
+
+        auto repeater_root =
+                abuilder->attachDiscardableAdapter<RepeaterAdapter>(jrepeater,
+                                                                    *jtransform,
+                                                                    *abuilder,
+                                                                    std::move(repeater_node));
+        repeater_draws.reserve(1);
+        repeater_draws.push_back(std::move(repeater_root));
+    } else {
+        repeater_draws = std::move(draws);
+    }
+
+    return repeater_draws;
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/RoundCorners.cpp b/modules/skottie/src/layers/shapelayer/RoundCorners.cpp
new file mode 100644
index 0000000..9896584
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/RoundCorners.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGRoundEffect.h"
+
+namespace skottie {
+namespace internal {
+
+namespace  {
+
+class RoundCornersAdapter final : public DiscardableAdapterBase<RoundCornersAdapter,
+                                                                sksg::RoundEffect> {
+public:
+    RoundCornersAdapter(const skjson::ObjectValue& jround,
+                        const AnimationBuilder& abuilder,
+                        sk_sp<sksg::GeometryNode> child)
+        : INHERITED(sksg::RoundEffect::Make(std::move(child))) {
+        this->bind(abuilder, jround["r"], fRadius);
+    }
+
+private:
+    void onSync() override {
+        this->node()->setRadius(fRadius);
+    }
+
+    ScalarValue fRadius = 0;
+
+    using INHERITED = DiscardableAdapterBase<RoundCornersAdapter, sksg::RoundEffect>;
+};
+
+} // namespace
+
+std::vector<sk_sp<sksg::GeometryNode>> ShapeBuilder::AttachRoundGeometryEffect(
+        const skjson::ObjectValue& jround, const AnimationBuilder* abuilder,
+        std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
+    std::vector<sk_sp<sksg::GeometryNode>> rounded;
+    rounded.reserve(geos.size());
+
+    for (auto& g : geos) {
+        rounded.push_back(
+            abuilder->attachDiscardableAdapter<RoundCornersAdapter, sk_sp<sksg::RoundEffect>>
+                        (jround, *abuilder, std::move(g)));
+    }
+
+    return rounded;
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/ShapeLayer.cpp b/modules/skottie/src/layers/shapelayer/ShapeLayer.cpp
new file mode 100644
index 0000000..1b04189
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/ShapeLayer.cpp
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+
+#include "include/core/SkPath.h"
+#include "modules/skottie/src/SkottieAdapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/sksg/include/SkSGDraw.h"
+#include "modules/sksg/include/SkSGGeometryTransform.h"
+#include "modules/sksg/include/SkSGGradient.h"
+#include "modules/sksg/include/SkSGGroup.h"
+#include "modules/sksg/include/SkSGMerge.h"
+#include "modules/sksg/include/SkSGPaint.h"
+#include "modules/sksg/include/SkSGPath.h"
+#include "modules/sksg/include/SkSGRect.h"
+#include "modules/sksg/include/SkSGRenderEffect.h"
+#include "modules/sksg/include/SkSGRoundEffect.h"
+#include "modules/sksg/include/SkSGTransform.h"
+#include "modules/sksg/include/SkSGTrimEffect.h"
+#include "src/utils/SkJSON.h"
+
+#include <algorithm>
+#include <iterator>
+
+namespace skottie {
+namespace internal {
+
+namespace {
+
+sk_sp<sksg::PaintNode> AttachPaint(const skjson::ObjectValue& jpaint,
+                                   const AnimationBuilder* abuilder,
+                                   sk_sp<sksg::PaintNode> paint_node) {
+    if (paint_node) {
+        paint_node->setAntiAlias(true);
+
+        abuilder->bindProperty<ScalarValue>(jpaint["o"],
+            [paint_node](const ScalarValue& o) {
+                // BM opacity is [0..100]
+                paint_node->setOpacity(o * 0.01f);
+            });
+    }
+
+    return paint_node;
+}
+
+sk_sp<sksg::PaintNode> AttachStroke(const skjson::ObjectValue& jstroke,
+                                    const AnimationBuilder* abuilder,
+                                    sk_sp<sksg::PaintNode> stroke_node) {
+    if (!stroke_node)
+        return nullptr;
+
+    stroke_node->setStyle(SkPaint::kStroke_Style);
+
+    abuilder->bindProperty<ScalarValue>(jstroke["w"],
+        [stroke_node](const ScalarValue& w) {
+            stroke_node->setStrokeWidth(w);
+        });
+
+    stroke_node->setStrokeMiter(ParseDefault<SkScalar>(jstroke["ml"], 4.0f));
+
+    static constexpr SkPaint::Join gJoins[] = {
+        SkPaint::kMiter_Join,
+        SkPaint::kRound_Join,
+        SkPaint::kBevel_Join,
+    };
+    stroke_node->setStrokeJoin(gJoins[SkTMin<size_t>(ParseDefault<size_t>(jstroke["lj"], 1) - 1,
+                                                     SK_ARRAY_COUNT(gJoins) - 1)]);
+
+    static constexpr SkPaint::Cap gCaps[] = {
+        SkPaint::kButt_Cap,
+        SkPaint::kRound_Cap,
+        SkPaint::kSquare_Cap,
+    };
+    stroke_node->setStrokeCap(gCaps[SkTMin<size_t>(ParseDefault<size_t>(jstroke["lc"], 1) - 1,
+                                                   SK_ARRAY_COUNT(gCaps) - 1)]);
+
+    return stroke_node;
+}
+
+using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const skjson::ObjectValue&,
+                                                        const AnimationBuilder*);
+static constexpr GeometryAttacherT gGeometryAttachers[] = {
+    ShapeBuilder::AttachPathGeometry,
+    ShapeBuilder::AttachRRectGeometry,
+    ShapeBuilder::AttachEllipseGeometry,
+    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*,
+                                               std::vector<sk_sp<sksg::GeometryNode>>&&);
+static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = {
+    ShapeBuilder::AttachMergeGeometryEffect,
+    ShapeBuilder::AttachTrimGeometryEffect,
+    ShapeBuilder::AttachRoundGeometryEffect,
+};
+
+using DrawEffectAttacherT =
+    std::vector<sk_sp<sksg::RenderNode>> (*)(const skjson::ObjectValue&,
+                                             const AnimationBuilder*,
+                                             std::vector<sk_sp<sksg::RenderNode>>&&);
+
+static constexpr DrawEffectAttacherT gDrawEffectAttachers[] = {
+    ShapeBuilder::AttachRepeaterDrawEffect,
+};
+
+enum class ShapeType {
+    kGeometry,
+    kGeometryEffect,
+    kPaint,
+    kGroup,
+    kTransform,
+    kDrawEffect,
+};
+
+struct ShapeInfo {
+    const char* fTypeString;
+    ShapeType   fShapeType;
+    uint32_t    fAttacherIndex; // index into respective attacher tables
+};
+
+const ShapeInfo* FindShapeInfo(const skjson::ObjectValue& jshape) {
+    static constexpr ShapeInfo gShapeInfo[] = {
+        { "el", ShapeType::kGeometry      , 2 }, // ellipse   -> AttachEllipseGeometry
+        { "fl", ShapeType::kPaint         , 0 }, // fill      -> AttachColorFill
+        { "gf", ShapeType::kPaint         , 2 }, // gfill     -> AttachGradientFill
+        { "gr", ShapeType::kGroup         , 0 }, // group     -> Inline handler
+        { "gs", ShapeType::kPaint         , 3 }, // gstroke   -> AttachGradientStroke
+        { "mm", ShapeType::kGeometryEffect, 0 }, // merge     -> AttachMergeGeometryEffect
+        { "rc", ShapeType::kGeometry      , 1 }, // rrect     -> AttachRRectGeometry
+        { "rd", ShapeType::kGeometryEffect, 2 }, // round     -> AttachRoundGeometryEffect
+        { "rp", ShapeType::kDrawEffect    , 0 }, // repeater  -> AttachRepeaterDrawEffect
+        { "sh", ShapeType::kGeometry      , 0 }, // shape     -> AttachPathGeometry
+        { "sr", ShapeType::kGeometry      , 3 }, // polystar  -> AttachPolyStarGeometry
+        { "st", ShapeType::kPaint         , 1 }, // stroke    -> AttachColorStroke
+        { "tm", ShapeType::kGeometryEffect, 1 }, // trim      -> AttachTrimGeometryEffect
+        { "tr", ShapeType::kTransform     , 0 }, // transform -> Inline handler
+    };
+
+    const skjson::StringValue* type = jshape["ty"];
+    if (!type) {
+        return nullptr;
+    }
+
+    const auto* info = bsearch(type->begin(),
+                               gShapeInfo,
+                               SK_ARRAY_COUNT(gShapeInfo),
+                               sizeof(ShapeInfo),
+                               [](const void* key, const void* info) {
+                                  return strcmp(static_cast<const char*>(key),
+                                                static_cast<const ShapeInfo*>(info)->fTypeString);
+                               });
+
+    return static_cast<const ShapeInfo*>(info);
+}
+
+struct GeometryEffectRec {
+    const skjson::ObjectValue& fJson;
+    GeometryEffectAttacherT    fAttach;
+};
+
+} // namespace
+
+sk_sp<sksg::GeometryNode> ShapeBuilder::AttachPathGeometry(const skjson::ObjectValue& jpath,
+                                                           const AnimationBuilder* abuilder) {
+    return abuilder->attachPath(jpath["ks"]);
+}
+
+sk_sp<sksg::PaintNode> ShapeBuilder::AttachColorFill(const skjson::ObjectValue& jfill,
+                                                     const AnimationBuilder* abuilder) {
+    return AttachPaint(jfill, abuilder, abuilder->attachColor(jfill, "c"));
+}
+
+sk_sp<sksg::PaintNode> ShapeBuilder::AttachGradientFill(const skjson::ObjectValue& jfill,
+                                                        const AnimationBuilder* abuilder) {
+    return AttachPaint(jfill, abuilder, ShapeBuilder::AttachGradient(jfill, abuilder));
+}
+
+sk_sp<sksg::PaintNode> ShapeBuilder::AttachColorStroke(const skjson::ObjectValue& jstroke,
+                                                       const AnimationBuilder* abuilder) {
+    return AttachStroke(jstroke, abuilder, AttachPaint(jstroke, abuilder,
+                                                       abuilder->attachColor(jstroke, "c")));
+}
+
+sk_sp<sksg::PaintNode> ShapeBuilder::AttachGradientStroke(const skjson::ObjectValue& jstroke,
+                                                          const AnimationBuilder* abuilder) {
+    return AttachStroke(jstroke, abuilder, AttachPaint(jstroke, abuilder,
+                                                       ShapeBuilder::AttachGradient(jstroke,
+                                                                                    abuilder)));
+}
+
+struct AnimationBuilder::AttachShapeContext {
+    AttachShapeContext(std::vector<sk_sp<sksg::GeometryNode>>* geos,
+                       std::vector<GeometryEffectRec>* effects,
+                       size_t committedAnimators)
+        : fGeometryStack(geos)
+        , fGeometryEffectStack(effects)
+        , fCommittedAnimators(committedAnimators) {}
+
+    std::vector<sk_sp<sksg::GeometryNode>>* fGeometryStack;
+    std::vector<GeometryEffectRec>*         fGeometryEffectStack;
+    size_t                                  fCommittedAnimators;
+};
+
+sk_sp<sksg::RenderNode> AnimationBuilder::attachShape(const skjson::ArrayValue* jshape,
+                                                      AttachShapeContext* ctx) const {
+    if (!jshape)
+        return nullptr;
+
+    SkDEBUGCODE(const auto initialGeometryEffects = ctx->fGeometryEffectStack->size();)
+
+    const skjson::ObjectValue* jtransform = nullptr;
+
+    struct ShapeRec {
+        const skjson::ObjectValue& fJson;
+        const ShapeInfo&           fInfo;
+    };
+
+    // First pass (bottom->top):
+    //
+    //   * pick up the group transform and opacity
+    //   * push local geometry effects onto the stack
+    //   * store recs for next pass
+    //
+    std::vector<ShapeRec> recs;
+    for (size_t i = 0; i < jshape->size(); ++i) {
+        const skjson::ObjectValue* shape = (*jshape)[jshape->size() - 1 - i];
+        if (!shape) continue;
+
+        const auto* info = FindShapeInfo(*shape);
+        if (!info) {
+            this->log(Logger::Level::kError, &(*shape)["ty"], "Unknown shape.");
+            continue;
+        }
+
+        if (ParseDefault<bool>((*shape)["hd"], false)) {
+            // Ignore hidden shapes.
+            continue;
+        }
+
+        recs.push_back({ *shape, *info });
+
+        switch (info->fShapeType) {
+        case ShapeType::kTransform:
+            // Just track the transform property for now -- we'll deal with it later.
+            jtransform = shape;
+            break;
+        case ShapeType::kGeometryEffect:
+            SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
+            ctx->fGeometryEffectStack->push_back(
+                { *shape, gGeometryEffectAttachers[info->fAttacherIndex] });
+            break;
+        default:
+            break;
+        }
+    }
+
+    // Second pass (top -> bottom, after 2x reverse):
+    //
+    //   * track local geometry
+    //   * emit local paints
+    //
+    std::vector<sk_sp<sksg::GeometryNode>> geos;
+    std::vector<sk_sp<sksg::RenderNode  >> draws;
+
+    const auto add_draw = [this, &draws](sk_sp<sksg::RenderNode> draw, const ShapeRec& rec) {
+        // All draws can have an optional blend mode.
+        draws.push_back(this->attachBlendMode(rec.fJson, std::move(draw)));
+    };
+
+    for (auto rec = recs.rbegin(); rec != recs.rend(); ++rec) {
+        const AutoPropertyTracker apt(this, rec->fJson);
+
+        switch (rec->fInfo.fShapeType) {
+        case ShapeType::kGeometry: {
+            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers));
+            if (auto geo = gGeometryAttachers[rec->fInfo.fAttacherIndex](rec->fJson, this)) {
+                geos.push_back(std::move(geo));
+            }
+        } break;
+        case ShapeType::kGeometryEffect: {
+            // Apply the current effect and pop from the stack.
+            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
+            if (!geos.empty()) {
+                geos = gGeometryEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
+                                                                           this,
+                                                                           std::move(geos));
+            }
+
+            SkASSERT(&ctx->fGeometryEffectStack->back().fJson == &rec->fJson);
+            SkASSERT(ctx->fGeometryEffectStack->back().fAttach ==
+                     gGeometryEffectAttachers[rec->fInfo.fAttacherIndex]);
+            ctx->fGeometryEffectStack->pop_back();
+        } break;
+        case ShapeType::kGroup: {
+            AttachShapeContext groupShapeCtx(&geos,
+                                             ctx->fGeometryEffectStack,
+                                             ctx->fCommittedAnimators);
+            if (auto subgroup = this->attachShape(rec->fJson["it"], &groupShapeCtx)) {
+                add_draw(std::move(subgroup), *rec);
+                SkASSERT(groupShapeCtx.fCommittedAnimators >= ctx->fCommittedAnimators);
+                ctx->fCommittedAnimators = groupShapeCtx.fCommittedAnimators;
+            }
+        } break;
+        case ShapeType::kPaint: {
+            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gPaintAttachers));
+            auto paint = gPaintAttachers[rec->fInfo.fAttacherIndex](rec->fJson, this);
+            if (!paint || geos.empty())
+                break;
+
+            auto drawGeos = geos;
+
+            // Apply all pending effects from the stack.
+            for (auto it = ctx->fGeometryEffectStack->rbegin();
+                 it != ctx->fGeometryEffectStack->rend(); ++it) {
+                drawGeos = it->fAttach(it->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)
+                : drawGeos[0];
+
+            SkASSERT(geo);
+            add_draw(sksg::Draw::Make(std::move(geo), std::move(paint)), *rec);
+            ctx->fCommittedAnimators = fCurrentAnimatorScope->size();
+        } break;
+        case ShapeType::kDrawEffect: {
+            SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gDrawEffectAttachers));
+            if (!draws.empty()) {
+                draws = gDrawEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
+                                                                        this,
+                                                                        std::move(draws));
+                ctx->fCommittedAnimators = fCurrentAnimatorScope->size();
+            }
+        } break;
+        default:
+            break;
+        }
+    }
+
+    // By now we should have popped all local geometry effects.
+    SkASSERT(ctx->fGeometryEffectStack->size() == initialGeometryEffects);
+
+    sk_sp<sksg::RenderNode> shape_wrapper;
+    if (draws.size() == 1) {
+        // For a single draw, we don't need a group.
+        shape_wrapper = std::move(draws.front());
+    } else if (!draws.empty()) {
+        // Emit local draws reversed (bottom->top, per spec).
+        std::reverse(draws.begin(), draws.end());
+        draws.shrink_to_fit();
+
+        // We need a group to dispatch multiple draws.
+        shape_wrapper = sksg::Group::Make(std::move(draws));
+    }
+
+    sk_sp<sksg::Transform> shape_transform;
+    if (jtransform) {
+        const AutoPropertyTracker apt(this, *jtransform);
+
+        // This is tricky due to the interaction with ctx->fCommittedAnimators: we want any
+        // animators related to tranform/opacity to be committed => they must be inserted in front
+        // of the dangling/uncommitted ones.
+        AutoScope ascope(this);
+
+        if ((shape_transform = this->attachMatrix2D(*jtransform, nullptr))) {
+            shape_wrapper = sksg::TransformEffect::Make(std::move(shape_wrapper), shape_transform);
+        }
+        shape_wrapper = this->attachOpacity(*jtransform, std::move(shape_wrapper));
+
+        auto local_scope = ascope.release();
+        fCurrentAnimatorScope->insert(fCurrentAnimatorScope->begin() + ctx->fCommittedAnimators,
+                                      std::make_move_iterator(local_scope.begin()),
+                                      std::make_move_iterator(local_scope.end()));
+        ctx->fCommittedAnimators += local_scope.size();
+    }
+
+    // Push transformed local geometries to parent list, for subsequent paints.
+    for (auto& geo : geos) {
+        ctx->fGeometryStack->push_back(shape_transform
+            ? sksg::GeometryTransform::Make(std::move(geo), shape_transform)
+            : std::move(geo));
+    }
+
+    return shape_wrapper;
+}
+
+sk_sp<sksg::RenderNode> AnimationBuilder::attachShapeLayer(const skjson::ObjectValue& layer,
+                                                           LayerInfo*) const {
+    std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
+    std::vector<GeometryEffectRec> geometryEffectStack;
+    AttachShapeContext shapeCtx(&geometryStack, &geometryEffectStack,
+                                fCurrentAnimatorScope->size());
+    auto shapeNode = this->attachShape(layer["shapes"], &shapeCtx);
+
+    // Trim uncommitted animators: AttachShape consumes effects on the fly, and greedily attaches
+    // geometries => at the end, we can end up with unused geometries, which are nevertheless alive
+    // due to attached animators.  To avoid this, we track committed animators and discard the
+    // orphans here.
+    SkASSERT(shapeCtx.fCommittedAnimators <= fCurrentAnimatorScope->size());
+    fCurrentAnimatorScope->resize(shapeCtx.fCommittedAnimators);
+
+    return shapeNode;
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/layers/shapelayer/ShapeLayer.h b/modules/skottie/src/layers/shapelayer/ShapeLayer.h
new file mode 100644
index 0000000..3e7e572
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/ShapeLayer.h
@@ -0,0 +1,81 @@
+/*
+ * 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 SkottieShapeLayer_DEFINED
+#define SkottieShapeLayer_DEFINED
+
+#include "include/private/SkNoncopyable.h"
+#include "modules/sksg/include/SkSGMerge.h"
+
+#include <vector>
+
+namespace skjson {
+
+class ObjectValue;
+
+} // namespace skjson
+
+namespace sksg {
+
+class GeometryNode;
+class PaintNode;
+class RenderNode;
+
+} // namespace sksg
+
+namespace skottie {
+namespace internal {
+
+class AnimationBuilder;
+
+// TODO/TRANSITIONAL: not much state here yet, but will eventually hold ShapeLayer-related stuff.
+class ShapeBuilder final : SkNoncopyable {
+public:
+    static sk_sp<sksg::Merge> MergeGeometry(std::vector<sk_sp<sksg::GeometryNode>>&&,
+                                            sksg::Merge::Mode);
+
+    static sk_sp<sksg::GeometryNode> AttachPathGeometry(const skjson::ObjectValue&,
+                                                        const AnimationBuilder*);
+    static sk_sp<sksg::GeometryNode> AttachRRectGeometry(const skjson::ObjectValue&,
+                                                         const AnimationBuilder*);
+    static sk_sp<sksg::GeometryNode> AttachEllipseGeometry(const skjson::ObjectValue&,
+                                                           const AnimationBuilder*);
+    static sk_sp<sksg::GeometryNode> AttachPolystarGeometry(const skjson::ObjectValue&,
+                                                            const AnimationBuilder*);
+
+    static sk_sp<sksg::PaintNode> AttachGradient(const skjson::ObjectValue&,
+                                                 const AnimationBuilder*);
+    static sk_sp<sksg::PaintNode> AttachColorFill(const skjson::ObjectValue&,
+                                                  const AnimationBuilder*);
+    static sk_sp<sksg::PaintNode> AttachColorStroke(const skjson::ObjectValue&,
+                                                    const AnimationBuilder*);
+    static sk_sp<sksg::PaintNode> AttachGradientFill(const skjson::ObjectValue&,
+                                                     const AnimationBuilder*);
+    static sk_sp<sksg::PaintNode> AttachGradientStroke(const skjson::ObjectValue&,
+                                                       const AnimationBuilder*);
+
+    static std::vector<sk_sp<sksg::GeometryNode>> AttachMergeGeometryEffect(
+            const skjson::ObjectValue&, const AnimationBuilder*,
+            std::vector<sk_sp<sksg::GeometryNode>>&&);
+    static std::vector<sk_sp<sksg::GeometryNode>> AttachTrimGeometryEffect(
+            const skjson::ObjectValue&,
+            const AnimationBuilder*,
+            std::vector<sk_sp<sksg::GeometryNode>>&&);
+    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::RenderNode>> AttachRepeaterDrawEffect(
+            const skjson::ObjectValue&,
+            const AnimationBuilder*,
+            std::vector<sk_sp<sksg::RenderNode>>&&);
+};
+
+} // namespace internal
+} // namespace skottie
+
+#endif // SkottieShapeLayer_DEFINED
diff --git a/modules/skottie/src/layers/shapelayer/TrimPaths.cpp b/modules/skottie/src/layers/shapelayer/TrimPaths.cpp
new file mode 100644
index 0000000..a96aecc
--- /dev/null
+++ b/modules/skottie/src/layers/shapelayer/TrimPaths.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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/skottie/src/Adapter.h"
+#include "modules/skottie/src/SkottieJson.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/SkottieValue.h"
+#include "modules/skottie/src/layers/shapelayer/ShapeLayer.h"
+#include "modules/sksg/include/SkSGMerge.h"
+#include "modules/sksg/include/SkSGTrimEffect.h"
+
+#include <vector>
+
+namespace skottie {
+namespace internal {
+
+namespace  {
+
+class TrimEffectAdapter final : public DiscardableAdapterBase<TrimEffectAdapter, sksg::TrimEffect> {
+public:
+    TrimEffectAdapter(const skjson::ObjectValue& jtrim,
+                      const AnimationBuilder& abuilder,
+                      sk_sp<sksg::GeometryNode> child)
+        : INHERITED(sksg::TrimEffect::Make(std::move(child))) {
+        this->bind(abuilder, jtrim["s"], &fStart);
+        this->bind(abuilder, jtrim["e"], &fEnd);
+        this->bind(abuilder, jtrim["o"], &fOffset);
+    }
+
+private:
+    void onSync() override {
+        // BM semantics: start/end are percentages, offset is "degrees" (?!).
+        const auto  start = fStart  / 100,
+                      end = fEnd    / 100,
+                   offset = fOffset / 360;
+
+        auto startT = SkTMin(start, end) + offset,
+              stopT = SkTMax(start, end) + offset;
+        auto   mode = SkTrimPathEffect::Mode::kNormal;
+
+        if (stopT - startT < 1) {
+            startT -= SkScalarFloorToScalar(startT);
+            stopT  -= SkScalarFloorToScalar(stopT);
+
+            if (startT > stopT) {
+                using std::swap;
+                swap(startT, stopT);
+                mode = SkTrimPathEffect::Mode::kInverted;
+            }
+        } else {
+            startT = 0;
+            stopT  = 1;
+        }
+
+        this->node()->setStart(startT);
+        this->node()->setStop(stopT);
+        this->node()->setMode(mode);
+    }
+
+    ScalarValue fStart  =   0,
+                fEnd    = 100,
+                fOffset =   0;
+
+    using INHERITED = DiscardableAdapterBase<TrimEffectAdapter, sksg::TrimEffect>;
+};
+
+} // namespace
+
+std::vector<sk_sp<sksg::GeometryNode>> ShapeBuilder::AttachTrimGeometryEffect(
+        const skjson::ObjectValue& jtrim,
+        const AnimationBuilder* abuilder,
+        std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
+
+    enum class Mode {
+        kParallel, // "m": 1 (Trim Multiple Shapes: Simultaneously)
+        kSerial,   // "m": 2 (Trim Multiple Shapes: Individually)
+    } gModes[] = { Mode::kParallel, Mode::kSerial};
+
+    const auto mode = gModes[SkTMin<size_t>(ParseDefault<size_t>(jtrim["m"], 1) - 1,
+                                            SK_ARRAY_COUNT(gModes) - 1)];
+
+    std::vector<sk_sp<sksg::GeometryNode>> inputs;
+    if (mode == Mode::kSerial) {
+        inputs.push_back(ShapeBuilder::MergeGeometry(std::move(geos), sksg::Merge::Mode::kMerge));
+    } else {
+        inputs = std::move(geos);
+    }
+
+    std::vector<sk_sp<sksg::GeometryNode>> trimmed;
+    trimmed.reserve(inputs.size());
+
+    for (const auto& i : inputs) {
+        trimmed.push_back(
+            abuilder->attachDiscardableAdapter<TrimEffectAdapter, sk_sp<sksg::TrimEffect>>
+                        (jtrim, *abuilder, i));
+    }
+
+    return trimmed;
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/text/TextAdapter.h b/modules/skottie/src/text/TextAdapter.h
index 550d1ab..501a479 100644
--- a/modules/skottie/src/text/TextAdapter.h
+++ b/modules/skottie/src/text/TextAdapter.h
@@ -33,7 +33,7 @@
 
     ~TextAdapter() override;
 
-    const sk_sp<sksg::Group>& renderNode() const { return fRoot; }
+    const sk_sp<sksg::Group>& node() const { return fRoot; }
 
     const TextValue& getText() const { return fText.fCurrentValue; }
     void setText(const TextValue&);
diff --git a/public.bzl b/public.bzl
index 729d99e..45d43e7 100644
--- a/public.bzl
+++ b/public.bzl
@@ -738,6 +738,8 @@
             "modules/skottie/src/effects/*.h",
             "modules/skottie/src/layers/*.cpp",
             "modules/skottie/src/layers/*.h",
+            "modules/skottie/src/layers/shapelayer/*.cpp",
+            "modules/skottie/src/layers/shapelayer/*.h",
             "modules/skottie/src/text/*.cpp",
             "modules/skottie/src/text/*.h",
         ],