[skottie] Initial animated text properties support

Limitations:

  - no range selectors (applies to the whole text)
  - only position, fill color and stroke color for now

Change-Id: I91e88a6107c5f66687c1c27f27a71be3914bde25
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/217386
Commit-Queue: Florin Malita <fmalita@chromium.org>
Reviewed-by: Ben Wagner <bungeman@google.com>
diff --git a/modules/skottie/skottie.gni b/modules/skottie/skottie.gni
index 1b64d66..6a1488e 100644
--- a/modules/skottie/skottie.gni
+++ b/modules/skottie/skottie.gni
@@ -32,6 +32,8 @@
   "$_src/text/SkottieShaper.h",
   "$_src/text/TextAdapter.cpp",
   "$_src/text/TextAdapter.h",
+  "$_src/text/TextAnimator.cpp",
+  "$_src/text/TextAnimator.h",
   "$_src/text/TextLayer.cpp",
   "$_src/text/TextValue.cpp",
   "$_src/text/TextValue.h",
diff --git a/modules/skottie/src/text/TextAdapter.cpp b/modules/skottie/src/text/TextAdapter.cpp
index 2bfb238..8ccc5bb 100644
--- a/modules/skottie/src/text/TextAdapter.cpp
+++ b/modules/skottie/src/text/TextAdapter.cpp
@@ -23,10 +23,12 @@
 
 struct TextAdapter::FragmentRec {
     // More text SG props will surface here as we add range selector support.
-    sk_sp<sksg::TransformEffect> fRoot;
+    sk_sp<sksg::Matrix<SkMatrix>> fMatrixNode;
+    sk_sp<sksg::Color>            fFillColorNode,
+                                  fStrokeColorNode;
 };
 
-TextAdapter::FragmentRec TextAdapter::buildFragment(const skottie::Shaper::Fragment& frag) const {
+void TextAdapter::addFragment(const skottie::Shaper::Fragment& frag) {
     // For a given shaped fragment, build a corresponding SG fragment:
     //
     //   [TransformEffect] -> [Transform]
@@ -39,21 +41,24 @@
     auto blob_node = sksg::TextBlob::Make(frag.fBlob);
     blob_node->setPosition(frag.fPos);
 
+    FragmentRec rec;
+    rec.fMatrixNode = sksg::Matrix<SkMatrix>::Make(SkMatrix::I());
+
     std::vector<sk_sp<sksg::RenderNode>> draws;
     draws.reserve(static_cast<size_t>(fText.fHasFill) + static_cast<size_t>(fText.fHasStroke));
 
     SkASSERT(fText.fHasFill || fText.fHasStroke);
 
     if (fText.fHasFill) {
-        auto fill_paint = sksg::Color::Make(fText.fFillColor);
-        fill_paint->setAntiAlias(true);
-        draws.push_back(sksg::Draw::Make(blob_node, std::move(fill_paint)));
+        rec.fFillColorNode = sksg::Color::Make(fText.fFillColor);
+        rec.fFillColorNode->setAntiAlias(true);
+        draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
     }
     if (fText.fHasStroke) {
-        auto stroke_paint = sksg::Color::Make(fText.fStrokeColor);
-        stroke_paint->setAntiAlias(true);
-        stroke_paint->setStyle(SkPaint::kStroke_Style);
-        draws.push_back(sksg::Draw::Make(blob_node, std::move(stroke_paint)));
+        rec.fStrokeColorNode = sksg::Color::Make(fText.fStrokeColor);
+        rec.fStrokeColorNode->setAntiAlias(true);
+        rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
+        draws.push_back(sksg::Draw::Make(blob_node, rec.fStrokeColorNode));
     }
 
     SkASSERT(!draws.empty());
@@ -62,9 +67,8 @@
             ? sksg::Group::Make(std::move(draws))
             : std::move(draws[0]);
 
-    return {
-        sksg::TransformEffect::Make(std::move(draws_node), SkMatrix::I()),
-    };
+    fRoot->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
+    fFragments.push_back(std::move(rec));
 }
 
 void TextAdapter::apply() {
@@ -89,8 +93,7 @@
     fFragments.clear();
 
     for (const auto& frag : shape_result.fFragments) {
-        fFragments.push_back(this->buildFragment(frag));
-        fRoot->addChild(fFragments.back().fRoot);
+        this->addFragment(frag);
     }
 
 #if (0)
@@ -112,4 +115,19 @@
 #endif
 }
 
+void TextAdapter::applyAnimatedProps(const AnimatedProps& props) {
+    const auto t = SkMatrix::MakeTrans(props.position.x(), props.position.y());
+
+    for (const auto& rec : fFragments) {
+        rec.fMatrixNode->setMatrix(t);
+
+        if (rec.fFillColorNode) {
+            rec.fFillColorNode->setColor(props.fill_color);
+        }
+        if (rec.fStrokeColorNode) {
+            rec.fStrokeColorNode->setColor(props.stroke_color);
+        }
+    }
+}
+
 } // namespace skottie
