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