[skottie] Add support for keyframed text nodes

 -- introduce a new animatable value (TextValue)

 -- introduce a new adapter (TextAdapter) to translate Lottie text props to SG text props

 -- use existing animated property-bind machinery and the new constructs when parsing text layers

Change-Id: Ibbfb69daf5b0a3c9a5ce8d1ccdeedca5b5d0fa6f
Reviewed-on: https://skia-review.googlesource.com/149266
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/src/SkottieAdapter.cpp b/modules/skottie/src/SkottieAdapter.cpp
index 1c76e19..3c0fb54 100644
--- a/modules/skottie/src/SkottieAdapter.cpp
+++ b/modules/skottie/src/SkottieAdapter.cpp
@@ -10,9 +10,13 @@
 #include "SkMatrix.h"
 #include "SkPath.h"
 #include "SkRRect.h"
+#include "SkSGColor.h"
+#include "SkSGDraw.h"
 #include "SkSGGradient.h"
+#include "SkSGGroup.h"
 #include "SkSGPath.h"
 #include "SkSGRect.h"
+#include "SkSGText.h"
 #include "SkSGTransform.h"
 #include "SkSGTrimEffect.h"
 #include "SkTo.h"
@@ -169,4 +173,75 @@
     fTrimEffect->setMode(mode);
 }
 
+TextAdapter::TextAdapter(sk_sp<sksg::Group> root)
+    : fRoot(std::move(root))
+    , fTextNode(sksg::Text::Make(nullptr, SkString()))
+    , fFillColor(sksg::Color::Make(SK_ColorTRANSPARENT))
+    , fStrokeColor(sksg::Color::Make(SK_ColorTRANSPARENT))
+    , fFillNode(sksg::Draw::Make(fTextNode, fFillColor))
+    , fStrokeNode(sksg::Draw::Make(fTextNode, fStrokeColor))
+    , fHadFill(false)
+    , fHadStroke(false) {
+    // Build a SG fragment with the following general format:
+    //
+    // [Group]
+    //   [Draw]
+    //     [FillPaint]
+    //     [Text]*
+    //   [Draw]
+    //     [StrokePaint]
+    //     [Text]*
+    //
+    // * where the text node is shared
+
+    fTextNode->setFlags(fTextNode->getFlags() |
+                        SkPaint::kAntiAlias_Flag |
+                        SkPaint::kSubpixelText_Flag);
+    fTextNode->setHinting(SkPaint::kNo_Hinting);
+
+    fStrokeColor->setStyle(SkPaint::kStroke_Style);
+}
+
+void TextAdapter::apply() {
+    // Push text props to the scene graph.
+    fTextNode->setTypeface(fText.fTypeface);
+    fTextNode->setText(fText.fText);
+    fTextNode->setSize(fText.fTextSize);
+    fTextNode->setAlign(fText.fAlign);
+
+    fFillColor->setColor(fText.fFillColor);
+    fStrokeColor->setColor(fText.fStrokeColor);
+    fStrokeColor->setStrokeWidth(fText.fStrokeWidth);
+
+    // Turn the state transition into a tri-state value:
+    //   -1: detach node
+    //    0: no change
+    //    1: attach node
+    const auto   fill_change = SkToInt(fText.fHasFill) - SkToInt(fHadFill);
+    const auto stroke_change = SkToInt(fText.fHasStroke) - SkToInt(fHadStroke);
+
+    // Sync SG topology.
+    if (fill_change || stroke_change) {
+        // This is trickier than it should be because sksg::Group only allows adding children
+        // in paint-order.
+        if (stroke_change < 0 || (fHadStroke && fill_change > 0)) {
+            fRoot->removeChild(fStrokeNode);
+        }
+
+        if (fill_change < 0) {
+            fRoot->removeChild(fFillNode);
+        } else if (fill_change > 0) {
+            fRoot->addChild(fFillNode);
+        }
+
+        if (stroke_change > 0 || (fHadStroke && fill_change > 0)) {
+            fRoot->addChild(fStrokeNode);
+        }
+    }
+
+    // Track current state.
+    fHadFill   = fText.fHasFill;
+    fHadStroke = fText.fHasStroke;
+}
+
 } // namespace skottie
diff --git a/modules/skottie/src/SkottieAdapter.h b/modules/skottie/src/SkottieAdapter.h
index 76b3f46..37dd077 100644
--- a/modules/skottie/src/SkottieAdapter.h
+++ b/modules/skottie/src/SkottieAdapter.h
@@ -15,12 +15,16 @@
 
 namespace sksg {
 
+class Color;
+class Draw;
 class Gradient;
+class Group;
 class LinearGradient;
 class Matrix;
 class Path;
 class RadialGradient;
 class RRect;
+class Text;
 class TrimEffect;
 
 };
