[skottie] Make AnimationBuilder available for value parsing

Plumb AnimationBuilder throught a bazillion layers to make it reachable
when parsing animatable values.

This is in preparation of keyframed text, which will require access to
the font set when parsing.

Refactor only, no functional changes.

TBR=
Change-Id: Ide2ef2ba66fbcc75fdcc785f987b364d45dff5b6
Reviewed-on: https://skia-review.googlesource.com/149264
Commit-Queue: Florin Malita <fmalita@chromium.org>
Reviewed-by: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/skottie.gni b/modules/skottie/skottie.gni
index eb85a4a..6d285ea 100644
--- a/modules/skottie/skottie.gni
+++ b/modules/skottie/skottie.gni
@@ -14,7 +14,6 @@
   "$_src/SkottieAdapter.cpp",
   "$_src/SkottieAdapter.h",
   "$_src/SkottieAnimator.cpp",
-  "$_src/SkottieAnimator.h",
   "$_src/SkottieJson.cpp",
   "$_src/SkottieJson.h",
   "$_src/SkottieLayer.cpp",
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
index 40e94ec..e7f3dda 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -25,7 +25,6 @@
 #include "SkTime.h"
 #include "SkTo.h"
 #include "SkottieAdapter.h"
-#include "SkottieAnimator.h"
 #include "SkottieJson.h"
 #include "SkottiePriv.h"
 #include "SkottieValue.h"
@@ -43,23 +42,24 @@
     LOG("%s: %s\n", msg, dump.c_str());
 }
 
