[skottie] Refactor property animators

Currently, property animators use lambda captures (std::function<>) to push
values to adapters and then to the scene graph.  Some downsides:

  * complex lambda captures are expensive in terms of object code size
  * adapters with multiple animated properties don't synchronize/quiesce: each individual property tick triggers a SG
    synchronization, possibly with inconsistent state (as animator running
    order is unspecified)
  * there is no enforced scoping, resulting in fragile constructs when SG
    fragments are discarded

This CL introduces a simplified and more robust animator pattern:

  * property animators are scoped to explicit containers
  * instead of capturing arbitrary value functors, animators only capture
    a pointer to the target value

Some implementation details:

  * keyframe/interpolation logic is pretty much unchanged (just relocated)
  * introduced AnimatablePropertyContainer - a base class for animatable
    adapters
  * legacy binding functions are refactored based on the new mechanism
    (they now/transitionally inject adapter objects)
  * converted a handful of effects, to exercise trivial refactoring patterns
  * converted the text animator goo, to exercise non-trivial refactoring:
    - detecting value changes is now trickier (no more lambda magic)
    - value adjustments must be hoisted into adapter logic (no more lambda magic)
    - all dependent animated values (selectors, etc) must be scoped to the
      text adapter to avoid lifetime issues

TBR=
Change-Id: Ia5821982f251de0de58fd3f87812219ff7fcc726
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/263938
Commit-Queue: Florin Malita <fmalita@chromium.org>
Reviewed-by: Mike Reed <reed@google.com>
This commit is contained in:
Florin Malita 2020-01-16 16:46:04 -05:00 committed by Skia Commit-Bot
parent a8fa49016c
commit d5c42c8c03
18 changed files with 698 additions and 699 deletions

View File