@@ -156,6 +160,30 @@
     using INHERITED = SkRefCnt;
 };
 
+class TextAdapter final : public SkRefCnt {
+public:
+    explicit TextAdapter(sk_sp<sksg::Group> root);
+
+    ADAPTER_PROPERTY(Text, TextValue, TextValue())
+
+    const sk_sp<sksg::Group>& root() const { return fRoot; }
+
+private:
+    void apply();
+
+    sk_sp<sksg::Group> fRoot;
+    sk_sp<sksg::Text>  fTextNode;
+    sk_sp<sksg::Color> fFillColor,
+                       fStrokeColor;
+    sk_sp<sksg::Draw>  fFillNode,
+                       fStrokeNode;
+
+    bool               fHadFill   : 1, //  - state cached from the prev apply()
+                       fHadStroke : 1; //  /
+
+    using INHERITED = SkRefCnt;
+};
+
 #undef ADAPTER_PROPERTY
 
 } // namespace skottie
diff --git a/modules/skottie/src/SkottieAnimator.cpp b/modules/skottie/src/SkottieAnimator.cpp
index 3433d1e..c5cdf09 100644
--- a/modules/skottie/src/SkottieAnimator.cpp
+++ b/modules/skottie/src/SkottieAnimator.cpp
@@ -389,5 +389,13 @@
     return BindPropertyImpl(jv, this, ascope, std::move(apply), noop);
 }
 
+template <>
+bool AnimationBuilder::bindProperty(const skjson::Value& jv,
+                  AnimatorScope* ascope,
+                  std::function<void(const TextValue&)>&& apply,
+                  const TextValue* noop) const {
+    return BindPropertyImpl(jv, this, ascope, std::move(apply), noop);
+}
+
 } // namespace internal
 } // namespace skottie
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index 3551029..90af97f 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -49,6 +49,8 @@
 
     std::unique_ptr<sksg::Scene> parse(const skjson::ObjectValue&);
 
+    sk_sp<SkTypeface> findFont(const SkString& name) const;
+
     // 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>
diff --git a/modules/skottie/src/SkottieTextLayer.cpp b/modules/skottie/src/SkottieTextLayer.cpp
index cb62a5b..1f22657 100644
--- a/modules/skottie/src/SkottieTextLayer.cpp
+++ b/modules/skottie/src/SkottieTextLayer.cpp
@@ -9,6 +9,7 @@
 
 #include "SkFontMgr.h"
 #include "SkMakeUnique.h"
+#include "SkottieAdapter.h"
 #include "SkottieJson.h"
 #include "SkottieValue.h"
 #include "SkSGColor.h"
@@ -205,6 +206,15 @@
     }
 }
 
+sk_sp<SkTypeface> AnimationBuilder::findFont(const SkString& font_name) const {
+    if (const auto* font = fFonts.find(font_name)) {
+        return font->fTypeface;
+    }
+
+    LOG("!! Unknown font: \"%s\"\n", font_name.c_str());
+    return nullptr;
+}
+
 sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& layer,
                                                           AnimatorScope* ascope) const {
     // General text node format:
@@ -245,93 +255,19 @@
         LOG("?? Unsupported animated text properties.\n");
     }
 
-    // TODO: The "d" node is keyframed, not static. Add a new animated value type and parse as such.
     const skjson::ObjectValue* jd  = (*jt)["d"];
