blob: afc2ddc11235cfa7256a6744c487d6d7eaff9908 [file] [log] [blame]
/*
* 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();
}
void AnimatablePropertyContainer::attachDiscardableAdapter(
sk_sp<AnimatablePropertyContainer> child) {
if (!child) {
return;
}
if (child->isStatic()) {
child->tick(0);
return;
}
fAnimators.push_back(child);
}
void AnimatablePropertyContainer::shrink_to_fit() {
fAnimators.shrink_to_fit();
}
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(std::max<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