@ -13,6 +13,8 @@ skia_skottie_public = [
]
skia_skottie_sources = [
"$_src/Animator.cpp",
"$_src/Animator.h",
"$_src/Camera.cpp",
"$_src/Camera.h",
"$_src/Composition.cpp",

View File

@ -0,0 +1,429 @@
/*
* Copyright 2020 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/Animator.h"
#include "include/core/SkCubicMap.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/skottie/src/text/TextValue.h"
#include <vector>
namespace skottie {
namespace internal {
void AnimatablePropertyContainer::onTick(float t) {
for (const auto& animator : fAnimators) {
animator->tick(t);
}
this->onSync();
}
namespace {
class KeyframeAnimatorBase : public sksg::Animator {
protected:
KeyframeAnimatorBase() = default;
struct KeyframeRec {
float t0, t1;
int vidx0, vidx1, // v0/v1 indices
cmidx; // cubic map index
bool contains(float t) const { return t0 <= t && t <= t1; }
bool isConstant() const { return vidx0 == vidx1; }
bool isValid() const {
SkASSERT(t0 <= t1);
// Constant frames don't need/use t1 and vidx1.
return t0 < t1 || this->isConstant();
}
};
const KeyframeRec& frame(float t) {
if (!fCachedRec || !fCachedRec->contains(t)) {
fCachedRec = findFrame(t);
}
return *fCachedRec;
}
bool isEmpty() const { return fRecs.empty(); }
float localT(const KeyframeRec& rec, float t) const {
SkASSERT(rec.isValid());
SkASSERT(!rec.isConstant());
SkASSERT(t > rec.t0 && t < rec.t1);
auto lt = (t - rec.t0) / (rec.t1 - rec.t0);
return rec.cmidx < 0
? lt
: fCubicMaps[SkToSizeT(rec.cmidx)].computeYFromX(lt);
}
virtual int parseValue(const AnimationBuilder&, const skjson::Value&) = 0;
void parseKeyFrames(const AnimationBuilder& abuilder, const skjson::ArrayValue& jframes) {
// Logically, a keyframe is defined as a (t0, t1, v0, v1) tuple: a given value
// is interpolated in the [v0..v1] interval over the [t0..t1] time span.
//
// There are three interestingly-different keyframe formats handled here.
//
// 1) Legacy keyframe format
//
// - normal keyframes specify t0 ("t"), v0 ("s") and v1 ("e")
// - last frame only specifies a t0
// - t1[frame] == t0[frame + 1]
// - the last entry (where we cannot determine t1) is ignored
//
// 2) Regular (new) keyframe format
//
// - all keyframes specify t0 ("t") and v0 ("s")
// - t1[frame] == t0[frame + 1]
// - v1[frame] == v0[frame + 1]
// - the last entry (where we cannot determine t1/v1) is ignored
//
// 3) Text value keyframe format
//
// - similar to case #2, all keyframes specify t0 & v0
// - unlike case #2, all keyframes are assumed to be constant (v1 == v0),
// and the last frame is not discarded (its t1 is assumed -> inf)
//
SkPoint prev_c0 = { 0, 0 },
prev_c1 = prev_c0;
fRecs.reserve(SkTMax<size_t>(jframes.size(), 1) - 1);
for (const skjson::ObjectValue* jframe : jframes) {
if (!jframe) continue;
float t0;
if (!Parse<float>((*jframe)["t"], &t0))
continue;
const auto v0_idx = this->parseValue(abuilder, (*jframe)["s"]),
v1_idx = this->parseValue(abuilder, (*jframe)["e"]);
if (!fRecs.empty()) {
if (fRecs.back().t1 >= t0) {
abuilder.log(Logger::Level::kWarning, nullptr,
"Ignoring out-of-order key frame (t:%f < t:%f).",
t0, fRecs.back().t1);
continue;
}
// Back-fill t1 and v1 (if needed).
auto& prev = fRecs.back();
prev.t1 = t0;
// Previous keyframe did not specify an end value (case #2, #3).
if (prev.vidx1 < 0) {
// If this frame has no v0, we're in case #3 (constant text value),
// otherwise case #2 (v0 for current frame is the same as prev frame v1).
prev.vidx1 = v0_idx < 0 ? prev.vidx0 : v0_idx;
}
}
// Start value 's' is required.
if (v0_idx < 0)
continue;
if ((v1_idx < 0) && ParseDefault((*jframe)["h"], false)) {
// Constant keyframe ("h": true).
fRecs.push_back({t0, t0, v0_idx, v0_idx, -1 });
continue;
}
const auto cubic_mapper_index = [&]() -> int {
// Do we have non-linear control points?
SkPoint c0, c1;
if (!Parse((*jframe)["o"], &c0) ||
!Parse((*jframe)["i"], &c1) ||
SkCubicMap::IsLinear(c0, c1)) {
// No need for a cubic mapper.
return -1;
}
// De-dupe sequential cubic mappers.
if (c0 != prev_c0 || c1 != prev_c1) {
fCubicMaps.emplace_back(c0, c1);
prev_c0 = c0;
prev_c1 = c1;
}
SkASSERT(!fCubicMaps.empty());
return SkToInt(fCubicMaps.size()) - 1;
};
fRecs.push_back({t0, t0, v0_idx, v1_idx, cubic_mapper_index()});
}
if (!fRecs.empty()) {
auto& last = fRecs.back();
// If the last entry has only a v0, we're in case #3 - make it a constant frame.
if (last.vidx0 >= 0 && last.vidx1 < 0) {
last.vidx1 = last.vidx0;
last.t1 = last.t0;
}
// If we couldn't determine a valid t1 for the last frame, discard it
// (most likely the last frame entry for all 3 cases).
if (!last.isValid()) {
fRecs.pop_back();
}
}
fRecs.shrink_to_fit();
fCubicMaps.shrink_to_fit();
SkASSERT(fRecs.empty() || fRecs.back().isValid());
}
private:
const KeyframeRec* findFrame(float t) const {
SkASSERT(!fRecs.empty());
auto f0 = &fRecs.front(),
f1 = &fRecs.back();
SkASSERT(f0->isValid());
SkASSERT(f1->isValid());
if (t < f0->t0) {
return f0;
}
if (t > f1->t1) {
return f1;
}
while (f0 != f1) {
SkASSERT(f0 < f1);
SkASSERT(t >= f0->t0 && t <= f1->t1);
const auto f = f0 + (f1 - f0) / 2;
SkASSERT(f->isValid());
if (t > f->t1) {
f0 = f + 1;
} else {
f1 = f;
}
}
SkASSERT(f0 == f1);
SkASSERT(f0->contains(t));
return f0;
}
std::vector<KeyframeRec> fRecs;
std::vector<SkCubicMap> fCubicMaps;
const KeyframeRec* fCachedRec = nullptr;
};
template <typename T>
class KeyframeAnimator final : public KeyframeAnimatorBase {
public:
static sk_sp<KeyframeAnimator> Make(const AnimationBuilder& abuilder,
const skjson::ArrayValue* jv,
T* target_value) {
if (!jv) return nullptr;
sk_sp<KeyframeAnimator> animator(new KeyframeAnimator(abuilder, *jv, target_value));
return animator->isEmpty() ? nullptr : animator;
}
bool isConstant() const {
SkASSERT(!fValues.empty());
return fValues.size() == 1ul;
}
private:
KeyframeAnimator(const AnimationBuilder& abuilder,
const skjson::ArrayValue& jframes,
T* target_value)
: fTarget(target_value) {
// Generally, each keyframe holds two values (start, end) and a cubic mapper. Except
// the last frame, which only holds a marker timestamp. Then, the values series is
// contiguous (keyframe[i].end == keyframe[i + 1].start), and we dedupe them.
// => we'll store (keyframes.size) values and (keyframe.size - 1) recs and cubic maps.
fValues.reserve(jframes.size());
this->parseKeyFrames(abuilder, jframes);
fValues.shrink_to_fit();
}
int parseValue(const AnimationBuilder& abuilder, const skjson::Value& jv) override {
T val;
if (!ValueTraits<T>::FromJSON(jv, &abuilder, &val) ||
(!fValues.empty() && !ValueTraits<T>::CanLerp(val, fValues.back()))) {
return -1;
}
// TODO: full deduping?
if (fValues.empty() || val != fValues.back()) {
fValues.push_back(std::move(val));
}
return SkToInt(fValues.size()) - 1;
}
void onTick(float t) override {
const auto& rec = this->frame(t);
SkASSERT(rec.isValid());
if (rec.isConstant() || t <= rec.t0) {
*fTarget = fValues[SkToSizeT(rec.vidx0)];
return;
} else if (t >= rec.t1) {
*fTarget = fValues[SkToSizeT(rec.vidx1)];
return;
}
ValueTraits<T>::Lerp(fValues[SkToSizeT(rec.vidx0)],
fValues[SkToSizeT(rec.vidx1)],
this->localT(rec, t),
fTarget);
}
std::vector<T> fValues;
T* fTarget;
};
template <typename T>
bool BindPropertyImpl(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
AnimatorScope* ascope,
T* target_value) {
if (!jprop) {
return false;
}
const auto& jpropA = (*jprop)["a"];
const auto& jpropK = (*jprop)["k"];
if (!(*jprop)["x"].is<skjson::NullValue>()) {
abuilder.log(Logger::Level::kWarning, nullptr, "Unsupported expression.");
}
// Older Json versions don't have an "a" animation marker.
// For those, we attempt to parse both ways.
if (!ParseDefault<bool>(jpropA, false)) {
if (ValueTraits<T>::FromJSON(jpropK, &abuilder, target_value)) {
// Static property.
return true;
}
if (!jpropA.is<skjson::NullValue>()) {
abuilder.log(Logger::Level::kError, jprop,
"Could not parse (explicit) static property.");
return false;
}
}
// Keyframe property.
auto animator = KeyframeAnimator<T>::Make(abuilder, jpropK, target_value);
if (!animator) {
abuilder.log(Logger::Level::kError, jprop, "Could not parse keyframed property.");
return false;
}
if (animator->isConstant()) {
// If all keyframes are constant, there is no reason to treat this
// as an animated property - apply immediately and discard the animator.
animator->tick(0);
} else {
ascope->push_back(std::move(animator));
}
return true;
}
} // namespace
// Explicit instantiations
template <>
bool AnimatablePropertyContainer::bind<ScalarValue>(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
ScalarValue* v) {
return BindPropertyImpl(abuilder, jprop, &fAnimators, v);
}
template <>
bool AnimatablePropertyContainer::bind<ShapeValue>(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
ShapeValue* v) {
return BindPropertyImpl(abuilder, jprop, &fAnimators, v);
}
template <>
bool AnimatablePropertyContainer::bind<TextValue>(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
TextValue* v) {
return BindPropertyImpl(abuilder, jprop, &fAnimators, v);
}
template <>
bool AnimatablePropertyContainer::bind<VectorValue>(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
VectorValue* v) {
if (!jprop) {
return false;
}
if (!ParseDefault<bool>((*jprop)["s"], false)) {
// Regular (static or keyframed) vector value.
return BindPropertyImpl(abuilder, jprop, &fAnimators, v);
}
// Separate-dimensions vector value: each component is animated independently.
class SeparateDimensionsAnimator final : public AnimatablePropertyContainer {
public:
static sk_sp<SeparateDimensionsAnimator> Make(const AnimationBuilder& abuilder,
const skjson::ObjectValue& jprop,
VectorValue* v) {
sk_sp<SeparateDimensionsAnimator> animator(new SeparateDimensionsAnimator(v));
auto bound = animator->bind(abuilder, jprop["x"], &animator->fX);
bound |= animator->bind(abuilder, jprop["y"], &animator->fY);
bound |= animator->bind(abuilder, jprop["z"], &animator->fZ);
return bound ? animator : nullptr;
}
private:
explicit SeparateDimensionsAnimator(VectorValue* v)
: fTarget(v) {}
void onSync() override {
*fTarget = { fX, fY, fZ };
}
VectorValue* fTarget;
ScalarValue fX = 0,
fY = 0,
fZ = 0;
};
if (auto sd_animator = SeparateDimensionsAnimator::Make(abuilder, *jprop, v)) {
if (sd_animator->isStatic()) {
sd_animator->tick(0);
} else {
fAnimators.push_back(std::move(sd_animator));
}
return true;
}
return false;
}
} // namespace internal
} // namespace skottie

View File

@ -0,0 +1,46 @@
/*
* Copyright 2020 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#ifndef SkottieAnimator_DEFINED
#define SkottieAnimator_DEFINED
#include "modules/sksg/include/SkSGScene.h"
namespace skjson {
class ObjectValue;
} // namespace skjson
namespace skottie {
namespace internal {
class AnimationBuilder;
class AnimatablePropertyContainer : public sksg::Animator {
public:
// 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, scoped to
// this container.
template <typename T>
bool bind(const AnimationBuilder&, const skjson::ObjectValue*, T*);
bool isStatic() const { return fAnimators.empty(); }
protected:
virtual void onSync() = 0;
private:
void onTick(float) final;
sksg::AnimatorList fAnimators;
};
} // namespace internal
} // namespace skottie
#endif // SkottieAnimator_DEFINED

View File

@ -30,24 +30,6 @@
namespace skottie {
namespace internal {
DiscardableAdaptorBase::DiscardableAdaptorBase() = default;
void DiscardableAdaptorBase::setAnimators(sksg::AnimatorList&& animators) {
fAnimators = std::move(animators);
}
void DiscardableAdaptorBase::onTick(float t) {
for (auto& animator : fAnimators) {
animator->tick(t);
}
this->onSync();
}
} // namespace internal
RRectAdapter::RRectAdapter(sk_sp<sksg::RRect> wrapped_node)
: fRRectNode(std::move(wrapped_node)) {}

View File

@ -42,27 +42,6 @@ namespace skjson {
}
namespace skottie {
namespace internal {
class DiscardableAdaptorBase : public sksg::Animator {
protected:
DiscardableAdaptorBase();
void onTick(float t) final;
virtual void onSync() = 0;
private:
friend class AnimationBuilder;
void setAnimators(sksg::AnimatorList&&);
sksg::AnimatorList fAnimators;
using INHERITED = sksg::Animator;
};
} // namespace internal
#define ADAPTER_PROPERTY(p_name, p_type, p_default) \
const p_type& get##p_name() const { \

View File

@ -5,468 +5,90 @@
* found in the LICENSE file.
*/
#include "include/core/SkCubicMap.h"
#include "include/core/SkString.h"
#include "modules/skottie/src/Animator.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/skottie/src/text/TextValue.h"
#include "modules/sksg/include/SkSGScene.h"
#include <memory>
#include <vector>
namespace skottie {
namespace internal {
namespace {
class KeyframeAnimatorBase : public sksg::Animator {
public:
size_t count() const { return fRecs.size(); }
protected:
KeyframeAnimatorBase() = default;
struct KeyframeRec {
float t0, t1;
int vidx0, vidx1, // v0/v1 indices
cmidx; // cubic map index
bool contains(float t) const { return t0 <= t && t <= t1; }
bool isConstant() const { return vidx0 == vidx1; }
bool isValid() const {
SkASSERT(t0 <= t1);
// Constant frames don't need/use t1 and vidx1.
return t0 < t1 || this->isConstant();
}
};
const KeyframeRec& frame(float t) {
if (!fCachedRec || !fCachedRec->contains(t)) {
fCachedRec = findFrame(t);
}
return *fCachedRec;
}
float localT(const KeyframeRec& rec, float t) const {
SkASSERT(rec.isValid());
SkASSERT(!rec.isConstant());
SkASSERT(t > rec.t0 && t < rec.t1);
auto lt = (t - rec.t0) / (rec.t1 - rec.t0);
return rec.cmidx < 0
? lt
: fCubicMaps[rec.cmidx].computeYFromX(lt);
}
virtual int parseValue(const skjson::Value&, const AnimationBuilder* abuilder) = 0;
void parseKeyFrames(const skjson::ArrayValue& jframes, const AnimationBuilder* abuilder) {
// Logically, a keyframe is defined as a (t0, t1, v0, v1) tuple: a given value
// is interpolated in the [v0..v1] interval over the [t0..t1] time span.
//
// There are three interestingly-different keyframe formats handled here.
//
// 1) Legacy keyframe format
//
// - normal keyframes specify t0 ("t"), v0 ("s") and v1 ("e")
// - last frame only specifies a t0
// - t1[frame] == t0[frame + 1]
// - the last entry (where we cannot determine t1) is ignored
//
// 2) Regular (new) keyframe format
//
// - all keyframes specify t0 ("t") and v0 ("s")
// - t1[frame] == t0[frame + 1]
// - v1[frame] == v0[frame + 1]
// - the last entry (where we cannot determine t1/v1) is ignored
//
// 3) Text value keyframe format
//
// - similar to case #2, all keyframes specify t0 & v0
// - unlike case #2, all keyframes are assumed to be constant (v1 == v0),
// and the last frame is not discarded (its t1 is assumed -> inf)
//
SkPoint prev_c0 = { 0, 0 },
prev_c1 = prev_c0;
for (const skjson::ObjectValue* jframe : jframes) {
if (!jframe) continue;
float t0;
if (!Parse<float>((*jframe)["t"], &t0))
continue;
const auto v0_idx = this->parseValue((*jframe)["s"], abuilder),
v1_idx = this->parseValue((*jframe)["e"], abuilder);
if (!fRecs.empty()) {
if (fRecs.back().t1 >= t0) {
abuilder->log(Logger::Level::kWarning, nullptr,
"Ignoring out-of-order key frame (t:%f < t:%f).",
t0, fRecs.back().t1);
continue;
}
// Back-fill t1 and v1 (if needed).
auto& prev = fRecs.back();
prev.t1 = t0;
// Previous keyframe did not specify an end value (case #2, #3).
if (prev.vidx1 < 0) {
// If this frame has no v0, we're in case #3 (constant text value),
// otherwise case #2 (v0 for current frame is the same as prev frame v1).
prev.vidx1 = v0_idx < 0 ? prev.vidx0 : v0_idx;
}
}
// Start value 's' is required.
if (v0_idx < 0)
continue;
if ((v1_idx < 0) && ParseDefault((*jframe)["h"], false)) {
// Constant keyframe ("h": true).
fRecs.push_back({t0, t0, v0_idx, v0_idx, -1 });
continue;
}
const auto cubic_mapper_index = [&]() -> int {
// Do we have non-linear control points?
SkPoint c0, c1;
if (!Parse((*jframe)["o"], &c0) ||
!Parse((*jframe)["i"], &c1) ||
SkCubicMap::IsLinear(c0, c1)) {
// No need for a cubic mapper.
return -1;
}
// De-dupe sequential cubic mappers.
if (c0 != prev_c0 || c1 != prev_c1) {
fCubicMaps.emplace_back(c0, c1);
prev_c0 = c0;
prev_c1 = c1;
}
SkASSERT(!fCubicMaps.empty());
return SkToInt(fCubicMaps.size()) - 1;
};
fRecs.push_back({t0, t0, v0_idx, v1_idx, cubic_mapper_index()});
}
if (!fRecs.empty()) {
auto& last = fRecs.back();
// If the last entry has only a v0, we're in case #3 - make it a constant frame.
if (last.vidx0 >= 0 && last.vidx1 < 0) {
last.vidx1 = last.vidx0;
last.t1 = last.t0;
}
// If we couldn't determine a valid t1 for the last frame, discard it
// (most likely the last frame entry for all 3 cases).
if (!last.isValid()) {
fRecs.pop_back();
}
}
fRecs.shrink_to_fit();
fCubicMaps.shrink_to_fit();
SkASSERT(fRecs.empty() || fRecs.back().isValid());
}
void reserve(size_t frame_count) {
fRecs.reserve(frame_count);
fCubicMaps.reserve(frame_count);
}
private:
const KeyframeRec* findFrame(float t) const {
SkASSERT(!fRecs.empty());
auto f0 = &fRecs.front(),
f1 = &fRecs.back();
SkASSERT(f0->isValid());
SkASSERT(f1->isValid());
if (t < f0->t0) {
return f0;
}
if (t > f1->t1) {
return f1;
}
while (f0 != f1) {
SkASSERT(f0 < f1);
SkASSERT(t >= f0->t0 && t <= f1->t1);
const auto f = f0 + (f1 - f0) / 2;
SkASSERT(f->isValid());
if (t > f->t1) {
f0 = f + 1;
} else {
f1 = f;
}
}
SkASSERT(f0 == f1);
SkASSERT(f0->contains(t));
return f0;
}
std::vector<KeyframeRec> fRecs;
std::vector<SkCubicMap> fCubicMaps;
const KeyframeRec* fCachedRec = nullptr;
using INHERITED = sksg::Animator;
};
template <typename T>
class KeyframeAnimator final : public KeyframeAnimatorBase {
class LegacyAnimatorAdapter final : public AnimatablePropertyContainer {
public:
static sk_sp<KeyframeAnimator> Make(const skjson::ArrayValue* jv,
const AnimationBuilder* abuilder,
std::function<void(const T&)>&& apply) {
if (!jv) return nullptr;
sk_sp<KeyframeAnimator> animator(new KeyframeAnimator(*jv, abuilder, std::move(apply)));
return animator->count() ? animator : nullptr;
LegacyAnimatorAdapter(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
std::function<void(const T&)>&& apply)
: fApplyFunc(std::move(apply)) {
this->bind<T>(abuilder, jprop, &fValue);
}
bool isConstant() const {
SkASSERT(!fVs.empty());
return fVs.size() == 1ul;
}
protected:
void onTick(float t) override {
fApplyFunc(*this->eval(this->frame(t), t, &fScratch));
}
const T& value() const { return fValue; }
private:
KeyframeAnimator(const skjson::ArrayValue& jframes,
const AnimationBuilder* abuilder,
std::function<void(const T&)>&& apply)
: fApplyFunc(std::move(apply)) {
// Generally, each keyframe holds two values (start, end) and a cubic mapper. Except
// the last frame, which only holds a marker timestamp. Then, the values series is
// contiguous (keyframe[i].end == keyframe[i + 1].start), and we dedupe them.
// => we'll store (keyframes.size) values and (keyframe.size - 1) recs and cubic maps.
fVs.reserve(jframes.size());
this->reserve(SkTMax<size_t>(jframes.size(), 1) - 1);
this->parseKeyFrames(jframes, abuilder);
fVs.shrink_to_fit();
}
int parseValue(const skjson::Value& jv, const AnimationBuilder* abuilder) override {
T val;
if (!ValueTraits<T>::FromJSON(jv, abuilder, &val) ||
(!fVs.empty() && !ValueTraits<T>::CanLerp(val, fVs.back()))) {
return -1;
}
// TODO: full deduping?
if (fVs.empty() || val != fVs.back()) {
fVs.push_back(std::move(val));
}
return SkToInt(fVs.size()) - 1;
}
const T* eval(const KeyframeRec& rec, float t, T* v) const {
SkASSERT(rec.isValid());
if (rec.isConstant() || t <= rec.t0) {
return &fVs[rec.vidx0];
} else if (t >= rec.t1) {
return &fVs[rec.vidx1];
}
const auto lt = this->localT(rec, t);
const auto& v0 = fVs[rec.vidx0];
const auto& v1 = fVs[rec.vidx1];
ValueTraits<T>::Lerp(v0, v1, lt, v);
return v;
void onSync() override {
fApplyFunc(fValue);
}
const std::function<void(const T&)> fApplyFunc;
std::vector<T> fVs;
// LERP storage: we use this to temporarily store interpolation results.
// Alternatively, the temp result could live on the stack -- but for vector values that would
// involve dynamic allocations on each tick. This a trade-off to avoid allocator pressure
// during animation.
T fScratch; // lerp storage
using INHERITED = KeyframeAnimatorBase;
T fValue;
};
template <typename T>
static inline bool BindPropertyImpl(const skjson::ObjectValue* jprop,
const AnimationBuilder* abuilder,
AnimatorScope* ascope,
std::function<void(const T&)>&& apply,
const T* noop = nullptr) {
if (!jprop) return false;
const auto& jpropA = (*jprop)["a"];
const auto& jpropK = (*jprop)["k"];
if (!(*jprop)["x"].is<skjson::NullValue>()) {
abuilder->log(Logger::Level::kWarning, nullptr, "Unsupported expression.");
}
// Older Json versions don't have an "a" animation marker.
// For those, we attempt to parse both ways.
if (!ParseDefault<bool>(jpropA, false)) {
T val;
if (ValueTraits<T>::FromJSON(jpropK, abuilder, &val)) {
// Static property.
if (noop && val == *noop)
return false;
apply(val);
return true;
}
if (!jpropA.is<skjson::NullValue>()) {
abuilder->log(Logger::Level::kError, jprop,
"Could not parse (explicit) static property.");
return false;
}
}
// Keyframe property.
auto animator = KeyframeAnimator<T>::Make(jpropK, abuilder, std::move(apply));
if (!animator) {
abuilder->log(Logger::Level::kError, jprop, "Could not parse keyframed property.");
bool BindLegacyPropertyImpl(const AnimationBuilder& abuilder,
const skjson::ObjectValue* jprop,
AnimatorScope* ascope,
std::function<void(const T&)>&& apply,
const T* noop) {
if (!jprop) {
return false;
}
if (animator->isConstant()) {
// If all keyframes are constant, there is no reason to treat this
// as an animated property - apply immediately and discard the animator.
animator->tick(0);
auto adapter = sk_make_sp<LegacyAnimatorAdapter<T>>(abuilder, jprop, std::move(apply));
if (adapter->isStatic()) {
if (noop && *noop == adapter->value()) {
return false;
}
adapter->tick(0);
} else {
ascope->push_back(std::move(animator));
ascope->push_back(std::move(adapter));
}
return true;
}
class SplitPointAnimator final : public sksg::Animator {
public:
static sk_sp<SplitPointAnimator> Make(const skjson::ObjectValue* jprop,
const AnimationBuilder* abuilder,
std::function<void(const VectorValue&)>&& apply,
const VectorValue*) {
if (!jprop) return nullptr;
sk_sp<SplitPointAnimator> split_animator(new SplitPointAnimator(std::move(apply)));
// This raw pointer is captured in lambdas below. But the lambdas are owned by
// the object itself, so the scope is bound to the life time of the object.
auto* split_animator_ptr = split_animator.get();
if (!BindPropertyImpl<ScalarValue>((*jprop)["x"], abuilder, &split_animator->fAnimators,
[split_animator_ptr](const ScalarValue& x) { split_animator_ptr->setX(x); }) ||
!BindPropertyImpl<ScalarValue>((*jprop)["y"], abuilder, &split_animator->fAnimators,
[split_animator_ptr](const ScalarValue& y) { split_animator_ptr->setY(y); })) {
abuilder->log(Logger::Level::kError, jprop, "Could not parse split property.");
return nullptr;
}
if (split_animator->fAnimators.empty()) {
// Static split property: commit the (buffered) value and discard.
split_animator->onTick(0);
return nullptr;
}
return split_animator;
}
void onTick(float t) override {
for (const auto& animator : fAnimators) {
animator->tick(t);
}
const VectorValue vec = { fX, fY };
fApplyFunc(vec);
}
void setX(const ScalarValue& x) { fX = x; }
void setY(const ScalarValue& y) { fY = y; }
private:
explicit SplitPointAnimator(std::function<void(const VectorValue&)>&& apply)
: fApplyFunc(std::move(apply)) {}
const std::function<void(const VectorValue&)> fApplyFunc;
sksg::AnimatorList fAnimators;
ScalarValue fX = 0,
fY = 0;
using INHERITED = sksg::Animator;
};
bool BindSplitPositionProperty(const skjson::Value& jv,
const AnimationBuilder* abuilder,
AnimatorScope* ascope,
std::function<void(const VectorValue&)>&& apply,
const VectorValue* noop) {
if (auto split_animator = SplitPointAnimator::Make(jv, abuilder, std::move(apply), noop)) {
ascope->push_back(std::move(split_animator));
return true;
}
return false;
}
} // namespace
template <>
bool AnimationBuilder::bindProperty(const skjson::Value& jv,
std::function<void(const ScalarValue&)>&& apply,
const ScalarValue* noop) const {
return BindPropertyImpl(jv, this, fCurrentAnimatorScope, std::move(apply), noop);
return BindLegacyPropertyImpl(*this, jv, fCurrentAnimatorScope, std::move(apply), noop);
}
template <>
bool AnimationBuilder::bindProperty(const skjson::Value& jv,
std::function<void(const VectorValue&)>&& apply,
const VectorValue* noop) const {
if (!jv.is<skjson::ObjectValue>())
return false;
return ParseDefault<bool>(jv.as<skjson::ObjectValue>()["s"], false)
? BindSplitPositionProperty(jv, this, fCurrentAnimatorScope, std::move(apply), noop)
: BindPropertyImpl(jv, this, fCurrentAnimatorScope, std::move(apply), noop);
return BindLegacyPropertyImpl(*this, jv, fCurrentAnimatorScope, std::move(apply), noop);
}
template <>
bool AnimationBuilder::bindProperty(const skjson::Value& jv,
std::function<void(const ShapeValue&)>&& apply,
const ShapeValue* noop) const {
return BindPropertyImpl(jv, this, fCurrentAnimatorScope, std::move(apply), noop);
return BindLegacyPropertyImpl(*this, jv, fCurrentAnimatorScope, std::move(apply), noop);
}
template <>
bool AnimationBuilder::bindProperty(const skjson::Value& jv,
std::function<void(const TextValue&)>&& apply,
const TextValue* noop) const {
return BindPropertyImpl(jv, this, fCurrentAnimatorScope, std::move(apply), noop);
return BindLegacyPropertyImpl(*this, jv, fCurrentAnimatorScope, std::move(apply), noop);
}
} // namespace internal

View File

@ -63,8 +63,7 @@ public:
};
const FontInfo* 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.
// DEPRECATED/TO-BE-REMOVED: use AnimatablePropertyContainer::bind<> instead.
template <typename T>
bool bindProperty(const skjson::Value&,
std::function<void(const T&)>&&,
@ -118,22 +117,18 @@ public:
template <typename T, typename... Args>
sk_sp<sksg::RenderNode> attachDiscardableAdapter(Args&&... args) const {
AutoScope ascope(this);
auto adapter = T::Make(std::forward<Args>(args)...);
auto adapter_animators = ascope.release();
if (!adapter) { return nullptr; }
const auto& node = adapter->renderNode();
if (adapter_animators.empty()) {
// Fire off a synthetic tick to force a single SG sync before discarding the adapter.
adapter->tick(0);
} else {
adapter->setAnimators(std::move(adapter_animators));
fCurrentAnimatorScope->push_back(std::move(adapter));
if (auto adapter = T::Make(std::forward<Args>(args)...)) {
sk_sp<sksg::RenderNode> node = adapter->renderNode();
if (adapter->isStatic()) {
// Fire off a synthetic tick to force a single SG sync before discarding.
adapter->tick(0);
} else {
fCurrentAnimatorScope->push_back(std::move(adapter));
}
return node;
}
return node;
return nullptr;
}
class AutoPropertyTracker {

View File

@ -7,7 +7,7 @@
#include "modules/skottie/src/effects/Effects.h"
#include "modules/skottie/src/SkottieAdapter.h"
#include "modules/skottie/src/Animator.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/sksg/include/SkSGColorFilter.h"
@ -18,11 +18,23 @@ namespace internal {
namespace {
class HueSaturationEffectAdapter final : public DiscardableAdaptorBase {
class HueSaturationEffectAdapter final : public AnimatablePropertyContainer {
public:
static sk_sp<HueSaturationEffectAdapter> Make(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder) {
return sk_sp<HueSaturationEffectAdapter>(
new HueSaturationEffectAdapter(jprops, std::move(layer), abuilder));
}
const sk_sp<sksg::ExternalColorFilter>& renderNode() const { return fColorFilter; }
private:
HueSaturationEffectAdapter(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder)
: fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {
enum : size_t {
kChannelControl_Index = 0,
kChannelRange_Index = 1,
@ -33,52 +45,24 @@ public:
kColorizeHue_Index = 6,
kColorizeSat_Index = 7,
kColorizeLightness_Index = 8,
kMax_Index = kColorizeLightness_Index
};
auto adapter = sk_sp<HueSaturationEffectAdapter>(
new HueSaturationEffectAdapter(std::move(layer)));
auto* raw_adapter = adapter.get();
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kChannelControl_Index),
[raw_adapter](const ScalarValue& c) {
raw_adapter->fChanCtrl = c;
});
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kMasterHue_Index),
[raw_adapter](const ScalarValue& h) {
raw_adapter->fMasterHue = h;
});
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kMasterSat_Index),
[raw_adapter](const ScalarValue& s) {
raw_adapter->fMasterSat = s;
});
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kMasterLightness_Index),
[raw_adapter](const ScalarValue& l) {
raw_adapter->fMasterLight = l;
});
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kChannelControl_Index),
&fChanCtrl);
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kMasterHue_Index),
&fMasterHue);
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kMasterSat_Index),
&fMasterSat);
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kMasterLightness_Index),
&fMasterLight);
// TODO: colorize support?
return adapter;
}
const sk_sp<sksg::ExternalColorFilter>& renderNode() const { return fColorFilter; }
protected:
void onSync() override {
fColorFilter->setColorFilter(this->makeColorFilter());
}
private:
explicit HueSaturationEffectAdapter(sk_sp<sksg::RenderNode> layer)
: fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {}
sk_sp<SkColorFilter> makeColorFilter() const {
enum : uint8_t {
kMaster_Chan = 0x01,
@ -127,8 +111,6 @@ private:
fMasterHue = 0.0f,
fMasterSat = 0.0f,
fMasterLight = 0.0f;
using INHERITED = DiscardableAdaptorBase;
};
} // namespace

