1/*
2 * Copyright 2020 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "include/core/SkContourMeasure.h"
9#include "modules/skottie/src/SkottieJson.h"
10#include "modules/skottie/src/SkottieValue.h"
11#include "modules/skottie/src/animator/Animator.h"
12#include "modules/skottie/src/animator/KeyframeAnimator.h"
13
14#include <cmath>
15
16namespace skottie::internal {
17
18namespace {
19
20// Spatial 2D specialization: stores SkV2s and optional contour interpolators externally.
21class Vec2KeyframeAnimator final : public KeyframeAnimator {
22 struct SpatialValue {
23 Vec2Value v2;
24 sk_sp<SkContourMeasure> cmeasure;
25 };
26
27public:
28 class Builder final : public KeyframeAnimatorBuilder {
29 public:
30 Builder(Vec2Value* vec_target, float* rot_target)
31 : fVecTarget(vec_target)
32 , fRotTarget(rot_target) {}
33
34 sk_sp<KeyframeAnimator> make(const AnimationBuilder& abuilder,
35 const skjson::ArrayValue& jkfs) override {
36 SkASSERT(jkfs.size() > 0);
37
38 fValues.reserve(jkfs.size());
39 if (!this->parseKeyframes(abuilder, jkfs)) {
40 return nullptr;
41 }
42 fValues.shrink_to_fit();
43
44 return sk_sp<Vec2KeyframeAnimator>(
45 new Vec2KeyframeAnimator(std::move(fKFs),
46 std::move(fCMs),
47 std::move(fValues),
48 fVecTarget,
49 fRotTarget));
50 }
51
52 bool parseValue(const AnimationBuilder&, const skjson::Value& jv) const override {
53 return Parse(jv, fVecTarget);
54 }
55
56 private:
57 void backfill_spatial(const SpatialValue& val) {
58 SkASSERT(!fValues.empty());
59 auto& prev_val = fValues.back();
60 SkASSERT(!prev_val.cmeasure);
61
62 if (val.v2 == prev_val.v2) {
63 // spatial interpolation only make sense for noncoincident values
64 return;
65 }
66
67 // Check whether v0 and v1 have the same direction AND ||v0||>=||v1||
68 auto check_vecs = [](const SkV2& v0, const SkV2& v1) {
69 const auto v0_len2 = v0.lengthSquared(),
70 v1_len2 = v1.lengthSquared();
71
72 // check magnitude
73 if (v0_len2 < v1_len2) {
74 return false;
75 }
76
77 // v0, v1 have the same direction iff dot(v0,v1) = ||v0||*||v1||
78 // <=> dot(v0,v1)^2 = ||v0||^2 * ||v1||^2
79 const auto dot = v0.dot(v1);
80 return SkScalarNearlyEqual(dot * dot, v0_len2 * v1_len2);
81 };
82
83 if (check_vecs(val.v2 - prev_val.v2, fTo) &&
84 check_vecs(prev_val.v2 - val.v2, fTi)) {
85 // Both control points lie on the [prev_val..val] segment
86 // => we can power-reduce the Bezier "curve" to a straight line.
87 return;
88 }
89
90 // Finally, this looks like a legitimate spatial keyframe.
91 SkPath p;
92 p.moveTo (prev_val.v2.x , prev_val.v2.y);
93 p.cubicTo(prev_val.v2.x + fTo.x, prev_val.v2.y + fTo.y,
94 val.v2.x + fTi.x, val.v2.y + fTi.y,
95 val.v2.x, val.v2.y);
96 prev_val.cmeasure = SkContourMeasureIter(p, false).next();
97 }
98
99 bool parseKFValue(const AnimationBuilder&,
100 const skjson::ObjectValue& jkf,
101 const skjson::Value& jv,
102 Keyframe::Value* v) override {
103 SpatialValue val;
104 if (!Parse(jv, &val.v2)) {
105 return false;
106 }
107
108 if (fPendingSpatial) {
109 this->backfill_spatial(val);
110 }
111
112 // Track the last keyframe spatial tangents (checked on next parseValue).
113 fTi = ParseDefault<SkV2>(jkf["ti"], {0,0});
114 fTo = ParseDefault<SkV2>(jkf["to"], {0,0});
115 fPendingSpatial = fTi != SkV2{0,0} || fTo != SkV2{0,0};
116
117 if (fValues.empty() || val.v2 != fValues.back().v2 || fPendingSpatial) {
118 fValues.push_back(std::move(val));
119 }
120
121 v->idx = SkToU32(fValues.size() - 1);
122
123 return true;
124 }
125
126 std::vector<SpatialValue> fValues;
127 Vec2Value* fVecTarget; // required
128 float* fRotTarget; // optional
129 SkV2 fTi{0,0},
130 fTo{0,0};
131 bool fPendingSpatial = false;
132 };
133
134private:
135 Vec2KeyframeAnimator(std::vector<Keyframe> kfs, std::vector<SkCubicMap> cms,
136 std::vector<SpatialValue> vs, Vec2Value* vec_target, float* rot_target)
137 : INHERITED(std::move(kfs), std::move(cms))
138 , fValues(std::move(vs))
139 , fVecTarget(vec_target)
140 , fRotTarget(rot_target) {}
141
142 StateChanged update(const Vec2Value& new_vec_value, const Vec2Value& new_tan_value) {
143 auto changed = (new_vec_value != *fVecTarget);
144 *fVecTarget = new_vec_value;
145
146 if (fRotTarget) {
147 const auto new_rot_value = SkRadiansToDegrees(std::atan2(new_tan_value.y,
148 new_tan_value.x));
149 changed |= new_rot_value != *fRotTarget;
150 *fRotTarget = new_rot_value;
151 }
152
153 return changed;
154 }
155
156 StateChanged onSeek(float t) override {
157 auto get_lerp_info = [this](float t) {
158 auto lerp_info = this->getLERPInfo(t);
159
160 // When tracking rotation/orientation, the last keyframe requires special handling:
161 // it doesn't store any spatial information but it is expected to maintain the
162 // previous orientation (per AE semantics).
163 //
164 // The easiest way to achieve this is to actually swap with the previous keyframe,
165 // with an adjusted weight of 1.
166 const auto vidx = lerp_info.vrec0.idx;
167 if (fRotTarget && vidx == fValues.size() - 1 && vidx > 0) {
168 SkASSERT(!fValues[vidx].cmeasure);
169 SkASSERT(lerp_info.vrec1.idx == vidx);
170
171 // Change LERPInfo{0, SIZE - 1, SIZE - 1}
172 // to LERPInfo{1, SIZE - 2, SIZE - 1}
173 lerp_info.weight = 1;
174 lerp_info.vrec0 = {vidx - 1};
175
176 // This yields equivalent lerp results because keyframed values are contiguous
177 // i.e frame[n-1].end_val == frame[n].start_val.
178 }
179
180 return lerp_info;
181 };
182
183 const auto lerp_info = get_lerp_info(t);
184
185 const auto& v0 = fValues[lerp_info.vrec0.idx];
186 if (v0.cmeasure) {
187 // Spatial keyframe: the computed weight is relative to the interpolation path
188 // arc length.
189 SkPoint pos;
190 SkVector tan;
191 if (v0.cmeasure->getPosTan(lerp_info.weight * v0.cmeasure->length(), &pos, &tan)) {
192 return this->update({ pos.fX, pos.fY }, {tan.fX, tan.fY});
193 }
194 }
195
196 const auto& v1 = fValues[lerp_info.vrec1.idx];
197 const auto tan = v1.v2 - v0.v2;
198
199 return this->update(Lerp(v0.v2, v1.v2, lerp_info.weight), tan);
200 }
201
202 const std::vector<SpatialValue> fValues;
203 Vec2Value* fVecTarget;
204 float* fRotTarget;
205
206 using INHERITED = KeyframeAnimator;
207};
208
209} // namespace
210
211bool AnimatablePropertyContainer::bindAutoOrientable(const AnimationBuilder& abuilder,
212 const skjson::ObjectValue* jprop,
213 Vec2Value* v, float* orientation) {
214 if (!jprop) {
215 return false;
216 }
217
218 if (!ParseDefault<bool>((*jprop)["s"], false)) {
219 // Regular (static or keyframed) 2D value.
220 Vec2KeyframeAnimator::Builder builder(v, orientation);
221 return this->bindImpl(abuilder, jprop, builder);
222 }
223
224 // Separate-dimensions vector value: each component is animated independently.
225 return this->bind(abuilder, (*jprop)["x"], &v->x)
226 | this->bind(abuilder, (*jprop)["y"], &v->y);
227}
228
229template <>
230bool AnimatablePropertyContainer::bind<Vec2Value>(const AnimationBuilder& abuilder,
231 const skjson::ObjectValue* jprop,
232 Vec2Value* v) {
233 return this->bindAutoOrientable(abuilder, jprop, v, nullptr);
234}
235
236} // namespace skottie::internal
237