[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:
parent
a8fa49016c
commit
d5c42c8c03
@ -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",
|
||||
|
429
modules/skottie/src/Animator.cpp
Normal file
429
modules/skottie/src/Animator.cpp
Normal 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
|
46
modules/skottie/src/Animator.h
Normal file
46
modules/skottie/src/Animator.h
Normal 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
|
@ -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)) {}
|
||||
|
||||
|
@ -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 { \
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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)) {}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user