View File

@ -7,7 +7,7 @@
#include "modules/skottie/src/effects/Effects.h"
#include "modules/skottie/src/SkottieAdapter.h"
#include "modules/skottie/src/Animator.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/sksg/include/SkSGColorFilter.h"
#include "src/utils/SkJSON.h"
@ -17,31 +17,28 @@ namespace internal {
namespace {
class InvertEffectAdapter final : public DiscardableAdaptorBase {
class InvertEffectAdapter final : public AnimatablePropertyContainer {
public:
static sk_sp<InvertEffectAdapter> Make(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder) {
enum : size_t {
kChannel_Index = 0,
};
auto adapter = sk_sp<InvertEffectAdapter>(new InvertEffectAdapter(std::move(layer)));
auto* raw_adapter = adapter.get();
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops, kChannel_Index),
[raw_adapter](const ScalarValue& c) {
raw_adapter->fChannel = c;
});
return adapter;
return sk_sp<InvertEffectAdapter>(
new InvertEffectAdapter(jprops, std::move(layer), abuilder));
}
const sk_sp<sksg::ExternalColorFilter>& renderNode() const { return fColorFilter; }
private:
explicit InvertEffectAdapter(sk_sp<sksg::RenderNode> layer)
: fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {}
InvertEffectAdapter(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder)
: fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {
enum : size_t {
kChannel_Index = 0,
};
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kChannel_Index), &fChannel);
}
void onSync() override {
struct STColorMatrix {

View File

@ -8,7 +8,8 @@
#include "modules/skottie/src/effects/Effects.h"
#include "include/private/SkColorData.h"
#include "modules/skottie/src/SkottieAdapter.h"
#include "modules/skottie/src/Animator.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/sksg/include/SkSGColorFilter.h"
namespace skottie {
@ -25,47 +26,34 @@ namespace {
*
* C.r, C.g, C.b, C.a, Luminance(C), Hue(C), Saturation(C), Lightness(C), 1 or 0.
*/
class ShiftChannelsEffectAdapter final : public DiscardableAdaptorBase {
class ShiftChannelsEffectAdapter final : public AnimatablePropertyContainer {
public:
static sk_sp<ShiftChannelsEffectAdapter> Make(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder) {
enum : size_t {
kTakeAlphaFrom_Index = 0,
kTakeRedFrom_Index = 1,
kTakeGreenFrom_Index = 2,
kTakeBlueFrom_Index = 3,
kMax_Index = kTakeBlueFrom_Index
};
auto adapter = sk_sp<ShiftChannelsEffectAdapter>(
new ShiftChannelsEffectAdapter(std::move(layer)));
// Use raw captures, pending TheBigRefactoringToComeReallySoonNow.
auto* raw_adapter = adapter.get();
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kTakeRedFrom_Index),
[raw_adapter](const ScalarValue& r) { raw_adapter->fR = r; });
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kTakeGreenFrom_Index),
[raw_adapter](const ScalarValue& g) { raw_adapter->fG = g; });
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kTakeBlueFrom_Index),
[raw_adapter](const ScalarValue& b) { raw_adapter->fB = b; });
abuilder->bindProperty<ScalarValue>(EffectBuilder::GetPropValue(jprops,
kTakeAlphaFrom_Index),
[raw_adapter](const ScalarValue& a) { raw_adapter->fA = a; });
return adapter;
return sk_sp<ShiftChannelsEffectAdapter>(
new ShiftChannelsEffectAdapter(jprops, std::move(layer), abuilder));
}
const sk_sp<sksg::ExternalColorFilter>& renderNode() const { return fColorFilter; }
private:
explicit ShiftChannelsEffectAdapter(sk_sp<sksg::RenderNode> layer)
: fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {}
ShiftChannelsEffectAdapter(const skjson::ArrayValue& jprops,
sk_sp<sksg::RenderNode> layer,
const AnimationBuilder* abuilder)
: fColorFilter(sksg::ExternalColorFilter::Make(std::move(layer))) {
enum : size_t {
kTakeAlphaFrom_Index = 0,
kTakeRedFrom_Index = 1,
kTakeGreenFrom_Index = 2,
kTakeBlueFrom_Index = 3,
};
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kTakeRedFrom_Index), &fR);
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kTakeGreenFrom_Index), &fG);
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kTakeBlueFrom_Index), &fB);
this->bind(*abuilder, EffectBuilder::GetPropValue(jprops, kTakeAlphaFrom_Index), &fA);
}
enum class Source : uint8_t {
kAlpha = 1,
@ -127,8 +115,6 @@ private:
fG = static_cast<float>(Source::kGreen),
fB = static_cast<float>(Source::kBlue),
fA = static_cast<float>(Source::kAlpha);
using INHERITED = DiscardableAdaptorBase;
};
} // namespace