-    const skjson::ArrayValue*  jk  = jd
-            ? (*jd)["k"].operator const skjson::ArrayValue*() : nullptr;
-    const skjson::ObjectValue* jv0 = jk && jk->size() == 1
-            ? (*jk)[0].operator const skjson::ObjectValue*() : nullptr;
-    const skjson::ObjectValue* jprops = jv0
-            ? (*jv0)["s"].operator const skjson::ObjectValue*() : nullptr;
-
-    if (!jprops) {
-        LogJSON(*jt, "!! Unexpected text property");
+    if (!jd) {
         return nullptr;
     }
 
-    const skjson::StringValue* font_name = (*jprops)["f"];
-    const skjson::StringValue* text      = (*jprops)["t"];
-    const skjson::NumberValue* text_size = (*jprops)["s"];
-    if (!font_name || !text || !text_size) {
-        LogJSON(*jprops, "!! Invalid text properties");
-        return nullptr;
-    }
+    auto text_root = sksg::Group::Make();
+    auto adapter   = sk_make_sp<TextAdapter>(text_root);
 
-    const auto* font = fFonts.find(SkString(font_name->begin(), font_name->size()));
-    if (!font) {
-        LOG("!! Unknown font: \"%s\"\n", font_name->begin());
-        return nullptr;
-    }
+    this->bindProperty<TextValue>(*jd, ascope, [adapter] (const TextValue& txt) {
+        adapter->setText(txt);
+    });
 
-    static constexpr SkPaint::Align gAlignMap[] = {
-        SkPaint::kLeft_Align,  // 'j': 0
-        SkPaint::kRight_Align, // 'j': 1
-        SkPaint::kCenter_Align // 'j': 2
-    };
-    const auto align = gAlignMap[SkTMin<size_t>(ParseDefault<size_t>((*jprops)["j"], 0),
-                                                SK_ARRAY_COUNT(gAlignMap))];
-
-    // Emit a SG fragment with the following general format:
-    //
-    // [Group]
-    //   [Draw]
-    //     [FillPaint]
-    //     [Text]
-    //   [Draw]
-    //     [StrokePaint]
-    //     [Text]
-    //
-    auto text_node = sksg::Text::Make(font->fTypeface, SkString(text->begin(), text->size()));
-    text_node->setFlags(text_node->getFlags() |
-                        SkPaint::kAntiAlias_Flag |
-                        SkPaint::kSubpixelText_Flag);
-    text_node->setSize(**text_size);
-    text_node->setAlign(align);
-    text_node->setHinting(SkPaint::kNo_Hinting);
-
-    const auto parse_color = [](const skjson::ArrayValue* jcolor) -> sk_sp<sksg::Color> {
-        VectorValue color_vec;
-        if (!jcolor || !Parse(*jcolor, &color_vec)) {
-            return nullptr;
-        }
-
-        auto paint = sksg::Color::Make(ValueTraits<VectorValue>::As<SkColor>(color_vec));
-
-        return paint;
-    };
-
-    auto fill_paint = parse_color((*jprops)["fc"]),
-       stroke_paint = parse_color((*jprops)["sc"]);
-    auto  fill_node = sksg::Draw::Make(text_node, fill_paint),
-        stroke_node = sksg::Draw::Make(text_node, stroke_paint);
-
-    if (!stroke_node) {
-        return std::move(fill_node);
-    }
-
-    stroke_paint->setStyle(SkPaint::kStroke_Style);
-    stroke_paint->setStrokeWidth(ParseDefault((*jprops)["sw"], 0.0f));
-
-    if (!fill_node) {
-        return std::move(stroke_node);
-    }
-
-    // Fill & stroke
-    auto group_node = sksg::Group::Make();
-    group_node->addChild(std::move(fill_node));
-    group_node->addChild(std::move(stroke_node));
-
-    return std::move(group_node);
+    return std::move(text_root);
 }
 
 } // namespace internal
diff --git a/modules/skottie/src/SkottieValue.cpp b/modules/skottie/src/SkottieValue.cpp
index 910e8c3..da683db 100644
--- a/modules/skottie/src/SkottieValue.cpp
+++ b/modules/skottie/src/SkottieValue.cpp
@@ -234,4 +234,69 @@
     return path;
 }
 
+template <>
+bool ValueTraits<TextValue>::FromJSON(const skjson::Value& jv,
+                                       const internal::AnimationBuilder* abuilder,
+                                       TextValue* v) {
+    const skjson::ObjectValue* jtxt = jv;
+    if (!jtxt) {
+        return false;
+    }
+
+    const skjson::StringValue* font_name = (*jtxt)["f"];
+    const skjson::StringValue* text      = (*jtxt)["t"];
+    const skjson::NumberValue* text_size = (*jtxt)["s"];
+    if (!font_name || !text || !text_size ||
+        !(v->fTypeface = abuilder->findFont(SkString(font_name->begin(), font_name->size())))) {
+        return false;
+    }
+    v->fText.set(text->begin(), text->size());
+    v->fTextSize = **text_size;
+
+    static constexpr SkPaint::Align gAlignMap[] = {
+        SkPaint::kLeft_Align,  // 'j': 0
+        SkPaint::kRight_Align, // 'j': 1
+        SkPaint::kCenter_Align // 'j': 2
+    };
+    v->fAlign = gAlignMap[SkTMin<size_t>(ParseDefault<size_t>((*jtxt)["j"], 0),
+                                         SK_ARRAY_COUNT(gAlignMap))];
+
+    const auto& parse_color = [] (const skjson::ArrayValue* jcolor,
+                                  const internal::AnimationBuilder* abuilder,
+                                  SkColor* c) {
+        if (!jcolor) {
+            return false;
+        }
+
+        VectorValue color_vec;
+        if (!ValueTraits<VectorValue>::FromJSON(*jcolor, abuilder, &color_vec)) {
+            return false;
+        }
+
+        *c = ValueTraits<VectorValue>::As<SkColor>(color_vec);
+        return true;
+    };
+
+    v->fHasFill   = parse_color((*jtxt)["fc"], abuilder, &v->fFillColor);
+    v->fHasStroke = parse_color((*jtxt)["sc"], abuilder, &v->fStrokeColor);
+
+    if (v->fHasStroke) {
+        v->fStrokeWidth = ParseDefault((*jtxt)["s"], 0.0f);
+    }
+
+    return true;
+}
+
+template <>
+bool ValueTraits<TextValue>::CanLerp(const TextValue&, const TextValue&) {
+    // Text values are never interpolated, but we pretend that they could be.
+    return true;
+}
+
+template <>
+void ValueTraits<TextValue>::Lerp(const TextValue& v0, const TextValue&, float, TextValue* result) {
+    // Text value keyframes are treated as selectors, not as interpolated values.
+    *result = v0;
+}
+
 } // namespace skottie