diff --git a/modules/skottie/src/text/TextAdapter.h b/modules/skottie/src/text/TextAdapter.h
index 8fcdf14..dd27e64 100644
--- a/modules/skottie/src/text/TextAdapter.h
+++ b/modules/skottie/src/text/TextAdapter.h
@@ -29,10 +29,18 @@
 
     const sk_sp<sksg::Group>& root() const { return fRoot; }
 
+    struct AnimatedProps {
+        SkPoint   position = { 0, 0 };
+        SkColor fill_color = SK_ColorTRANSPARENT,
+              stroke_color = SK_ColorTRANSPARENT;
+    };
+
+    void applyAnimatedProps(const AnimatedProps&);
+
 private:
     struct FragmentRec;
 
-    FragmentRec buildFragment(const skottie::Shaper::Fragment&) const;
+    void addFragment(const skottie::Shaper::Fragment&);
 
     void apply();
 
diff --git a/modules/skottie/src/text/TextAnimator.cpp b/modules/skottie/src/text/TextAnimator.cpp
new file mode 100644
index 0000000..376fd86
--- /dev/null
+++ b/modules/skottie/src/text/TextAnimator.cpp
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2019 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/text/TextAnimator.h"
+
+#include "include/core/SkColor.h"
+#include "include/core/SkPoint.h"
+#include "modules/skottie/src/SkottiePriv.h"
+#include "modules/skottie/src/text/TextAdapter.h"
+#include "src/utils/SkJSON.h"
+
+namespace skottie {
+namespace internal {
+
+/*
+ * Text layers can have optional text property animators.
+ *
+ * Each animator consists of
+ *
+ *   1) a list of animated properties (e.g. position, fill color, etc)
+ *
+ *   2) a list of range selectors
+ *
+ * Animated properties yield new values to be applied to the text, while range selectors
+ * determine the text subset these new values are applied to.
+ *
+ * The best way to think of range selectors is in terms of coverage: they combine to generate
+ * a coverage value [0..1] for each text fragment/glyph.  This coverage is then used to modulate
+ * how the new property value is applied to a given fragment (interpolation weight).
+ *
+ * Note: Bodymovin currently only supports a single selector.
+ *
+ * JSON structure:
+ *
+ * "t": {              // text node
+ *   "a": [            // animators list
+ *     {               // animator node
+ *       "s": {...},   // selector node (TODO)
+ *       "a": {        // animator properties node
+ *         "a":  {}    // optional anchor point value
+ *         "p":  {},   // optional position value
+ *         "s":  {},   // optional scale value
+ *         "o":  {},   // optional opacity
+ *         "fc": {},   // optional fill color value
+ *         "sc": {},   // optional stroke color value
+ *
+ *         // TODO: more props?
+ *       }
+ *     },
+ *     ...
+ *   ],
+ *   ...
+ * }
+ */
+class TextAnimator final : public SkNVRefCnt<TextAnimator> {
+public:
+    static sk_sp<TextAnimator> Make(const skjson::ObjectValue* janimator,
+                                    const AnimationBuilder* abuilder,
+                                    AnimatorScope* ascope) {
+        if (!janimator) {
+            return nullptr;
+        }
+
+        if (const skjson::ObjectValue* jselector = (*janimator)["s"]) {
+            abuilder->log(Logger::Level::kWarning, jselector, "Unsupported text range selector.");
+        }
+
+        const skjson::ObjectValue* jprops = (*janimator)["a"];
+
+        return jprops
+            ? sk_sp<TextAnimator>(new TextAnimator(*jprops, abuilder, ascope))
+            : nullptr;
+    }
+
+    void modulateProps(TextAdapter::AnimatedProps* dst) const {
+        // Position is additive.
+        if (fHasPosition) {
+            dst->position += fTextProps.position;
+        }
+
+        // Colors are overridden.
+        if (fHasFillColor) {
+            dst->fill_color = fTextProps.fill_color;
+        }
+        if (fHasStrokeColor) {
+            dst->stroke_color = fTextProps.stroke_color;
+        }
+    }
+
+private:
+    TextAnimator(const skjson::ObjectValue& jprops,
+                 const AnimationBuilder* abuilder,
+                 AnimatorScope* ascope) {
+        // It's *probably* OK to capture a raw pointer to this animator, because the lambda
+        // life time is limited to |ascope|, which is also the container for the TextAnimatorList
+        // owning us. But for peace of mind (and future-proofing) let's grab a ref.
+        auto animator = sk_ref_sp(this);
+
+        fHasPosition    = abuilder->bindProperty<VectorValue>(jprops["p"], ascope,
+            [animator](const VectorValue& p) {
+                animator->fTextProps.position = ValueTraits<VectorValue>::As<SkPoint>(p);
+            });
+        fHasFillColor   = abuilder->bindProperty<VectorValue>(jprops["fc"], ascope,
+            [animator](const VectorValue& fc) {
+                animator->fTextProps.fill_color = ValueTraits<VectorValue>::As<SkColor>(fc);
+            });
+        fHasStrokeColor = abuilder->bindProperty<VectorValue>(jprops["sc"], ascope,
+            [animator](const VectorValue& sc) {
+                animator->fTextProps.stroke_color = ValueTraits<VectorValue>::As<SkColor>(sc);
+            });
+    }
+
+    TextAdapter::AnimatedProps fTextProps;
+    bool                       fHasPosition    : 1,
+                               fHasFillColor   : 1,
+                               fHasStrokeColor : 1;
+};
+
+std::unique_ptr<TextAnimatorList> TextAnimatorList::Make(const skjson::ArrayValue& janimators,
+                                                         const AnimationBuilder* abuilder,
+                                                         sk_sp<TextAdapter> adapter) {
+    AnimatorScope local_animator_scope;
+    std::vector<sk_sp<TextAnimator>> animators;
+    animators.reserve(janimators.size());
+
+    for (const skjson::ObjectValue* janimator : janimators) {
+        if (auto animator = TextAnimator::Make(janimator, abuilder, &local_animator_scope)) {
+            animators.push_back(std::move(animator));
+        }
+    }
+
+    if (animators.empty()) {
+        return nullptr;
+    }
+
+    return std::unique_ptr<TextAnimatorList>(new TextAnimatorList(std::move(adapter),
+                                                                  std::move(local_animator_scope),
+                                                                  std::move(animators)));
+}
+
+TextAnimatorList::TextAnimatorList(sk_sp<TextAdapter> adapter,
+                                   sksg::AnimatorList&& alist,
+                                   std::vector<sk_sp<TextAnimator>>&& tanimators)
+    : INHERITED(std::move(alist))
+    , fAnimators(std::move(tanimators))
+    , fAdapter(std::move(adapter)) {}
+
+TextAnimatorList::~TextAnimatorList() = default;
+
+void TextAnimatorList::onTick(float t) {
+    // First, update all locally-scoped animated props.
+    this->INHERITED::onTick(t);
+
+    // Then push the final property values to the text adapter.
+    this->applyAnimators();
+}
+
+void TextAnimatorList::applyAnimators() const {
+    const auto& txt_val = fAdapter->getText();
+
+    // Seed props from the current text value.
+    TextAdapter::AnimatedProps modulated_props;
+    modulated_props.fill_color   = txt_val.fFillColor;
+    modulated_props.stroke_color = txt_val.fStrokeColor;
+
+    for (const auto& animator : fAnimators) {
+        animator->modulateProps(&modulated_props);
+    }
+
+    fAdapter->applyAnimatedProps(modulated_props);
+}
+
+} // namespace internal
+} // namespace skottie
diff --git a/modules/skottie/src/text/TextAnimator.h b/modules/skottie/src/text/TextAnimator.h
new file mode 100644
index 0000000..04a8f21
--- /dev/null
+++ b/modules/skottie/src/text/TextAnimator.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkottieTextAnimator_DEFINED
+#define SkottieTextAnimator_DEFINED
+
+#include "modules/skottie/src/SkottieAdapter.h"
+#include "modules/sksg/include/SkSGScene.h"
+
+#include <memory>
+#include <vector>
+
+namespace skjson {
+class ArrayValue;
+}
+
+namespace skottie {
+
+class TextAdapter;
+
+namespace internal {
+
+class AnimationBuilder;
+class TextAnimator;
+
+class TextAnimatorList final : public sksg::GroupAnimator {
+public:
+    static std::unique_ptr<TextAnimatorList> Make(const skjson::ArrayValue&,
+                                                  const AnimationBuilder*,
+                                                  sk_sp<TextAdapter>);
+    ~TextAnimatorList() override;
+
+protected:
+    void onTick(float) override;
+
+private:
+    TextAnimatorList(sk_sp<TextAdapter>, sksg::AnimatorList&&, std::vector<sk_sp<TextAnimator>>&&);
+
+    void applyAnimators() const;
+
+    const std::vector<sk_sp<TextAnimator>> fAnimators;
+    const sk_sp<TextAdapter>               fAdapter;
+
+    using INHERITED = sksg::GroupAnimator;
+};
+
+} // namespace internal
+} // namespace skottie
+
+#endif // SkottieTextAnimator_DEFINED
diff --git a/modules/skottie/src/text/TextLayer.cpp b/modules/skottie/src/text/TextLayer.cpp
index 4b90375..0403870 100644
--- a/modules/skottie/src/text/TextLayer.cpp
+++ b/modules/skottie/src/text/TextLayer.cpp
@@ -12,6 +12,7 @@
 #include "include/core/SkTypes.h"
 #include "modules/skottie/src/SkottieJson.h"
 #include "modules/skottie/src/text/TextAdapter.h"
+#include "modules/skottie/src/text/TextAnimator.h"
 #include "modules/skottie/src/text/TextValue.h"
 #include "modules/sksg/include/SkSGDraw.h"
 #include "modules/sksg/include/SkSGGroup.h"
@@ -257,7 +258,7 @@
                                                           AnimatorScope* ascope) const {
     // General text node format:
     // "t": {
-    //    "a": [], // animators (TODO)
+    //    "a": [], // animators (see TextAnimator.cpp)
     //    "d": {
     //        "k": [
     //            {
@@ -290,7 +291,6 @@
 
     const skjson::ArrayValue* animated_props = (*jt)["a"];
     const auto has_animators = (animated_props && animated_props->size() > 0);
-    // TODO: actually parse/implement animators.
 
     const skjson::ObjectValue* jd  = (*jt)["d"];
     if (!jd) {
@@ -304,6 +304,12 @@
         adapter->setText(txt);
     });
 
+    if (has_animators) {
+        if (auto alist = TextAnimatorList::Make(*animated_props, this, adapter)) {
+            ascope->push_back(std::move(alist));
+        }
+    }
+
     return std::move(text_root);
 }