View File

@ -8,6 +8,7 @@
#include "modules/skottie/src/text/RangeSelector.h"
#include "include/core/SkCubicMap.h"
#include "modules/skottie/src/Animator.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/SkottieValue.h"
@ -229,7 +230,8 @@ static constexpr ShapeInfo gShapeInfo[] = {
} // namespace
sk_sp<RangeSelector> RangeSelector::Make(const skjson::ObjectValue* jrange,
const AnimationBuilder* abuilder) {
const AnimationBuilder* abuilder,
AnimatablePropertyContainer* acontainer) {
if (!jrange) {
return nullptr;
}
@ -280,39 +282,17 @@ sk_sp<RangeSelector> RangeSelector::Make(const skjson::ObjectValue* jrange,
ParseEnum<Domain>(gDomainMap, (*jrange)["b" ], abuilder, "domain"),
ParseEnum<Mode> (gModeMap , (*jrange)["m" ], abuilder, "mode" ),
ParseEnum<Shape> (gShapeMap , (*jrange)["sh"], abuilder, "shape" )));
auto* raw_selector = selector.get();
abuilder->bindProperty<ScalarValue>((*jrange)["s"],
[raw_selector](const ScalarValue& s) {
raw_selector->fStart = s;
});
abuilder->bindProperty<ScalarValue>((*jrange)["e"],
[raw_selector](const ScalarValue& e) {
raw_selector->fEnd = e;
});
abuilder->bindProperty<ScalarValue>((*jrange)["o"],
[raw_selector](const ScalarValue& o) {
raw_selector->fOffset = o;
});
abuilder->bindProperty<ScalarValue>((*jrange)["a"],
[raw_selector](const ScalarValue& a) {
raw_selector->fAmount = a;
});
abuilder->bindProperty<ScalarValue>((*jrange)["ne"],
[raw_selector](const ScalarValue& ne) {
raw_selector->fEaseLo = ne;
});
abuilder->bindProperty<ScalarValue>((*jrange)["xe"],
[raw_selector](const ScalarValue& xe) {
raw_selector->fEaseHi = xe;
});
acontainer->bind(*abuilder, (*jrange)["s" ], &selector->fStart );
acontainer->bind(*abuilder, (*jrange)["e" ], &selector->fEnd );
acontainer->bind(*abuilder, (*jrange)["o" ], &selector->fOffset);
acontainer->bind(*abuilder, (*jrange)["a" ], &selector->fAmount);
acontainer->bind(*abuilder, (*jrange)["ne"], &selector->fEaseLo);
acontainer->bind(*abuilder, (*jrange)["xe"], &selector->fEaseHi);
// Optional square "smoothness" prop.
if (selector->fShape == Shape::kSquare) {
abuilder->bindProperty<ScalarValue>((*jrange)["sm"],
[selector](const ScalarValue& sm) {
selector->fSmoothness = sm;
});
acontainer->bind(*abuilder, (*jrange)["sm" ], &selector->fSmoothness);
}
return selector;

