[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>
This commit is contained in:
parent
adb4bfef0c
commit
9402c7dc37
@ -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 @@ void TrimEffectAdapter::apply() {
|
||||
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
|
||||
|
@ -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 @@ private:
|
||||
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
|
||||
|
@ -389,5 +389,13 @@ bool AnimationBuilder::bindProperty(const skjson::Value& jv,
|
||||
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
|
||||
|
@ -49,6 +49,8 @@ public:
|
||||
|
||||
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>
|
||||
|
@ -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 @@ void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts,
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectVa
|
||||
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
|
||||
|
@ -234,4 +234,69 @@ SkPath ValueTraits<ShapeValue>::As<SkPath>(const ShapeValue& shape) {
|
||||
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
|
||||
|
@ -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 @@ struct ShapeValue {
|
||||
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
|
||||
|
@ -29,14 +29,15 @@ public:
|
||||
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 @@ private:
|
||||
|
||||
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);
|
||||
|
Loading…
Reference in New Issue
Block a user