/* * 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 "include/core/SkContourMeasure.h" #include "include/core/SkPathBuilder.h" #include "modules/skottie/src/SkottieJson.h" #include "modules/skottie/src/SkottieValue.h" #include "modules/skottie/src/animator/Animator.h" #include "modules/skottie/src/animator/KeyframeAnimator.h" #include namespace skottie::internal { namespace { // Spatial 2D specialization: stores SkV2s and optional contour interpolators externally. class Vec2KeyframeAnimator final : public KeyframeAnimator { struct SpatialValue { Vec2Value v2; sk_sp cmeasure; }; public: class Builder final : public KeyframeAnimatorBuilder { public: Builder(Vec2Value* vec_target, float* rot_target) : fVecTarget(vec_target) , fRotTarget(rot_target) {} sk_sp make(const AnimationBuilder& abuilder, const skjson::ArrayValue& jkfs) override { SkASSERT(jkfs.size() > 0); fValues.reserve(jkfs.size()); if (!this->parseKeyframes(abuilder, jkfs)) { return nullptr; } fValues.shrink_to_fit(); return sk_sp( new Vec2KeyframeAnimator(std::move(fKFs), std::move(fCMs), std::move(fValues), fVecTarget, fRotTarget)); } bool parseValue(const AnimationBuilder&, const skjson::Value& jv) const override { return Parse(jv, fVecTarget); } private: void backfill_spatial(const SpatialValue& val) { SkASSERT(!fValues.empty()); auto& prev_val = fValues.back(); SkASSERT(!prev_val.cmeasure); if (val.v2 == prev_val.v2) { // spatial interpolation only make sense for noncoincident values return; } // Check whether v0 and v1 have the same direction AND ||v0||>=||v1|| auto check_vecs = [](const SkV2& v0, const SkV2& v1) { const auto v0_len2 = v0.lengthSquared(), v1_len2 = v1.lengthSquared(); // check magnitude if (v0_len2 < v1_len2) { return false; } // v0, v1 have the same direction iff dot(v0,v1) = ||v0||*||v1|| // <=> dot(v0,v1)^2 = ||v0||^2 * ||v1||^2 const auto dot = v0.dot(v1); return SkScalarNearlyEqual(dot * dot, v0_len2 * v1_len2); }; if (check_vecs(val.v2 - prev_val.v2, fTo) && check_vecs(prev_val.v2 - val.v2, fTi)) { // Both control points lie on the [prev_val..val] segment // => we can power-reduce the Bezier "curve" to a straight line. return; } // Finally, this looks like a legitimate spatial keyframe. SkPathBuilder p; p.moveTo (prev_val.v2.x , prev_val.v2.y); p.cubicTo(prev_val.v2.x + fTo.x, prev_val.v2.y + fTo.y, val.v2.x + fTi.x, val.v2.y + fTi.y, val.v2.x, val.v2.y); prev_val.cmeasure = SkContourMeasureIter(p.detach(), false).next(); } bool parseKFValue(const AnimationBuilder&, const skjson::ObjectValue& jkf, const skjson::Value& jv, Keyframe::Value* v) override { SpatialValue val; if (!Parse(jv, &val.v2)) { return false; } if (fPendingSpatial) { this->backfill_spatial(val); } // Track the last keyframe spatial tangents (checked on next parseValue). fTi = ParseDefault(jkf["ti"], {0,0}); fTo = ParseDefault(jkf["to"], {0,0}); fPendingSpatial = fTi != SkV2{0,0} || fTo != SkV2{0,0}; if (fValues.empty() || val.v2 != fValues.back().v2 || fPendingSpatial) { fValues.push_back(std::move(val)); } v->idx = SkToU32(fValues.size() - 1); return true; } std::vector fValues; Vec2Value* fVecTarget; // required float* fRotTarget; // optional SkV2 fTi{0,0}, fTo{0,0}; bool fPendingSpatial = false; }; private: Vec2KeyframeAnimator(std::vector kfs, std::vector cms, std::vector vs, Vec2Value* vec_target, float* rot_target) : INHERITED(std::move(kfs), std::move(cms)) , fValues(std::move(vs)) , fVecTarget(vec_target) , fRotTarget(rot_target) {} StateChanged update(const Vec2Value& new_vec_value, const Vec2Value& new_tan_value) { auto changed = (new_vec_value != *fVecTarget); *fVecTarget = new_vec_value; if (fRotTarget) { const auto new_rot_value = SkRadiansToDegrees(std::atan2(new_tan_value.y, new_tan_value.x)); changed |= new_rot_value != *fRotTarget; *fRotTarget = new_rot_value; } return changed; } StateChanged onSeek(float t) override { auto get_lerp_info = [this](float t) { auto lerp_info = this->getLERPInfo(t); // When tracking rotation/orientation, the last keyframe requires special handling: // it doesn't store any spatial information but it is expected to maintain the // previous orientation (per AE semantics). // // The easiest way to achieve this is to actually swap with the previous keyframe, // with an adjusted weight of 1. const auto vidx = lerp_info.vrec0.idx; if (fRotTarget && vidx == fValues.size() - 1 && vidx > 0) { SkASSERT(!fValues[vidx].cmeasure); SkASSERT(lerp_info.vrec1.idx == vidx); // Change LERPInfo{0, SIZE - 1, SIZE - 1} // to LERPInfo{1, SIZE - 2, SIZE - 1} lerp_info.weight = 1; lerp_info.vrec0 = {vidx - 1}; // This yields equivalent lerp results because keyframed values are contiguous // i.e frame[n-1].end_val == frame[n].start_val. } return lerp_info; }; const auto lerp_info = get_lerp_info(t); const auto& v0 = fValues[lerp_info.vrec0.idx]; if (v0.cmeasure) { // Spatial keyframe: the computed weight is relative to the interpolation path // arc length. SkPoint pos; SkVector tan; if (v0.cmeasure->getPosTan(lerp_info.weight * v0.cmeasure->length(), &pos, &tan)) { return this->update({ pos.fX, pos.fY }, {tan.fX, tan.fY}); } } const auto& v1 = fValues[lerp_info.vrec1.idx]; const auto tan = v1.v2 - v0.v2; return this->update(Lerp(v0.v2, v1.v2, lerp_info.weight), tan); } const std::vector fValues; Vec2Value* fVecTarget; float* fRotTarget; using INHERITED = KeyframeAnimator; }; } // namespace bool AnimatablePropertyContainer::bindAutoOrientable(const AnimationBuilder& abuilder, const skjson::ObjectValue* jprop, Vec2Value* v, float* orientation) { if (!jprop) { return false; } if (!ParseDefault((*jprop)["s"], false)) { // Regular (static or keyframed) 2D value. Vec2KeyframeAnimator::Builder builder(v, orientation); return this->bindImpl(abuilder, jprop, builder); } // Separate-dimensions vector value: each component is animated independently. return this->bind(abuilder, (*jprop)["x"], &v->x) | this->bind(abuilder, (*jprop)["y"], &v->y); } template <> bool AnimatablePropertyContainer::bind(const AnimationBuilder& abuilder, const skjson::ObjectValue* jprop, Vec2Value* v) { return this->bindAutoOrientable(abuilder, jprop, v, nullptr); } } // namespace skottie::internal