-sk_sp<sksg::Matrix> AttachMatrix(const skjson::ObjectValue& t, AnimatorScope* ascope,
-                                 sk_sp<sksg::Matrix> parentMatrix) {
+sk_sp<sksg::Matrix> AnimationBuilder::attachMatrix(const skjson::ObjectValue& t,
+                                                   AnimatorScope* ascope,
+                                                   sk_sp<sksg::Matrix> parentMatrix) const {
     static const VectorValue g_default_vec_0   = {  0,   0},
                              g_default_vec_100 = {100, 100};
 
     auto matrix = sksg::Matrix::Make(SkMatrix::I(), parentMatrix);
     auto adapter = sk_make_sp<TransformAdapter>(matrix);
 
-    auto bound = BindProperty<VectorValue>(t["a"], ascope,
+    auto bound = this->bindProperty<VectorValue>(t["a"], ascope,
             [adapter](const VectorValue& a) {
                 adapter->setAnchorPoint(ValueTraits<VectorValue>::As<SkPoint>(a));
             }, g_default_vec_0);
-    bound |= BindProperty<VectorValue>(t["p"], ascope,
+    bound |= this->bindProperty<VectorValue>(t["p"], ascope,
             [adapter](const VectorValue& p) {
                 adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
             }, g_default_vec_0);
-    bound |= BindProperty<VectorValue>(t["s"], ascope,
+    bound |= this->bindProperty<VectorValue>(t["s"], ascope,
             [adapter](const VectorValue& s) {
                 adapter->setScale(ValueTraits<VectorValue>::As<SkVector>(s));
             }, g_default_vec_100);
@@ -70,15 +70,15 @@
         // we can still make use of rz.
         jrotation = &t["rz"];
     }
-    bound |= BindProperty<ScalarValue>(*jrotation, ascope,
+    bound |= this->bindProperty<ScalarValue>(*jrotation, ascope,
             [adapter](const ScalarValue& r) {
                 adapter->setRotation(r);
             }, 0.0f);
-    bound |= BindProperty<ScalarValue>(t["sk"], ascope,
+    bound |= this->bindProperty<ScalarValue>(t["sk"], ascope,
             [adapter](const ScalarValue& sk) {
                 adapter->setSkew(sk);
             }, 0.0f);
-    bound |= BindProperty<ScalarValue>(t["sa"], ascope,
+    bound |= this->bindProperty<ScalarValue>(t["sa"], ascope,
             [adapter](const ScalarValue& sa) {
                 adapter->setSkewAxis(sa);
             }, 0.0f);
@@ -86,14 +86,15 @@
     return bound ? matrix : parentMatrix;
 }
 
-sk_sp<sksg::RenderNode> AttachOpacity(const skjson::ObjectValue& jtransform, AnimatorScope* ascope,
-                                      sk_sp<sksg::RenderNode> childNode) {
+sk_sp<sksg::RenderNode> AnimationBuilder::attachOpacity(const skjson::ObjectValue& jtransform,
+                                                        AnimatorScope* ascope,
+                                                        sk_sp<sksg::RenderNode> childNode) const {
     if (!childNode)
         return nullptr;
 
     auto opacityNode = sksg::OpacityEffect::Make(childNode);
 
-    if (!BindProperty<ScalarValue>(jtransform["o"], ascope,
+    if (!this->bindProperty<ScalarValue>(jtransform["o"], ascope,
         [opacityNode](const ScalarValue& o) {
             // BM opacity is [0..100]
             opacityNode->setOpacity(o * 0.01f);
@@ -105,9 +106,10 @@
     return std::move(opacityNode);
 }
 
-sk_sp<sksg::Path> AttachPath(const skjson::Value& jpath, AnimatorScope* ascope) {
+sk_sp<sksg::Path> AnimationBuilder::attachPath(const skjson::Value& jpath,
+                                               AnimatorScope* ascope) const {
     auto path_node = sksg::Path::Make();
-    return BindProperty<ShapeValue>(jpath, ascope,
+    return this->bindProperty<ShapeValue>(jpath, ascope,
         [path_node](const ShapeValue& p) {
             // FillType is tracked in the SG node, not in keyframes -- make sure we preserve it.
             auto path = ValueTraits<ShapeValue>::As<SkPath>(p);
@@ -118,10 +120,11 @@
         : nullptr;
 }
 
-sk_sp<sksg::Color> AttachColor(const skjson::ObjectValue& jcolor, AnimatorScope* ascope,
-                               const char prop_name[]) {
+sk_sp<sksg::Color> AnimationBuilder::attachColor(const skjson::ObjectValue& jcolor,
+                                                 AnimatorScope* ascope,
+                                                 const char prop_name[]) const {
     auto color_node = sksg::Color::Make(SK_ColorBLACK);
-    BindProperty<VectorValue>(jcolor[prop_name], ascope,
+    this->bindProperty<VectorValue>(jcolor[prop_name], ascope,
         [color_node](const VectorValue& c) {
             color_node->setColor(ValueTraits<VectorValue>::As<SkColor>(c));
         });
diff --git a/modules/skottie/src/SkottieAnimator.cpp b/modules/skottie/src/SkottieAnimator.cpp
index c9c43b4..7837f3e 100644
--- a/modules/skottie/src/SkottieAnimator.cpp
+++ b/modules/skottie/src/SkottieAnimator.cpp
@@ -5,11 +5,11 @@
  * found in the LICENSE file.
  */
 
-#include "SkottieAnimator.h"
-
 #include "SkCubicMap.h"
 #include "SkottieJson.h"
+#include "SkottiePriv.h"
 #include "SkottieValue.h"
+#include "SkSGScene.h"
 #include "SkString.h"
 #include "SkTArray.h"
 
@@ -68,9 +68,9 @@
             : SkTPin(fCubicMaps[rec.cmidx].computeYFromX(lt), 0.0f, 1.0f);
     }
 
-    virtual int parseValue(const skjson::Value&) = 0;
+    virtual int parseValue(const skjson::Value&, const AnimationBuilder* abuilder) = 0;
 
-    void parseKeyFrames(const skjson::ArrayValue& jframes) {
+    void parseKeyFrames(const skjson::ArrayValue& jframes, const AnimationBuilder* abuilder) {
         for (const skjson::ObjectValue* jframe : jframes) {
             if (!jframe) continue;
 
@@ -88,7 +88,7 @@
                 fRecs.back().t1 = t0;
             }
 
-            const auto vidx0 = this->parseValue((*jframe)["s"]);
+            const auto vidx0 = this->parseValue((*jframe)["s"], abuilder);
             if (vidx0 < 0)
                 continue;
 
@@ -97,7 +97,7 @@
 
             if (!ParseDefault<bool>((*jframe)["h"], false)) {
                 // Regular frame, requires an end value.
-                vidx1 = this->parseValue((*jframe)["e"]);
+                vidx1 = this->parseValue((*jframe)["e"], abuilder);
                 if (vidx1 < 0)
                     continue;
 
@@ -176,10 +176,12 @@
 class KeyframeAnimator final : public KeyframeAnimatorBase {
 public:
     static std::unique_ptr<KeyframeAnimator> Make(const skjson::ArrayValue* jv,
+                                                  const AnimationBuilder* abuilder,
                                                   std::function<void(const T&)>&& apply) {
         if (!jv) return nullptr;
 
-        std::unique_ptr<KeyframeAnimator> animator(new KeyframeAnimator(*jv, std::move(apply)));
+        std::unique_ptr<KeyframeAnimator> animator(
+            new KeyframeAnimator(*jv, abuilder, std::move(apply)));
         if (!animator->count())
             return nullptr;
 
@@ -193,14 +195,16 @@
 
 private:
     KeyframeAnimator(const skjson::ArrayValue& jframes,
+                     const AnimationBuilder* abuilder,
                      std::function<void(const T&)>&& apply)
         : fApplyFunc(std::move(apply)) {
-        this->parseKeyFrames(jframes);
+        this->parseKeyFrames(jframes, abuilder);
     }
 
-    int parseValue(const skjson::Value& jv) override {
+    int parseValue(const skjson::Value& jv, const AnimationBuilder* abuilder) override {
         T val;
-        if (!Parse<T>(jv, &val) || (!fVs.empty() && !ValueTraits<T>::CanLerp(val, fVs.back()))) {
+        if (!ValueTraits<T>::FromJSON(jv, abuilder, &val) ||
+            (!fVs.empty() && !ValueTraits<T>::CanLerp(val, fVs.back()))) {
             return -1;
         }
 
@@ -241,7 +245,8 @@
 
 template <typename T>
 static inline bool BindPropertyImpl(const skjson::ObjectValue* jprop,
-                                    sksg::AnimatorList* animators,
+                                    const AnimationBuilder* abuilder,
+                                    AnimatorScope* ascope,
                                     std::function<void(const T&)>&& apply,
                                     const T* noop = nullptr) {
     if (!jprop) return false;
@@ -257,7 +262,7 @@
     // For those, we attempt to parse both ways.
     if (!ParseDefault<bool>(jpropA, false)) {
         T val;
-        if (Parse<T>(jpropK, &val)) {
+        if (ValueTraits<T>::FromJSON(jpropK, abuilder, &val)) {
             // Static property.
             if (noop && val == *noop)
                 return false;
@@ -272,13 +277,13 @@
     }
 
     // Keyframe property.
-    auto animator = KeyframeAnimator<T>::Make(jpropK, std::move(apply));
+    auto animator = KeyframeAnimator<T>::Make(jpropK, abuilder, std::move(apply));
 
     if (!animator) {
         return LogFail(*jprop, "Could not parse keyframed property");
     }
 
-    animators->push_back(std::move(animator));
+    ascope->push_back(std::move(animator));
 
     return true;
 }
@@ -286,6 +291,7 @@
 class SplitPointAnimator final : public sksg::Animator {
 public:
     static std::unique_ptr<SplitPointAnimator> Make(const skjson::ObjectValue* jprop,
+                                                    const AnimationBuilder* abuilder,
                                                     std::function<void(const VectorValue&)>&& apply,
                                                     const VectorValue*) {
         if (!jprop) return nullptr;
@@ -297,9 +303,9 @@
         // the object itself, so the scope is bound to the life time of the object.
         auto* split_animator_ptr = split_animator.get();
 
-        if (!BindPropertyImpl<ScalarValue>((*jprop)["x"], &split_animator->fAnimators,
+        if (!BindPropertyImpl<ScalarValue>((*jprop)["x"], abuilder, &split_animator->fAnimators,
                 [split_animator_ptr](const ScalarValue& x) { split_animator_ptr->setX(x); }) ||
-            !BindPropertyImpl<ScalarValue>((*jprop)["y"], &split_animator->fAnimators,
+            !BindPropertyImpl<ScalarValue>((*jprop)["y"], abuilder, &split_animator->fAnimators,
                 [split_animator_ptr](const ScalarValue& y) { split_animator_ptr->setY(y); })) {
             LogFail(*jprop, "Could not parse split property");
             return nullptr;
@@ -340,11 +346,12 @@
 };
 
 bool BindSplitPositionProperty(const skjson::Value& jv,
-                               sksg::AnimatorList* animators,
+                               const AnimationBuilder* abuilder,
+                               AnimatorScope* ascope,
                                std::function<void(const VectorValue&)>&& apply,
                                const VectorValue* noop) {
-    if (auto split_animator = SplitPointAnimator::Make(jv, std::move(apply), noop)) {
-        animators->push_back(std::unique_ptr<sksg::Animator>(split_animator.release()));
+    if (auto split_animator = SplitPointAnimator::Make(jv, abuilder, std::move(apply), noop)) {
+        ascope->push_back(std::unique_ptr<sksg::Animator>(split_animator.release()));
         return true;
     }
 
@@ -354,32 +361,32 @@
 } // namespace
 
 template <>
-bool BindProperty(const skjson::Value& jv,
+bool AnimationBuilder::bindProperty(const skjson::Value& jv,
                   AnimatorScope* ascope,
                   std::function<void(const ScalarValue&)>&& apply,
-                  const ScalarValue* noop) {
-    return BindPropertyImpl(jv, ascope, std::move(apply), noop);
+                  const ScalarValue* noop) const {
+    return BindPropertyImpl(jv, this, ascope, std::move(apply), noop);
 }
 
 template <>
-bool BindProperty(const skjson::Value& jv,
+bool AnimationBuilder::bindProperty(const skjson::Value& jv,
                   AnimatorScope* ascope,
                   std::function<void(const VectorValue&)>&& apply,
-                  const VectorValue* noop) {
+                  const VectorValue* noop) const {
     if (!jv.is<skjson::ObjectValue>())
         return false;
 
     return ParseDefault<bool>(jv.as<skjson::ObjectValue>()["s"], false)
-        ? BindSplitPositionProperty(jv, ascope, std::move(apply), noop)
-        : BindPropertyImpl(jv, ascope, std::move(apply), noop);
+        ? BindSplitPositionProperty(jv, this, ascope, std::move(apply), noop)
+        : BindPropertyImpl(jv, this, ascope, std::move(apply), noop);
 }
 
 template <>
-bool BindProperty(const skjson::Value& jv,
+bool AnimationBuilder::bindProperty(const skjson::Value& jv,
                   AnimatorScope* ascope,
                   std::function<void(const ShapeValue&)>&& apply,
-                  const ShapeValue* noop) {
-    return BindPropertyImpl(jv, ascope, std::move(apply), noop);
+                  const ShapeValue* noop) const {
+    return BindPropertyImpl(jv, this, ascope, std::move(apply), noop);
 }
 
 } // namespace internal
diff --git a/modules/skottie/src/SkottieAnimator.h b/modules/skottie/src/SkottieAnimator.h
deleted file mode 100644
index 33abf1c..0000000
--- a/modules/skottie/src/SkottieAnimator.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#ifndef SkottieAnimator_DEFINED
-#define SkottieAnimator_DEFINED
-
-#include "SkottiePriv.h"
-
-#include <functional>
-
-namespace skjson { class Value; }
-
-namespace skottie {
-namespace internal {
-
-// This is the workhorse for property binding: depending on whether the property is animated,
-// it will either apply immediately or instantiate and attach a keyframe animator.
-template <typename T>
-bool BindProperty(const skjson::Value&,
-                  AnimatorScope*,
-                  std::function<void(const T&)>&&,
-                  const T* default_igore = nullptr);
-
-template <typename T>
-bool BindProperty(const skjson::Value& jv,
-                  AnimatorScope* animators,
-                  std::function<void(const T&)>&& apply,
-                  const T& default_ignore) {
-    return BindProperty(jv, animators, std::move(apply), &default_ignore);
-}
-
-} // namespace internal
-} // namespace skottie
-
-#endif // SkottieAnimator_DEFINED
diff --git a/modules/skottie/src/SkottieJson.cpp b/modules/skottie/src/SkottieJson.cpp
index 568f4df..7f1c3b6 100644
--- a/modules/skottie/src/SkottieJson.cpp
+++ b/modules/skottie/src/SkottieJson.cpp
@@ -110,63 +110,4 @@
     return true;
 }
 
-namespace {
-
-bool ParsePointVec(const Value& v, std::vector<SkPoint>* pts) {
-    if (!v.is<ArrayValue>())
-        return false;
-    const auto& av = v.as<ArrayValue>();
-
-    pts->clear();
-    pts->reserve(av.size());
-
-    std::vector<float> vec;
-    for (size_t i = 0; i < av.size(); ++i) {
-        if (!Parse(av[i], &vec) || vec.size() != 2)
-            return false;
-        pts->push_back(SkPoint::Make(vec[0], vec[1]));
-    }
-
-    return true;
-}
-
-} // namespace
-
-template <>
-bool Parse<ShapeValue>(const Value& v, ShapeValue* sh) {
-    SkASSERT(sh->fVertices.empty());
-
-    // Some versions wrap values as single-element arrays.
-    if (const skjson::ArrayValue* av = v) {
-        if (av->size() == 1) {
-            return Parse((*av)[0], sh);
-        }
-    }
-
-    if (!v.is<skjson::ObjectValue>())
-        return false;
-    const auto& ov = v.as<ObjectValue>();
-
-    std::vector<SkPoint> inPts,  // Cubic Bezier "in" control points, relative to vertices.
-                         outPts, // Cubic Bezier "out" control points, relative to vertices.
-                         verts;  // Cubic Bezier vertices.
-
-    if (!ParsePointVec(ov["i"], &inPts) ||
-        !ParsePointVec(ov["o"], &outPts) ||
-        !ParsePointVec(ov["v"], &verts) ||
-        inPts.size() != outPts.size() ||
-        inPts.size() != verts.size()) {
-
-        return false;
-    }
-
-    sh->fVertices.reserve(inPts.size());
-    for (size_t i = 0; i < inPts.size(); ++i) {
-        sh->fVertices.push_back(BezierVertex({inPts[i], outPts[i], verts[i]}));
-    }
-    sh->fClosed = ParseDefault<bool>(ov["c"], false);
-
-    return true;
-}
-
 } // namespace skottie
diff --git a/modules/skottie/src/SkottieLayer.cpp b/modules/skottie/src/SkottieLayer.cpp
index 1b00ffd..83cfc9f 100644
--- a/modules/skottie/src/SkottieLayer.cpp
+++ b/modules/skottie/src/SkottieLayer.cpp
@@ -12,7 +12,6 @@
 #include "SkImage.h"
 #include "SkJSON.h"
 #include "SkMakeUnique.h"
-#include "SkottieAnimator.h"
 #include "SkottieJson.h"
 #include "SkottieValue.h"
 #include "SkParse.h"
@@ -65,6 +64,7 @@
 }
 
 sk_sp<sksg::RenderNode> AttachMask(const skjson::ArrayValue* jmask,
+                                   const AnimationBuilder* abuilder,
                                    AnimatorScope* ascope,
                                    sk_sp<sksg::RenderNode> childNode) {
     if (!jmask) return childNode;
@@ -100,7 +100,7 @@
             continue;
         }
 
-        auto mask_path = AttachPath((*m)["pt"], ascope);
+        auto mask_path = abuilder->attachPath((*m)["pt"], ascope);
         if (!mask_path) {
             LogJSON(*m, "!! Could not parse mask path");
             continue;
@@ -118,7 +118,7 @@
         mask_paint->setBlendMode(mask_stack.empty() ? SkBlendMode::kSrc
                                                     : mask_info->fBlendMode);
 
-        has_opacity |= BindProperty<ScalarValue>((*m)["o"], ascope,
+        has_opacity |= abuilder->bindProperty<ScalarValue>((*m)["o"], ascope,
             [mask_paint](const ScalarValue& o) {
                 mask_paint->setOpacity(o * 0.01f);
         }, 100.0f);
@@ -164,7 +164,7 @@
 } // namespace
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachNestedAnimation(const char* name,
-                                                                AnimatorScope* ascope) {
+                                                                AnimatorScope* ascope) const {
     class SkottieSGAdapter final : public sksg::RenderNode {
     public:
         explicit SkottieSGAdapter(sk_sp<Animation> animation)
@@ -230,7 +230,7 @@
 sk_sp<sksg::RenderNode> AnimationBuilder::attachAssetRef(
     const skjson::ObjectValue& jlayer, AnimatorScope* ascope,
     sk_sp<sksg::RenderNode>(AnimationBuilder::* attach_proc)(const skjson::ObjectValue& comp,
-                                                             AnimatorScope* ascope)) {
+                                                             AnimatorScope* ascope) const) const {
 
     const auto refId = ParseDefault<SkString>(jlayer["refId"], SkString());
     if (refId.isEmpty()) {
@@ -261,7 +261,7 @@
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachSolidLayer(const skjson::ObjectValue& jlayer,
-                                                           AnimatorScope*) {
+                                                           AnimatorScope*) const {
     const auto size = SkSize::Make(ParseDefault<float>(jlayer["sw"], 0.0f),
                                    ParseDefault<float>(jlayer["sh"], 0.0f));
     const skjson::StringValue* hex_str = jlayer["sc"];
@@ -281,7 +281,7 @@
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachImageAsset(const skjson::ObjectValue& jimage,
-                                                           AnimatorScope*) {
+                                                           AnimatorScope*) const {
     const skjson::StringValue* name = jimage["p"];
     const skjson::StringValue* path = jimage["u"];
     if (!name) {
@@ -306,12 +306,12 @@
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachImageLayer(const skjson::ObjectValue& jlayer,
-                                                           AnimatorScope* ascope) {
+                                                           AnimatorScope* ascope) const {
     return this->attachAssetRef(jlayer, ascope, &AnimationBuilder::attachImageAsset);
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachNullLayer(const skjson::ObjectValue& layer,
-                                                          AnimatorScope*) {
+                                                          AnimatorScope*) const {
     // Null layers are used solely to drive dependent transforms,
     // but we use free-floating sksg::Matrices for that purpose.
     return nullptr;
@@ -326,7 +326,8 @@
     SkTHashMap<int, sk_sp<sksg::Matrix>> fLayerMatrixMap;
     sk_sp<sksg::RenderNode>              fCurrentMatte;
 
-    sk_sp<sksg::Matrix> AttachLayerMatrix(const skjson::ObjectValue& jlayer) {
+    sk_sp<sksg::Matrix> AttachLayerMatrix(const skjson::ObjectValue& jlayer,
+                                          const AnimationBuilder* abuilder) {
         const auto layer_index = ParseDefault<int>(jlayer["ind"], -1);
         if (layer_index < 0)
             return nullptr;
@@ -334,11 +335,12 @@
         if (auto* m = fLayerMatrixMap.find(layer_index))
             return *m;
 
-        return this->AttachLayerMatrixImpl(jlayer, layer_index);
+        return this->AttachLayerMatrixImpl(jlayer, abuilder, layer_index);
     }
 
 private:
     sk_sp<sksg::Matrix> AttachParentLayerMatrix(const skjson::ObjectValue& jlayer,
+                                                const AnimationBuilder* abuilder,
                                                 int layer_index) {
         const auto parent_index = ParseDefault<int>(jlayer["parent"], -1);
         if (parent_index < 0 || parent_index == layer_index)
@@ -351,24 +353,27 @@
             if (!l) continue;
 
             if (ParseDefault<int>((*l)["ind"], -1) == parent_index) {
-                return this->AttachLayerMatrixImpl(*l, parent_index);
+                return this->AttachLayerMatrixImpl(*l, abuilder, parent_index);
             }
         }
 
         return nullptr;
     }
 
-    sk_sp<sksg::Matrix> AttachLayerMatrixImpl(const skjson::ObjectValue& jlayer, int layer_index) {
+    sk_sp<sksg::Matrix> AttachLayerMatrixImpl(const skjson::ObjectValue& jlayer,
+                                              const AnimationBuilder* abuilder,
+                                              int layer_index) {
         SkASSERT(!fLayerMatrixMap.find(layer_index));
 
         // Add a stub entry to break recursion cycles.
         fLayerMatrixMap.set(layer_index, nullptr);
 
-        auto parent_matrix = this->AttachParentLayerMatrix(jlayer, layer_index);
+        auto parent_matrix = this->AttachParentLayerMatrix(jlayer, abuilder, layer_index);
 
         if (const skjson::ObjectValue* jtransform = jlayer["ks"]) {
-            return *fLayerMatrixMap.set(layer_index, AttachMatrix(*jtransform, fScope,
-                                                                  std::move(parent_matrix)));
+            return *fLayerMatrixMap.set(layer_index,
+                                        abuilder->attachMatrix(*jtransform, fScope,
+                                                               std::move(parent_matrix)));
 
         }
         return nullptr;
@@ -376,11 +381,11 @@
 };
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachLayer(const skjson::ObjectValue* jlayer,
-                                                     AttachLayerContext* layerCtx) {
+                                                     AttachLayerContext* layerCtx) const {
     if (!jlayer) return nullptr;
 
     using LayerAttacher = sk_sp<sksg::RenderNode> (AnimationBuilder::*)(const skjson::ObjectValue&,
-                                                                        AnimatorScope*);
+                                                                        AnimatorScope*) const;
     static constexpr LayerAttacher gLayerAttachers[] = {
         &AnimationBuilder::attachPrecompLayer,  // 'ty': 0
         &AnimationBuilder::attachSolidLayer,    // 'ty': 1
@@ -409,17 +414,17 @@
     }
 
     // Optional layer mask.
-    layer = AttachMask((*jlayer)["masksProperties"], &layer_animators, std::move(layer));
+    layer = AttachMask((*jlayer)["masksProperties"], this, &layer_animators, std::move(layer));
 
     // Optional layer transform.
-    if (auto layerMatrix = layerCtx->AttachLayerMatrix(*jlayer)) {
+    if (auto layerMatrix = layerCtx->AttachLayerMatrix(*jlayer, this)) {
         layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix));
     }
 
     // Optional layer opacity.
     // TODO: de-dupe this "ks" lookup with matrix above.
     if (const skjson::ObjectValue* jtransform = (*jlayer)["ks"]) {
-        layer = AttachOpacity(*jtransform, &layer_animators, std::move(layer));
+        layer = this->attachOpacity(*jtransform, &layer_animators, std::move(layer));
     }
 
     // Optional layer effects.
@@ -492,7 +497,7 @@
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachComposition(const skjson::ObjectValue& comp,
-                                                            AnimatorScope* scope) {
+                                                            AnimatorScope* scope) const {
     const skjson::ArrayValue* jlayers = comp["layers"];
     if (!jlayers) return nullptr;
 
diff --git a/modules/skottie/src/SkottieLayerEffect.cpp b/modules/skottie/src/SkottieLayerEffect.cpp
index f3e9331..f2743a3 100644
--- a/modules/skottie/src/SkottieLayerEffect.cpp
+++ b/modules/skottie/src/SkottieLayerEffect.cpp
@@ -18,6 +18,7 @@
 namespace {
 
 sk_sp<sksg::RenderNode> AttachFillLayerEffect(const skjson::ArrayValue* jeffect_props,
+                                              const AnimationBuilder* abuilder,
                                               AnimatorScope* ascope,
                                               sk_sp<sksg::RenderNode> layer) {
     if (!jeffect_props) return layer;
@@ -29,7 +30,7 @@
 
         switch (const auto ty = ParseDefault<int>((*jprop)["ty"], -1)) {
         case 2: // color
-            color_node = AttachColor(*jprop, ascope, "v");
+            color_node = abuilder->attachColor(*jprop, ascope, "v");
             break;
         default:
             LOG("?? Ignoring unsupported fill effect poperty type: %d\n", ty);
@@ -46,13 +47,13 @@
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachLayerEffects(const skjson::ArrayValue& jeffects,
                                                              AnimatorScope* ascope,
-                                                             sk_sp<sksg::RenderNode> layer) {
+                                                             sk_sp<sksg::RenderNode> layer) const {
     for (const skjson::ObjectValue* jeffect : jeffects) {
         if (!jeffect) continue;
 
         switch (const auto ty = ParseDefault<int>((*jeffect)["ty"], -1)) {
         case 21: // Fill
-            layer = AttachFillLayerEffect((*jeffect)["ef"], ascope, std::move(layer));
+            layer = AttachFillLayerEffect((*jeffect)["ef"], this, ascope, std::move(layer));
             break;
         default:
             LOG("?? Unsupported layer effect type: %d\n", ty);
diff --git a/modules/skottie/src/SkottiePrecompLayer.cpp b/modules/skottie/src/SkottiePrecompLayer.cpp
index 2266825..8fc4167 100644
--- a/modules/skottie/src/SkottiePrecompLayer.cpp
+++ b/modules/skottie/src/SkottiePrecompLayer.cpp
@@ -8,7 +8,6 @@
 #include "SkottiePriv.h"
 
 #include "SkJSON.h"
-#include "SkottieAnimator.h"
 #include "SkottieJson.h"
 #include "SkottieValue.h"
 #include "SkMakeUnique.h"
@@ -20,7 +19,7 @@
 namespace internal {
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachPrecompLayer(const skjson::ObjectValue& jlayer,
-                                                             AnimatorScope* ascope) {
+                                                             AnimatorScope* ascope) const {
     const skjson::ObjectValue* time_remap = jlayer["tm"];
     const auto start_time = ParseDefault<float>(jlayer["st"], 0.0f),
              stretch_time = ParseDefault<float>(jlayer["sr"], 1.0f);
@@ -70,7 +69,7 @@
             // because both the lambda and the mapper are scoped/owned by ctx->fAnimators.
             auto* raw_mapper = time_mapper.get();
             auto  frame_rate = fFrameRate;
-            BindProperty<ScalarValue>(*time_remap, ascope,
+            this->bindProperty<ScalarValue>(*time_remap, ascope,
                     [raw_mapper, frame_rate](const ScalarValue& t) {
                         raw_mapper->remapTime(t * frame_rate);
                     });
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index 37332f6..3551029 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -49,6 +49,30 @@
 
     std::unique_ptr<sksg::Scene> parse(const skjson::ObjectValue&);
 
+    // This is the workhorse for property binding: depending on whether the property is animated,
+    // it will either apply immediately or instantiate and attach a keyframe animator.
+    template <typename T>
+    bool bindProperty(const skjson::Value&,
+                      AnimatorScope*,
+                      std::function<void(const T&)>&&,
+                      const T* default_igore = nullptr) const;
+
+    template <typename T>
+    bool bindProperty(const skjson::Value& jv,
+                      AnimatorScope* ascope,
+                      std::function<void(const T&)>&& apply,
+                      const T& default_ignore) const {
+        return this->bindProperty(jv, ascope, std::move(apply), &default_ignore);
+    }
+
+    sk_sp<sksg::Color> attachColor(const skjson::ObjectValue&, AnimatorScope*,
+                                   const char prop_name[]) const;
+    sk_sp<sksg::Matrix> attachMatrix(const skjson::ObjectValue&, AnimatorScope*,
+                                     sk_sp<sksg::Matrix>) const;
+    sk_sp<sksg::RenderNode> attachOpacity(const skjson::ObjectValue&, AnimatorScope*,
+                                      sk_sp<sksg::RenderNode>) const;
+    sk_sp<sksg::Path> attachPath(const skjson::Value&, AnimatorScope*) const;
+
 private:
     struct AttachLayerContext;
 
@@ -56,24 +80,24 @@
     void parseFonts (const skjson::ObjectValue* jfonts,
                      const skjson::ArrayValue* jchars);
 
-    sk_sp<sksg::RenderNode> attachComposition(const skjson::ObjectValue&, AnimatorScope*);
-    sk_sp<sksg::RenderNode> attachLayer(const skjson::ObjectValue*, AttachLayerContext*);
+    sk_sp<sksg::RenderNode> attachComposition(const skjson::ObjectValue&, AnimatorScope*) const;
+    sk_sp<sksg::RenderNode> attachLayer(const skjson::ObjectValue*, AttachLayerContext*) const;
     sk_sp<sksg::RenderNode> attachLayerEffects(const skjson::ArrayValue& jeffects, AnimatorScope*,
-                                               sk_sp<sksg::RenderNode>);
+                                               sk_sp<sksg::RenderNode>) const;
 
     sk_sp<sksg::RenderNode> attachAssetRef(const skjson::ObjectValue&, AnimatorScope*,
         sk_sp<sksg::RenderNode>(AnimationBuilder::*)(const skjson::ObjectValue&,
-                                                     AnimatorScope* ctx));
-    sk_sp<sksg::RenderNode> attachImageAsset(const skjson::ObjectValue&, AnimatorScope*);
+                                                     AnimatorScope* ctx) const) const;
+    sk_sp<sksg::RenderNode> attachImageAsset(const skjson::ObjectValue&, AnimatorScope*) const;
 
-    sk_sp<sksg::RenderNode> attachNestedAnimation(const char* name, AnimatorScope* ascope);
+    sk_sp<sksg::RenderNode> attachNestedAnimation(const char* name, AnimatorScope* ascope) const;
 
-    sk_sp<sksg::RenderNode> attachImageLayer  (const skjson::ObjectValue&, AnimatorScope*);
-    sk_sp<sksg::RenderNode> attachNullLayer   (const skjson::ObjectValue&, AnimatorScope*);
-    sk_sp<sksg::RenderNode> attachPrecompLayer(const skjson::ObjectValue&, AnimatorScope*);
-    sk_sp<sksg::RenderNode> attachShapeLayer  (const skjson::ObjectValue&, AnimatorScope*);
-    sk_sp<sksg::RenderNode> attachSolidLayer  (const skjson::ObjectValue&, AnimatorScope*);
-    sk_sp<sksg::RenderNode> attachTextLayer   (const skjson::ObjectValue&, AnimatorScope*);
+    sk_sp<sksg::RenderNode> attachImageLayer  (const skjson::ObjectValue&, AnimatorScope*) const;
+    sk_sp<sksg::RenderNode> attachNullLayer   (const skjson::ObjectValue&, AnimatorScope*) const;
+    sk_sp<sksg::RenderNode> attachPrecompLayer(const skjson::ObjectValue&, AnimatorScope*) const;
+    sk_sp<sksg::RenderNode> attachShapeLayer  (const skjson::ObjectValue&, AnimatorScope*) const;
+    sk_sp<sksg::RenderNode> attachSolidLayer  (const skjson::ObjectValue&, AnimatorScope*) const;
+    sk_sp<sksg::RenderNode> attachTextLayer   (const skjson::ObjectValue&, AnimatorScope*) const;
 
     sk_sp<ResourceProvider>    fResourceProvider;
     sk_sp<SkFontMgr>           fFontMgr;
@@ -100,20 +124,13 @@
     using AssetCache = SkTHashMap<SkString, sk_sp<sksg::RenderNode>>;
     using FontMap    = SkTHashMap<SkString, FontInfo>;
 
-    AssetMap   fAssets;
-    AssetCache fAssetCache;
-    FontMap    fFonts;
+    AssetMap           fAssets;
+    FontMap            fFonts;
+    mutable AssetCache fAssetCache;
 
     using INHERITED = SkNoncopyable;
 };
 
-// Shared helpers
-sk_sp<sksg::Color> AttachColor(const skjson::ObjectValue&, AnimatorScope*, const char prop_name[]);
-sk_sp<sksg::Path> AttachPath(const skjson::Value&, AnimatorScope*);
-sk_sp<sksg::Matrix> AttachMatrix(const skjson::ObjectValue&, AnimatorScope*, sk_sp<sksg::Matrix>);
-sk_sp<sksg::RenderNode> AttachOpacity(const skjson::ObjectValue&, AnimatorScope*,
-                                      sk_sp<sksg::RenderNode>);
-
 } // namespace internal
 } // namespace skottie
 
diff --git a/modules/skottie/src/SkottieShapeLayer.cpp b/modules/skottie/src/SkottieShapeLayer.cpp
index 3ad7a50..54f7675 100644
--- a/modules/skottie/src/SkottieShapeLayer.cpp
+++ b/modules/skottie/src/SkottieShapeLayer.cpp
@@ -9,7 +9,6 @@
 
 #include "SkJSON.h"
 #include "SkottieAdapter.h"
-#include "SkottieAnimator.h"
 #include "SkottieJson.h"
 #include "SkottieValue.h"
 #include "SkPath.h"
@@ -31,24 +30,26 @@
 namespace {
 
 sk_sp<sksg::GeometryNode> AttachPathGeometry(const skjson::ObjectValue& jpath,
+                                             const AnimationBuilder* abuilder,
                                              AnimatorScope* ascope) {
-    return AttachPath(jpath["ks"], ascope);
+    return abuilder->attachPath(jpath["ks"], ascope);
 }
 
 sk_sp<sksg::GeometryNode> AttachRRectGeometry(const skjson::ObjectValue& jrect,
+                                              const AnimationBuilder* abuilder,
                                               AnimatorScope* ascope) {
     auto rect_node = sksg::RRect::Make();
     auto adapter = sk_make_sp<RRectAdapter>(rect_node);
 
-    auto p_attached = BindProperty<VectorValue>(jrect["p"], ascope,
+    auto p_attached = abuilder->bindProperty<VectorValue>(jrect["p"], ascope,
         [adapter](const VectorValue& p) {
             adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
         });
-    auto s_attached = BindProperty<VectorValue>(jrect["s"], ascope,
+    auto s_attached = abuilder->bindProperty<VectorValue>(jrect["s"], ascope,
         [adapter](const VectorValue& s) {
             adapter->setSize(ValueTraits<VectorValue>::As<SkSize>(s));
         });
-    auto r_attached = BindProperty<ScalarValue>(jrect["r"], ascope,
+    auto r_attached = abuilder->bindProperty<ScalarValue>(jrect["r"], ascope,
         [adapter](const ScalarValue& r) {
             adapter->setRadius(SkSize::Make(r, r));
         });
@@ -61,15 +62,16 @@
 }
 
 sk_sp<sksg::GeometryNode> AttachEllipseGeometry(const skjson::ObjectValue& jellipse,
+                                                const AnimationBuilder* abuilder,
                                                 AnimatorScope* ascope) {
     auto rect_node = sksg::RRect::Make();
     auto adapter = sk_make_sp<RRectAdapter>(rect_node);
 
-    auto p_attached = BindProperty<VectorValue>(jellipse["p"], ascope,
+    auto p_attached = abuilder->bindProperty<VectorValue>(jellipse["p"], ascope,
         [adapter](const VectorValue& p) {
             adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
         });
-    auto s_attached = BindProperty<VectorValue>(jellipse["s"], ascope,
+    auto s_attached = abuilder->bindProperty<VectorValue>(jellipse["s"], ascope,
         [adapter](const VectorValue& s) {
             const auto sz = ValueTraits<VectorValue>::As<SkSize>(s);
             adapter->setSize(sz);
@@ -84,6 +86,7 @@
 }
 
 sk_sp<sksg::GeometryNode> AttachPolystarGeometry(const skjson::ObjectValue& jstar,
+                                                 const AnimationBuilder* abuilder,
                                                  AnimatorScope* ascope) {
     static constexpr PolyStarAdapter::Type gTypes[] = {
         PolyStarAdapter::Type::kStar, // "sy": 1
@@ -99,31 +102,31 @@
     auto path_node = sksg::Path::Make();
     auto adapter = sk_make_sp<PolyStarAdapter>(path_node, gTypes[type]);
 
-    BindProperty<VectorValue>(jstar["p"], ascope,
+    abuilder->bindProperty<VectorValue>(jstar["p"], ascope,
         [adapter](const VectorValue& p) {
             adapter->setPosition(ValueTraits<VectorValue>::As<SkPoint>(p));
         });
-    BindProperty<ScalarValue>(jstar["pt"], ascope,
+    abuilder->bindProperty<ScalarValue>(jstar["pt"], ascope,
         [adapter](const ScalarValue& pt) {
             adapter->setPointCount(pt);
         });
-    BindProperty<ScalarValue>(jstar["ir"], ascope,
+    abuilder->bindProperty<ScalarValue>(jstar["ir"], ascope,
         [adapter](const ScalarValue& ir) {
             adapter->setInnerRadius(ir);
         });
-    BindProperty<ScalarValue>(jstar["or"], ascope,
+    abuilder->bindProperty<ScalarValue>(jstar["or"], ascope,
         [adapter](const ScalarValue& otr) {
             adapter->setOuterRadius(otr);
         });
-    BindProperty<ScalarValue>(jstar["is"], ascope,
+    abuilder->bindProperty<ScalarValue>(jstar["is"], ascope,
         [adapter](const ScalarValue& is) {
             adapter->setInnerRoundness(is);
         });
-    BindProperty<ScalarValue>(jstar["os"], ascope,
+    abuilder->bindProperty<ScalarValue>(jstar["os"], ascope,
         [adapter](const ScalarValue& os) {
             adapter->setOuterRoundness(os);
         });
-    BindProperty<ScalarValue>(jstar["r"], ascope,
+    abuilder->bindProperty<ScalarValue>(jstar["r"], ascope,
         [adapter](const ScalarValue& r) {
             adapter->setRotation(r);
         });
@@ -131,7 +134,8 @@
     return std::move(path_node);
 }
 
-sk_sp<sksg::Gradient> AttachGradient(const skjson::ObjectValue& jgrad, AnimatorScope* ascope) {
+sk_sp<sksg::Gradient> AttachGradient(const skjson::ObjectValue& jgrad,
+                                     const AnimationBuilder* abuilder, AnimatorScope* ascope) {
     const skjson::ObjectValue* stops = jgrad["g"];
     if (!stops)
         return nullptr;
@@ -155,15 +159,15 @@
         gradient_node = std::move(radial_node);
     }
 
-    BindProperty<VectorValue>((*stops)["k"], ascope,
+    abuilder->bindProperty<VectorValue>((*stops)["k"], ascope,
         [adapter](const VectorValue& stops) {
             adapter->setColorStops(stops);
         });
-    BindProperty<VectorValue>(jgrad["s"], ascope,
+    abuilder->bindProperty<VectorValue>(jgrad["s"], ascope,
         [adapter](const VectorValue& s) {
             adapter->setStartPoint(ValueTraits<VectorValue>::As<SkPoint>(s));
         });
-    BindProperty<VectorValue>(jgrad["e"], ascope,
+    abuilder->bindProperty<VectorValue>(jgrad["e"], ascope,
         [adapter](const VectorValue& e) {
             adapter->setEndPoint(ValueTraits<VectorValue>::As<SkPoint>(e));
         });
@@ -171,12 +175,13 @@
     return gradient_node;
 }
 
-sk_sp<sksg::PaintNode> AttachPaint(const skjson::ObjectValue& jpaint, AnimatorScope* ascope,
+sk_sp<sksg::PaintNode> AttachPaint(const skjson::ObjectValue& jpaint,
+                                   const AnimationBuilder* abuilder, AnimatorScope* ascope,
                                    sk_sp<sksg::PaintNode> paint_node) {
     if (paint_node) {
         paint_node->setAntiAlias(true);
 
-        BindProperty<ScalarValue>(jpaint["o"], ascope,
+        abuilder->bindProperty<ScalarValue>(jpaint["o"], ascope,
             [paint_node](const ScalarValue& o) {
                 // BM opacity is [0..100]
                 paint_node->setOpacity(o * 0.01f);
@@ -186,14 +191,15 @@
     return paint_node;
 }
 
-sk_sp<sksg::PaintNode> AttachStroke(const skjson::ObjectValue& jstroke, AnimatorScope* ascope,
+sk_sp<sksg::PaintNode> AttachStroke(const skjson::ObjectValue& jstroke,
+                                    const AnimationBuilder* abuilder, AnimatorScope* ascope,
                                     sk_sp<sksg::PaintNode> stroke_node) {
     if (!stroke_node)
         return nullptr;
 
     stroke_node->setStyle(SkPaint::kStroke_Style);
 
-    BindProperty<ScalarValue>(jstroke["w"], ascope,
+    abuilder->bindProperty<ScalarValue>(jstroke["w"], ascope,
         [stroke_node](const ScalarValue& w) {
             stroke_node->setStrokeWidth(w);
         });
@@ -219,24 +225,30 @@
     return stroke_node;
 }
 
-sk_sp<sksg::PaintNode> AttachColorFill(const skjson::ObjectValue& jfill, AnimatorScope* ascope) {
-    return AttachPaint(jfill, ascope, AttachColor(jfill, ascope, "c"));
+sk_sp<sksg::PaintNode> AttachColorFill(const skjson::ObjectValue& jfill,
+                                       const AnimationBuilder* abuilder, AnimatorScope* ascope) {
+    return AttachPaint(jfill, abuilder, ascope, abuilder->attachColor(jfill, ascope, "c"));
 }
 
-sk_sp<sksg::PaintNode> AttachGradientFill(const skjson::ObjectValue& jfill, AnimatorScope* ascope) {
-    return AttachPaint(jfill, ascope, AttachGradient(jfill, ascope));
+sk_sp<sksg::PaintNode> AttachGradientFill(const skjson::ObjectValue& jfill,
+                                          const AnimationBuilder* abuilder, AnimatorScope* ascope) {
+    return AttachPaint(jfill, abuilder, ascope, AttachGradient(jfill, abuilder, ascope));
 }
 
 sk_sp<sksg::PaintNode> AttachColorStroke(const skjson::ObjectValue& jstroke,
+                                         const AnimationBuilder* abuilder,
                                          AnimatorScope* ascope) {
-    return AttachStroke(jstroke, ascope, AttachPaint(jstroke, ascope,
-                                                     AttachColor(jstroke, ascope, "c")));
+    return AttachStroke(jstroke, abuilder, ascope,
+                        AttachPaint(jstroke, abuilder, ascope,
+                                    abuilder->attachColor(jstroke, ascope, "c")));
 }
 
 sk_sp<sksg::PaintNode> AttachGradientStroke(const skjson::ObjectValue& jstroke,
+                                            const AnimationBuilder* abuilder,
                                             AnimatorScope* ascope) {
-    return AttachStroke(jstroke, ascope, AttachPaint(jstroke, ascope,
-                                                     AttachGradient(jstroke, ascope)));
+    return AttachStroke(jstroke, abuilder, ascope,
+                        AttachPaint(jstroke, abuilder, ascope,
+                                    AttachGradient(jstroke, abuilder, ascope)));
 }
 
 sk_sp<sksg::Merge> Merge(std::vector<sk_sp<sksg::GeometryNode>>&& geos, sksg::Merge::Mode mode) {
@@ -252,7 +264,7 @@
 }
 
 std::vector<sk_sp<sksg::GeometryNode>> AttachMergeGeometryEffect(
-        const skjson::ObjectValue& jmerge, AnimatorScope*,
+        const skjson::ObjectValue& jmerge, const AnimationBuilder*, AnimatorScope*,
         std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
     static constexpr sksg::Merge::Mode gModes[] = {
         sksg::Merge::Mode::kMerge,      // "mm": 1
@@ -272,7 +284,7 @@
 }
 
 std::vector<sk_sp<sksg::GeometryNode>> AttachTrimGeometryEffect(
-        const skjson::ObjectValue& jtrim, AnimatorScope* ascope,
+        const skjson::ObjectValue& jtrim, const AnimationBuilder* abuilder, AnimatorScope* ascope,
         std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
 
     enum class Mode {
@@ -297,15 +309,15 @@
         trimmed.push_back(trimEffect);
 
         const auto adapter = sk_make_sp<TrimEffectAdapter>(std::move(trimEffect));
-        BindProperty<ScalarValue>(jtrim["s"], ascope,
+        abuilder->bindProperty<ScalarValue>(jtrim["s"], ascope,
             [adapter](const ScalarValue& s) {
                 adapter->setStart(s);
             });
-        BindProperty<ScalarValue>(jtrim["e"], ascope,
+        abuilder->bindProperty<ScalarValue>(jtrim["e"], ascope,
             [adapter](const ScalarValue& e) {
                 adapter->setEnd(e);
             });
-        BindProperty<ScalarValue>(jtrim["o"], ascope,
+        abuilder->bindProperty<ScalarValue>(jtrim["o"], ascope,
             [adapter](const ScalarValue& o) {
                 adapter->setOffset(o);
             });
@@ -315,7 +327,7 @@
 }
 
 std::vector<sk_sp<sksg::GeometryNode>> AttachRoundGeometryEffect(
-        const skjson::ObjectValue& jtrim, AnimatorScope* ascope,
+        const skjson::ObjectValue& jtrim, const AnimationBuilder* abuilder, AnimatorScope* ascope,
         std::vector<sk_sp<sksg::GeometryNode>>&& geos) {
 
     std::vector<sk_sp<sksg::GeometryNode>> rounded;
@@ -325,7 +337,7 @@
         const auto roundEffect = sksg::RoundEffect::Make(std::move(g));
         rounded.push_back(roundEffect);
 
-        BindProperty<ScalarValue>(jtrim["r"], ascope,
+        abuilder->bindProperty<ScalarValue>(jtrim["r"], ascope,
             [roundEffect](const ScalarValue& r) {
                 roundEffect->setRadius(r);
             });
@@ -334,7 +346,8 @@
     return rounded;
 }
 
-using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const skjson::ObjectValue&, AnimatorScope*);
+using GeometryAttacherT = sk_sp<sksg::GeometryNode> (*)(const skjson::ObjectValue&,
+                                                        const AnimationBuilder*, AnimatorScope*);
 static constexpr GeometryAttacherT gGeometryAttachers[] = {
     AttachPathGeometry,
     AttachRRectGeometry,
@@ -342,7 +355,8 @@
     AttachPolystarGeometry,
 };
 
-using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const skjson::ObjectValue&, AnimatorScope*);
+using PaintAttacherT = sk_sp<sksg::PaintNode> (*)(const skjson::ObjectValue&,
+                                                  const AnimationBuilder*, AnimatorScope*);
 static constexpr PaintAttacherT gPaintAttachers[] = {
     AttachColorFill,
     AttachColorStroke,
@@ -352,7 +366,7 @@
 
 using GeometryEffectAttacherT =
     std::vector<sk_sp<sksg::GeometryNode>> (*)(const skjson::ObjectValue&,
-                                               AnimatorScope*,
+                                               const AnimationBuilder*, AnimatorScope*,
                                                std::vector<sk_sp<sksg::GeometryNode>>&&);
 static constexpr GeometryEffectAttacherT gGeometryEffectAttachers[] = {
     AttachMergeGeometryEffect,
@@ -414,27 +428,29 @@
 };
 
 struct AttachShapeContext {
-    AttachShapeContext(AnimatorScope* ascope,
+    AttachShapeContext(const AnimationBuilder* abuilder,
+                       AnimatorScope* ascope,
                        std::vector<sk_sp<sksg::GeometryNode>>* geos,
                        std::vector<GeometryEffectRec>* effects,
                        size_t committedAnimators)
-        : fScope(ascope)
+        : fBuilder(abuilder)
+        , fScope(ascope)
         , fGeometryStack(geos)
         , fGeometryEffectStack(effects)
         , fCommittedAnimators(committedAnimators) {}
 
+    const AnimationBuilder*                 fBuilder;
     AnimatorScope*                          fScope;
     std::vector<sk_sp<sksg::GeometryNode>>* fGeometryStack;
     std::vector<GeometryEffectRec>*         fGeometryEffectStack;
     size_t                                  fCommittedAnimators;
 };
 
-sk_sp<sksg::RenderNode> AttachShape(const skjson::ArrayValue* jshape,
-                                    AttachShapeContext* shapeCtx) {
+sk_sp<sksg::RenderNode> AttachShape(const skjson::ArrayValue* jshape, AttachShapeContext* ctx) {
     if (!jshape)
         return nullptr;
 
-    SkDEBUGCODE(const auto initialGeometryEffects = shapeCtx->fGeometryEffectStack->size();)
+    SkDEBUGCODE(const auto initialGeometryEffects = ctx->fGeometryEffectStack->size();)
 
     sk_sp<sksg::Group> shape_group = sksg::Group::Make();
     sk_sp<sksg::RenderNode> shape_wrapper = shape_group;
@@ -466,14 +482,15 @@
 
         switch (info->fShapeType) {
         case ShapeType::kTransform:
-            if ((shape_matrix = AttachMatrix(*shape, shapeCtx->fScope, nullptr))) {
+            if ((shape_matrix = ctx->fBuilder->attachMatrix(*shape, ctx->fScope, nullptr))) {
                 shape_wrapper = sksg::Transform::Make(std::move(shape_wrapper), shape_matrix);
             }
-            shape_wrapper = AttachOpacity(*shape, shapeCtx->fScope, std::move(shape_wrapper));
+            shape_wrapper = ctx->fBuilder->attachOpacity(*shape, ctx->fScope,
+                                                         std::move(shape_wrapper));
             break;
         case ShapeType::kGeometryEffect:
             SkASSERT(info->fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
-            shapeCtx->fGeometryEffectStack->push_back(
+            ctx->fGeometryEffectStack->push_back(
                 { *shape, gGeometryEffectAttachers[info->fAttacherIndex] });
             break;
         default:
@@ -493,7 +510,8 @@
         case ShapeType::kGeometry: {
             SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryAttachers));
             if (auto geo = gGeometryAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
-                                                                         shapeCtx->fScope)) {
+                                                                         ctx->fBuilder,
+                                                                         ctx->fScope)) {
                 geos.push_back(std::move(geo));
             }
         } break;
@@ -502,38 +520,42 @@
             SkASSERT(rec->fInfo.fAttacherIndex < SK_ARRAY_COUNT(gGeometryEffectAttachers));
             if (!geos.empty()) {
                 geos = gGeometryEffectAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
-                                                                           shapeCtx->fScope,
+                                                                           ctx->fBuilder,
+                                                                           ctx->fScope,
                                                                            std::move(geos));
             }
 
-            SkASSERT(&shapeCtx->fGeometryEffectStack->back().fJson == &rec->fJson);
-            SkASSERT(shapeCtx->fGeometryEffectStack->back().fAttach ==
+            SkASSERT(&ctx->fGeometryEffectStack->back().fJson == &rec->fJson);
+            SkASSERT(ctx->fGeometryEffectStack->back().fAttach ==
                      gGeometryEffectAttachers[rec->fInfo.fAttacherIndex]);
-            shapeCtx->fGeometryEffectStack->pop_back();
+            ctx->fGeometryEffectStack->pop_back();
         } break;
         case ShapeType::kGroup: {
-            AttachShapeContext groupShapeCtx(shapeCtx->fScope,
+            AttachShapeContext groupShapeCtx(ctx->fBuilder,
+                                             ctx->fScope,
                                              &geos,
-                                             shapeCtx->fGeometryEffectStack,
-                                             shapeCtx->fCommittedAnimators);
+                                             ctx->fGeometryEffectStack,
+                                             ctx->fCommittedAnimators);
             if (auto subgroup = AttachShape(rec->fJson["it"], &groupShapeCtx)) {
                 draws.push_back(std::move(subgroup));
-                SkASSERT(groupShapeCtx.fCommittedAnimators >= shapeCtx->fCommittedAnimators);
-                shapeCtx->fCommittedAnimators = groupShapeCtx.fCommittedAnimators;
+                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, shapeCtx->fScope);
+            auto paint = gPaintAttachers[rec->fInfo.fAttacherIndex](rec->fJson,
+                                                                    ctx->fBuilder,
+                                                                    ctx->fScope);
             if (!paint || geos.empty())
                 break;
 
             auto drawGeos = geos;
 
             // Apply all pending effects from the stack.
-            for (auto it = shapeCtx->fGeometryEffectStack->rbegin();
-                 it != shapeCtx->fGeometryEffectStack->rend(); ++it) {
-                drawGeos = it->fAttach(it->fJson, shapeCtx->fScope, std::move(drawGeos));
+            for (auto it = ctx->fGeometryEffectStack->rbegin();
+                 it != ctx->fGeometryEffectStack->rend(); ++it) {
+                drawGeos = it->fAttach(it->fJson, ctx->fBuilder, ctx->fScope, std::move(drawGeos));
             }
 
             // If we still have multiple geos, reduce using 'merge'.
@@ -543,7 +565,7 @@
 
             SkASSERT(geo);
             draws.push_back(sksg::Draw::Make(std::move(geo), std::move(paint)));
-            shapeCtx->fCommittedAnimators = shapeCtx->fScope->size();
+            ctx->fCommittedAnimators = ctx->fScope->size();
         } break;
         default:
             break;
@@ -551,11 +573,11 @@
     }
 
     // By now we should have popped all local geometry effects.
-    SkASSERT(shapeCtx->fGeometryEffectStack->size() == initialGeometryEffects);
+    SkASSERT(ctx->fGeometryEffectStack->size() == initialGeometryEffects);
 
     // Push transformed local geometries to parent list, for subsequent paints.
     for (const auto& geo : geos) {
-        shapeCtx->fGeometryStack->push_back(shape_matrix
+        ctx->fGeometryStack->push_back(shape_matrix
             ? sksg::GeometryTransform::Make(std::move(geo), shape_matrix)
             : std::move(geo));
     }
@@ -571,10 +593,10 @@
 } // namespace
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachShapeLayer(const skjson::ObjectValue& layer,
-                                                           AnimatorScope* ascope) {
+                                                           AnimatorScope* ascope) const {
     std::vector<sk_sp<sksg::GeometryNode>> geometryStack;
     std::vector<GeometryEffectRec> geometryEffectStack;
-    AttachShapeContext shapeCtx(ascope, &geometryStack, &geometryEffectStack, ascope->size());
+    AttachShapeContext shapeCtx(this, ascope, &geometryStack, &geometryEffectStack, ascope->size());
     auto shapeNode = AttachShape(layer["shapes"], &shapeCtx);
 
     // Trim uncommitted animators: AttachShape consumes effects on the fly, and greedily attaches
diff --git a/modules/skottie/src/SkottieTextLayer.cpp b/modules/skottie/src/SkottieTextLayer.cpp
index d39f57d..cb62a5b 100644
--- a/modules/skottie/src/SkottieTextLayer.cpp
+++ b/modules/skottie/src/SkottieTextLayer.cpp
@@ -206,7 +206,7 @@
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& layer,
-                                                          AnimatorScope* ascope) {
+                                                          AnimatorScope* ascope) const {
     // General text node format:
     // "t": {
     //    "a": [], // animators (TODO)
diff --git a/modules/skottie/src/SkottieValue.cpp b/modules/skottie/src/SkottieValue.cpp
index d564e51..910e8c3 100644
--- a/modules/skottie/src/SkottieValue.cpp
+++ b/modules/skottie/src/SkottieValue.cpp
@@ -8,6 +8,8 @@
 #include "SkottieValue.h"
 
 #include "SkColor.h"
+#include "SkottieJson.h"
+#include "SkottiePriv.h"
 #include "SkNx.h"
 #include "SkPoint.h"
 #include "SkSize.h"
@@ -15,6 +17,12 @@
 namespace  skottie {
 
 template <>
+bool ValueTraits<ScalarValue>::FromJSON(const skjson::Value& jv, const internal::AnimationBuilder*,
+                                        ScalarValue* v) {
+    return Parse(jv, v);
+}
+
+template <>
 bool ValueTraits<ScalarValue>::CanLerp(const ScalarValue&, const ScalarValue&) {
     return true;
 }
@@ -33,6 +41,12 @@
 }
 
 template <>
+bool ValueTraits<VectorValue>::FromJSON(const skjson::Value& jv, const internal::AnimationBuilder*,
+                                        VectorValue* v) {
+    return Parse(jv, v);
+}
+
+template <>
 bool ValueTraits<VectorValue>::CanLerp(const VectorValue& v1, const VectorValue& v2) {
     return v1.size() == v2.size();
 }
@@ -80,6 +94,67 @@
     return SkSize::Make(pt.x(), pt.y());
 }
 
+namespace {
+
+bool ParsePointVec(const skjson::Value& jv, std::vector<SkPoint>* pts) {
+    if (!jv.is<skjson::ArrayValue>())
+        return false;
+    const auto& av = jv.as<skjson::ArrayValue>();
+
+    pts->clear();
+    pts->reserve(av.size());
+
+    std::vector<float> vec;
+    for (size_t i = 0; i < av.size(); ++i) {
+        if (!Parse(av[i], &vec) || vec.size() != 2)
+            return false;
+        pts->push_back(SkPoint::Make(vec[0], vec[1]));
+    }
+
+    return true;
+}
+
+} // namespace
+
+template <>
+bool ValueTraits<ShapeValue>::FromJSON(const skjson::Value& jv,
+                                       const internal::AnimationBuilder* abuilder,
+                                       ShapeValue* v) {
+    SkASSERT(v->fVertices.empty());
+
+    // Some versions wrap values as single-element arrays.
+    if (const skjson::ArrayValue* av = jv) {
+        if (av->size() == 1) {
+            return FromJSON((*av)[0], abuilder, v);
+        }
+    }
+
+    if (!jv.is<skjson::ObjectValue>())
+        return false;
+    const auto& ov = jv.as<skjson::ObjectValue>();
+
+    std::vector<SkPoint> inPts,  // Cubic Bezier "in" control points, relative to vertices.
+                         outPts, // Cubic Bezier "out" control points, relative to vertices.
+                         verts;  // Cubic Bezier vertices.
+
+    if (!ParsePointVec(ov["i"], &inPts) ||
+        !ParsePointVec(ov["o"], &outPts) ||
+        !ParsePointVec(ov["v"], &verts) ||
+        inPts.size() != outPts.size() ||
+        inPts.size() != verts.size()) {
+
+        return false;
+    }
+
+    v->fVertices.reserve(inPts.size());
+    for (size_t i = 0; i < inPts.size(); ++i) {
+        v->fVertices.push_back(BezierVertex({inPts[i], outPts[i], verts[i]}));
+    }
+    v->fClosed = ParseDefault<bool>(ov["c"], false);
+
+    return true;
+}
+
 template <>
 bool ValueTraits<ShapeValue>::CanLerp(const ShapeValue& v1, const ShapeValue& v2) {
     return v1.fVertices.size() == v2.fVertices.size()
diff --git a/modules/skottie/src/SkottieValue.h b/modules/skottie/src/SkottieValue.h
index 46e08cb..8e62985 100644
--- a/modules/skottie/src/SkottieValue.h
+++ b/modules/skottie/src/SkottieValue.h
@@ -13,10 +13,17 @@
 
 #include <vector>
 
-namespace  skottie {
+namespace skjson { class Value; }
+
+namespace skottie {
+namespace internal {
+class AnimationBuilder;
+} // namespace internal
 
 template <typename T>
 struct ValueTraits {
+    static bool FromJSON(const skjson::Value&, const internal::AnimationBuilder*, T*);
+
     template <typename U>
     static U As(const T&);