diff --git a/modules/skottie/src/SkottieValue.h b/modules/skottie/src/SkottieValue.h
index 8e62985..cb9dac8 100644
--- a/modules/skottie/src/SkottieValue.h
+++ b/modules/skottie/src/SkottieValue.h
@@ -8,8 +8,12 @@
 #ifndef SkottieValue_DEFINED
 #define SkottieValue_DEFINED
 
+#include "SkColor.h"
+#include "SkPaint.h"
 #include "SkPath.h"
 #include "SkScalar.h"
+#include "SkString.h"
+#include "SkTypeface.h"
 
 #include <vector>
 
@@ -65,6 +69,32 @@
     bool operator!=(const ShapeValue& other) const { return !(*this == other); }
 };
 
+struct TextValue {
+    sk_sp<SkTypeface> fTypeface;
+    SkString          fText;
+    float             fTextSize    = 0,
+                      fStrokeWidth = 0;
+    SkPaint::Align    fAlign       = SkPaint::kLeft_Align;
+    SkColor           fFillColor   = SK_ColorTRANSPARENT,
+                      fStrokeColor = SK_ColorTRANSPARENT;
+    bool              fHasFill   : 1,
+                      fHasStroke : 1;
+
+    bool operator==(const TextValue& other) const {
+        return fTypeface == other.fTypeface
+            && fText == other.fText
+            && fTextSize == other.fTextSize
+            && fStrokeWidth == other.fStrokeWidth
+            && fAlign == other.fAlign
+            && fFillColor == other.fFillColor
+            && fStrokeColor == other.fStrokeColor
+            && fHasFill == other.fHasFill
+            && fHasStroke == other.fHasStroke;
+    }
+
+    bool operator!=(const TextValue& other) const { return !(*this == other); }
+};
+
 } // namespace skottie
 
 #endif // SkottieValue_DEFINED
diff --git a/modules/sksg/include/SkSGText.h b/modules/sksg/include/SkSGText.h
index 8e1d79e..c1f3ee1 100644
--- a/modules/sksg/include/SkSGText.h
+++ b/modules/sksg/include/SkSGText.h
@@ -29,14 +29,15 @@
     static sk_sp<Text> Make(sk_sp<SkTypeface> tf, const SkString& text);
     ~Text() override;
 
-    SG_ATTRIBUTE(Text    , SkString        , fText    )
-    SG_ATTRIBUTE(Flags   , uint32_t        , fFlags   )
-    SG_ATTRIBUTE(Position, SkPoint         , fPosition)
-    SG_ATTRIBUTE(Size    , SkScalar        , fSize    )
-    SG_ATTRIBUTE(ScaleX  , SkScalar        , fScaleX  )
-    SG_ATTRIBUTE(SkewX   , SkScalar        , fSkewX   )
-    SG_ATTRIBUTE(Align   , SkPaint::Align  , fAlign   )
-    SG_ATTRIBUTE(Hinting , SkPaint::Hinting, fHinting )
+    SG_ATTRIBUTE(Typeface, sk_sp<SkTypeface>, fTypeface)
+    SG_ATTRIBUTE(Text    , SkString         , fText    )
+    SG_ATTRIBUTE(Flags   , uint32_t         , fFlags   )
+    SG_ATTRIBUTE(Position, SkPoint          , fPosition)
+    SG_ATTRIBUTE(Size    , SkScalar         , fSize    )
+    SG_ATTRIBUTE(ScaleX  , SkScalar         , fScaleX  )
+    SG_ATTRIBUTE(SkewX   , SkScalar         , fSkewX   )
+    SG_ATTRIBUTE(Align   , SkPaint::Align   , fAlign   )
+    SG_ATTRIBUTE(Hinting , SkPaint::Hinting , fHinting )
 
     // TODO: add shaping functionality.
 
@@ -52,7 +53,7 @@
 
     SkPoint alignedPosition(SkScalar advance) const;
 
-    const sk_sp<SkTypeface> fTypeface;
+    sk_sp<SkTypeface> fTypeface;
     SkString                fText;
     uint32_t                fFlags    = SkPaintDefaults_Flags;
     SkPoint                 fPosition = SkPoint::Make(0, 0);