View File

@ -21,7 +21,8 @@ namespace internal {
class RangeSelector final : public SkNVRefCnt<RangeSelector> {
public:
static sk_sp<RangeSelector> Make(const skjson::ObjectValue*,
const AnimationBuilder*);
const AnimationBuilder*,
AnimatablePropertyContainer*);
enum class Units : uint8_t {
kPercentage, // values are percentages of domain size

View File

@ -60,38 +60,27 @@ sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
return nullptr;
}
std::vector<sk_sp<TextAnimator>> animators;
auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr), std::move(logger)));
adapter->bind(*abuilder, jd, &adapter->fText.fCurrentValue);
if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
animators.reserve(janimators->size());
adapter->fAnimators.reserve(janimators->size());
for (const skjson::ObjectValue* janimator : *janimators) {
if (auto animator = TextAnimator::Make(janimator, abuilder)) {
animators.push_back(std::move(animator));
if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
adapter->fAnimators.push_back(std::move(animator));
}
}
}
auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
std::move(logger),
std::move(animators)));
auto* raw_adapter = adapter.get();
abuilder->bindProperty<TextValue>(*jd,
[raw_adapter] (const TextValue& txt) {
raw_adapter->setText(txt);
});
abuilder->dispatchTextProperty(adapter);
return adapter;
}
TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr,
sk_sp<Logger> logger,
std::vector<sk_sp<TextAnimator>>&& animators)
TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr, sk_sp<Logger> logger)
: fRoot(sksg::Group::Make())
, fFontMgr(std::move(fontmgr))
, fAnimators(std::move(animators))
, fLogger(std::move(logger)) {}
TextAdapter::~TextAdapter() = default;
@ -114,17 +103,17 @@ void TextAdapter::addFragment(const Shaper::Fragment& frag) {
frag.fPos.y()));
std::vector<sk_sp<sksg::RenderNode>> draws;
draws.reserve(static_cast<size_t>(fText.fHasFill) + static_cast<size_t>(fText.fHasStroke));
draws.reserve(static_cast<size_t>(fText->fHasFill) + static_cast<size_t>(fText->fHasStroke));
SkASSERT(fText.fHasFill || fText.fHasStroke);
SkASSERT(fText->fHasFill || fText->fHasStroke);
if (fText.fHasFill) {
rec.fFillColorNode = sksg::Color::Make(fText.fFillColor);
if (fText->fHasFill) {
rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
rec.fFillColorNode->setAntiAlias(true);
draws.push_back(sksg::Draw::Make(blob_node, rec.fFillColorNode));
}
if (fText.fHasStroke) {
rec.fStrokeColorNode = sksg::Color::Make(fText.fStrokeColor);
if (fText->fHasStroke) {
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));
@ -187,28 +176,25 @@ void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
}
void TextAdapter::setText(const TextValue& txt) {
if (txt != fText) {
fText = txt;
fTextDirty = true;
}
fText.fCurrentValue = txt;
}
void TextAdapter::reshape() {
const Shaper::TextDesc text_desc = {
fText.fTypeface,
fText.fTextSize,
fText.fLineHeight,
fText.fAscent,
fText.fHAlign,
fText.fVAlign,
fText->fTypeface,
fText->fTextSize,
fText->fLineHeight,
fText->fAscent,
fText->fHAlign,
fText->fVAlign,
fAnimators.empty() ? Shaper::Flags::kNone : Shaper::Flags::kFragmentGlyphs,
};
const auto shape_result = Shaper::Shape(fText.fText, text_desc, fText.fBox, fFontMgr);
const auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);
if (fLogger && shape_result.fMissingGlyphCount > 0) {
const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
shape_result.fMissingGlyphCount,
fText.fText.c_str());
fText->fText.c_str());
fLogger->log(Logger::Level::kWarning, msg.c_str());
// This may trigger repeatedly when the text is animating.
@ -251,13 +237,12 @@ void TextAdapter::reshape() {
}
void TextAdapter::onSync() {
if (!fText.fHasFill && !fText.fHasStroke) {
if (!fText->fHasFill && !fText->fHasStroke) {
return;
}
if (fTextDirty) {
if (fText.hasChanged()) {
this->reshape();
fTextDirty = false;
}
if (fFragments.empty()) {
@ -265,9 +250,9 @@ void TextAdapter::onSync() {
}
// Seed props from the current text value.
TextAnimator::AnimatedProps seed_props;
seed_props.fill_color = fText.fFillColor;
seed_props.stroke_color = fText.fStrokeColor;
TextAnimator::ResolvedProps seed_props;
seed_props.fill_color = fText->fFillColor;
seed_props.stroke_color = fText->fStrokeColor;
TextAnimator::ModulatorBuffer buf;
buf.resize(fFragments.size(), { seed_props, 0 });
@ -299,7 +284,7 @@ void TextAdapter::onSync() {
}
}
void TextAdapter::pushPropsToFragment(const TextAnimator::AnimatedProps& props,
void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
const FragmentRec& rec) const {
// TODO: share this with TransformAdapter2D?
auto t = SkMatrix::MakeTrans(rec.fOrigin.x() + props.position.x(),
@ -347,7 +332,7 @@ void TextAdapter::adjustLineTracking(const TextAnimator::ModulatorBuffer& buf,
return 0.0f;
};
const auto align_offset = total_tracking * align_factor(fText.fHAlign);
const auto align_offset = total_tracking * align_factor(fText->fHAlign);
float tracking_acc = 0;
for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {

View File

@ -8,7 +8,7 @@
#ifndef SkottieTextAdapter_DEFINED
#define SkottieTextAdapter_DEFINED
#include "modules/skottie/src/SkottieAdapter.h"
#include "modules/skottie/src/Animator.h"
#include "modules/skottie/src/text/SkottieShaper.h"
#include "modules/skottie/src/text/TextAnimator.h"
#include "modules/skottie/src/text/TextValue.h"
@ -17,10 +17,16 @@
class SkFontMgr;
namespace sksg {
class Group;
template <typename T>
class Matrix;
} // namespace sksg
namespace skottie {
namespace internal {
class TextAdapter final : public DiscardableAdaptorBase {
class TextAdapter final : public AnimatablePropertyContainer {
public:
static sk_sp<TextAdapter> Make(const skjson::ObjectValue&, const AnimationBuilder*,
sk_sp<SkFontMgr>, sk_sp<Logger>);
@ -29,14 +35,14 @@ public:
const sk_sp<sksg::Group>& renderNode() const { return fRoot; }
const TextValue& getText() const { return fText; }
const TextValue& getText() const { return fText.fCurrentValue; }
void setText(const TextValue&);
protected:
void onSync() override;
private:
TextAdapter(sk_sp<SkFontMgr>, sk_sp<Logger>, std::vector<sk_sp<TextAnimator>>&&);
TextAdapter(sk_sp<SkFontMgr>, sk_sp<Logger>);
struct FragmentRec {
SkPoint fOrigin; // fragment position
@ -50,22 +56,39 @@ private:
void addFragment(const Shaper::Fragment&);
void buildDomainMaps(const Shaper::Result&);
void pushPropsToFragment(const TextAnimator::AnimatedProps&, const FragmentRec&) const;
void pushPropsToFragment(const TextAnimator::ResolvedProps&, const FragmentRec&) const;
void adjustLineTracking(const TextAnimator::ModulatorBuffer&,
const TextAnimator::DomainSpan&,
float line_tracking) const;
const sk_sp<sksg::Group> fRoot;
const sk_sp<SkFontMgr> fFontMgr;
const std::vector<sk_sp<TextAnimator>> fAnimators;
sk_sp<Logger> fLogger;
const sk_sp<sksg::Group> fRoot;
const sk_sp<SkFontMgr> fFontMgr;
sk_sp<Logger> fLogger;
std::vector<FragmentRec> fFragments;
TextAnimator::DomainMaps fMaps;
std::vector<sk_sp<TextAnimator>> fAnimators;
std::vector<FragmentRec> fFragments;
TextAnimator::DomainMaps fMaps;
TextValue fText;
bool fTextDirty = true;
// Helps detect external value changes.
struct TextValueTracker {
TextValue fCurrentValue;
bool hasChanged() const {
if (fCurrentValue != fPrevValue) {
fPrevValue = fCurrentValue;
return true;
}
return false;
}
const TextValue* operator->() const { return &fCurrentValue; }
private:
mutable TextValue fPrevValue;
};
TextValueTracker fText;
};
} // namespace internal

View File

@ -10,6 +10,7 @@
#include "include/core/SkColor.h"
#include "include/core/SkPoint.h"
#include "include/private/SkNx.h"
#include "modules/skottie/src/Animator.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/skottie/src/text/RangeSelector.h"
#include "src/utils/SkJSON.h"
@ -58,7 +59,8 @@ namespace internal {
* }
*/
sk_sp<TextAnimator> TextAnimator::Make(const skjson::ObjectValue* janimator,
const AnimationBuilder* abuilder) {
const AnimationBuilder* abuilder,
AnimatablePropertyContainer* acontainer) {
if (!janimator) {
return nullptr;
}
@ -75,18 +77,19 @@ sk_sp<TextAnimator> TextAnimator::Make(const skjson::ObjectValue* janimator,
if (const skjson::ArrayValue* jselectors = (*janimator)["s"]) {
selectors.reserve(jselectors->size());
for (const skjson::ObjectValue* jselector : *jselectors) {
if (auto sel = RangeSelector::Make(*jselector, abuilder)) {
if (auto sel = RangeSelector::Make(*jselector, abuilder, acontainer)) {
selectors.push_back(std::move(sel));
}
}
} else {
if (auto sel = RangeSelector::Make((*janimator)["s"], abuilder)) {
if (auto sel = RangeSelector::Make((*janimator)["s"], abuilder, acontainer)) {
selectors.reserve(1);
selectors.push_back(std::move(sel));
}
}
return sk_sp<TextAnimator>(new TextAnimator(std::move(selectors), *jprops, abuilder));
return sk_sp<TextAnimator>(
new TextAnimator(std::move(selectors), *jprops, abuilder, acontainer));
}
void TextAnimator::modulateProps(const DomainMaps& maps, ModulatorBuffer& buf) const {
@ -109,15 +112,15 @@ void TextAnimator::modulateProps(const DomainMaps& maps, ModulatorBuffer& buf) c
}
}
TextAnimator::AnimatedProps TextAnimator::modulateProps(const AnimatedProps& props,
TextAnimator::ResolvedProps TextAnimator::modulateProps(const ResolvedProps& props,
float amount) const {
auto modulated_props = props;
// Transform props compose.
modulated_props.position += fTextProps.position * amount;
modulated_props.position += ValueTraits<VectorValue>::As<SkPoint>(fTextProps.position) * amount;
modulated_props.rotation += fTextProps.rotation * amount;
modulated_props.tracking += fTextProps.tracking * amount;
modulated_props.scale *= 1 + (fTextProps.scale - 1) * amount;
modulated_props.scale *= 1 + (fTextProps.scale * 0.01f - 1) * amount; // scale is 100-based
const auto lerp_color = [](SkColor c0, SkColor c1, float t) {
const auto c0_4f = SkNx_cast<float>(Sk4b::Load(&c0)),
@ -132,56 +135,32 @@ TextAnimator::AnimatedProps TextAnimator::modulateProps(const AnimatedProps& pro
// Colors and opacity are overridden, and use a clamped amount value.
const auto clamped_amount = std::max(amount, 0.0f);
if (fHasFillColor) {
modulated_props.fill_color = lerp_color(props.fill_color,
fTextProps.fill_color,
clamped_amount);
const auto fc = ValueTraits<VectorValue>::As<SkColor>(fTextProps.fill_color);
modulated_props.fill_color = lerp_color(props.fill_color, fc, clamped_amount);
}
if (fHasStrokeColor) {
modulated_props.stroke_color = lerp_color(props.stroke_color,
fTextProps.stroke_color,
clamped_amount);
const auto sc = ValueTraits<VectorValue>::As<SkColor>(fTextProps.stroke_color);
modulated_props.stroke_color = lerp_color(props.stroke_color, sc, clamped_amount);
}
modulated_props.opacity *= 1 + (fTextProps.opacity - 1) * clamped_amount;
modulated_props.opacity *= 1 + (fTextProps.opacity * 0.01f - 1) * clamped_amount; // 100-based
return modulated_props;
}
TextAnimator::TextAnimator(std::vector<sk_sp<RangeSelector>>&& selectors,
const skjson::ObjectValue& jprops,
const AnimationBuilder* abuilder)
const AnimationBuilder* abuilder,
AnimatablePropertyContainer* acontainer)
: fSelectors(std::move(selectors)) {
auto* animator = this;
abuilder->bindProperty<VectorValue>(jprops["p"],
[animator](const VectorValue& p) {
animator->fTextProps.position = ValueTraits<VectorValue>::As<SkPoint>(p);
});
abuilder->bindProperty<ScalarValue>(jprops["s"],
[animator](const ScalarValue& s) {
// Scale is 100-based.
animator->fTextProps.scale = s * 0.01f;
});
abuilder->bindProperty<ScalarValue>(jprops["r"],
[animator](const ScalarValue& r) {
animator->fTextProps.rotation = r;
});
fHasFillColor = abuilder->bindProperty<VectorValue>(jprops["fc"],
[animator](const VectorValue& fc) {
animator->fTextProps.fill_color = ValueTraits<VectorValue>::As<SkColor>(fc);
});
fHasStrokeColor = abuilder->bindProperty<VectorValue>(jprops["sc"],
[animator](const VectorValue& sc) {
animator->fTextProps.stroke_color = ValueTraits<VectorValue>::As<SkColor>(sc);
});
abuilder->bindProperty<ScalarValue>(jprops["o"],
[animator](const ScalarValue& o) {
// Opacity is 100-based.
animator->fTextProps.opacity = SkTPin<float>(o * 0.01f, 0, 1);
});
abuilder->bindProperty<ScalarValue>(jprops["t"],
[animator](const ScalarValue& t) {
animator->fTextProps.tracking = t;
});
acontainer->bind(*abuilder, jprops["p" ], &fTextProps.position);
acontainer->bind(*abuilder, jprops["s" ], &fTextProps.scale );
acontainer->bind(*abuilder, jprops["r" ], &fTextProps.rotation);
acontainer->bind(*abuilder, jprops["o" ], &fTextProps.opacity );
acontainer->bind(*abuilder, jprops["t" ], &fTextProps.tracking);
fHasFillColor = acontainer->bind(*abuilder, jprops["fc"], &fTextProps.fill_color );
fHasStrokeColor = acontainer->bind(*abuilder, jprops["sc"], &fTextProps.stroke_color);
}
} // namespace internal

View File

@ -10,6 +10,7 @@
#include "include/core/SkRefCnt.h"
#include "modules/skottie/src/SkottiePriv.h"
#include "modules/skottie/src/SkottieValue.h"
#include "modules/sksg/include/SkSGScene.h"
#include <memory>
@ -18,15 +19,28 @@
namespace skottie {
namespace internal {
class AnimatablePropertyContainer;
class AnimationBuilder;
class RangeSelector;
class TextAnimator final : public SkNVRefCnt<TextAnimator> {
public:
static sk_sp<TextAnimator> Make(const skjson::ObjectValue*,
const AnimationBuilder*);
const AnimationBuilder*,
AnimatablePropertyContainer* acontainer);
// Direct mapping of AE properties.
struct AnimatedProps {
VectorValue position,
fill_color,
stroke_color;
ScalarValue opacity = 100,
scale = 100,
rotation = 0,
tracking = 0;
};
struct ResolvedProps {
SkPoint position = { 0, 0 };
float opacity = 1,
scale = 1,
@ -37,7 +51,7 @@ public:
};
struct AnimatedPropsModulator {
AnimatedProps props; // accumulates properties across *all* animators
ResolvedProps props; // accumulates properties across *all* animators
float coverage; // accumulates range selector coverage for a given animator
};
using ModulatorBuffer = std::vector<AnimatedPropsModulator>;
@ -61,11 +75,12 @@ public:
void modulateProps(const DomainMaps&, ModulatorBuffer&) const;
private:
TextAnimator(std::vector<sk_sp<RangeSelector>>&& selectors,
const skjson::ObjectValue& jprops,
const AnimationBuilder* abuilder);
TextAnimator(std::vector<sk_sp<RangeSelector>>&&,
const skjson::ObjectValue&,
const AnimationBuilder*,
AnimatablePropertyContainer*);
AnimatedProps modulateProps(const AnimatedProps&, float amount) const;
ResolvedProps modulateProps(const ResolvedProps&, float amount) const;
const std::vector<sk_sp<RangeSelector>> fSelectors;

View File

@ -32,7 +32,7 @@ public:
Animator(const Animator&) = delete;
Animator& operator=(const Animator&) = delete;
void tick(float t);
void tick(float t) { this->onTick(t); }
protected:
Animator();

View File

@ -18,10 +18,6 @@ namespace sksg {
Animator::Animator() = default;
Animator::~Animator() = default;
void Animator::tick(float t) {
this->onTick(t);
}
GroupAnimator::GroupAnimator(AnimatorList&& animators)
: fAnimators(std::move(animators)) {}