/* * Copyright 2017 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SkCubicMap.h" #include "SkottieJson.h" #include "SkottiePriv.h" #include "SkottieValue.h" #include "SkSGScene.h" #include "SkString.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 : SkTPin(fCubicMaps[rec.cmidx].computeYFromX(lt), 0.0f, 1.0f); } virtual int parseValue(const skjson::Value&, const AnimationBuilder* abuilder) = 0; void parseKeyFrames(const skjson::ArrayValue& jframes, const AnimationBuilder* abuilder) { for (const skjson::ObjectValue* jframe : jframes) { if (!jframe) continue; float t0; if (!Parse<float>((*jframe)["t"], &t0)) continue; 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 in prev interval. Note: we do this even if we end up discarding // the current interval (to support "t"-only final frames). fRecs.back().t1 = t0; } // Required start value. const auto v0_idx = this->parseValue((*jframe)["s"], abuilder); if (v0_idx < 0) continue; // Optional end value. const auto v1_idx = this->parseValue((*jframe)["e"], abuilder); if (v1_idx < 0) { // Constant keyframe. fRecs.push_back({t0, t0, v0_idx, v0_idx, -1 }); continue; } // default is linear lerp static constexpr SkPoint kDefaultC0 = { 0, 0 }, kDefaultC1 = { 1, 1 }; const auto c0 = ParseDefault<SkPoint>((*jframe)["i"], kDefaultC0), c1 = ParseDefault<SkPoint>((*jframe)["o"], kDefaultC1); int cm_idx = -1; if (c0 != kDefaultC0 || c1 != kDefaultC1) { // TODO: is it worth de-duping these? cm_idx = SkToInt(fCubicMaps.size()); fCubicMaps.emplace_back(c1, c0); } fRecs.push_back({t0, t0, v0_idx, v1_idx, cm_idx }); } // If we couldn't determine a valid t1 for the last frame, discard it. if (!fRecs.empty() && !fRecs.back().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 { public: static std::unique_ptr<KeyframeAnimator> Make(const skjson::ArrayValue* jv, const AnimationBuilder* abuilder, std::function<void(const T&)>&& apply) { if (!jv) return nullptr; std::unique_ptr<KeyframeAnimator> animator( new KeyframeAnimator(*jv, abuilder, std::move(apply))); if (!animator->count()) return nullptr; return animator; } protected: void onTick(float t) override { fApplyFunc(*this->eval(this->frame(t), t, &fScratch)); } 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; } 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; }; 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."); return false; } ascope->push_back(std::move(animator)); return true; } class SplitPointAnimator final : public sksg::Animator { public: static std::unique_ptr<SplitPointAnimator> Make(const skjson::ObjectValue* jprop, const AnimationBuilder* abuilder, std::function<void(const VectorValue&)>&& apply, const VectorValue*) { if (!jprop) return nullptr; std::unique_ptr<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::unique_ptr<sksg::Animator>(split_animator.release())); return true; } return false; } } // namespace template <> bool AnimationBuilder::bindProperty(const skjson::Value& jv, AnimatorScope* ascope, std::function<void(const ScalarValue&)>&& apply, const ScalarValue* noop) const { return BindPropertyImpl(jv, this, ascope, std::move(apply), noop); } template <> bool AnimationBuilder::bindProperty(const skjson::Value& jv, AnimatorScope* ascope, 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, ascope, std::move(apply), noop) : BindPropertyImpl(jv, this, ascope, std::move(apply), noop); } template <> bool AnimationBuilder::bindProperty(const skjson::Value& jv, AnimatorScope* ascope, std::function<void(const ShapeValue&)>&& apply, const ShapeValue* noop) const { return BindPropertyImpl(jv, this, ascope, std::move(apply), noop); } template <> bool AnimationBuilder::bindProperty(const skjson::Value& jv, AnimatorScope* ascope, std::function<void(const TextValue&)>&& apply, const TextValue* noop) const { return BindPropertyImpl(jv, this, ascope, std::move(apply), noop); } } // namespace internal } // namespace skottie