[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);