1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#ifndef BMPROPERTY_P_H
5#define BMPROPERTY_P_H
6
7//
8// W A R N I N G
9// -------------
10//
11// This file is not part of the Qt API. It exists purely as an
12// implementation detail. This header file may change from version to
13// version without notice, or even be removed.
14//
15// We mean it.
16//
17
18#include <QJsonObject>
19#include <QJsonArray>
20#include <QJsonValue>
21#include <QPointF>
22#include <QLoggingCategory>
23#include <QtMath>
24
25#include <QDebug>
26
27#include <QtBodymovin/private/bmconstants_p.h>
28#include <QtBodymovin/private/bmlayer_p.h>
29#include <QtBodymovin/private/beziereasing_p.h>
30
31QT_BEGIN_NAMESPACE
32
33template<typename T>
34struct EasingSegment {
35 bool complete = false;
36 double startFrame = 0;
37 double endFrame = 0;
38 T startValue;
39 T endValue;
40 BezierEasing easing;
41 qreal valueForProgress(qreal progress) const {
42 return complete ? easing.valueForProgress(progress) : 1;
43 }
44};
45
46template<typename T>
47class BODYMOVIN_EXPORT BMProperty
48{
49public:
50 virtual ~BMProperty() = default;
51
52 virtual void construct(const QJsonObject &definition, const QVersionNumber &version)
53 {
54 if (definition.value(key: QLatin1String("s")).toVariant().toInt())
55 qCWarning(lcLottieQtBodymovinParser)
56 << "Property is split into separate x and y but it is not supported";
57
58 bool fromExpression = definition.value(key: QLatin1String("fromExpression")).toBool();
59 m_animated = definition.value(key: QLatin1String("a")).toDouble() > 0;
60 if (m_animated) {
61 QJsonArray keyframes = definition.value(key: QLatin1String("k")).toArray();
62 QJsonArray::const_iterator it = keyframes.constBegin();
63
64 bool schemaChanged = (version >= QVersionNumber(5, 5, 0));
65
66 if (!schemaChanged) {
67 while (it != keyframes.constEnd()) {
68 EasingSegment<T> easing = parseKeyframe((*it).toObject(), fromExpression);
69 addEasing(easing);
70 ++it;
71 }
72 } else {
73 while (it != (keyframes.constEnd() - 1)) {
74 EasingSegment<T> easing =
75 parseKeyframe((*it).toObject(), (*(it + 1)).toObject(), fromExpression);
76 addEasing(easing);
77 ++it;
78 }
79 int lastFrame = (*it).toObject().value(key: QLatin1String("t")).toVariant().toInt();
80 m_easingCurves.last().endFrame = lastFrame;
81 this->m_endFrame = lastFrame;
82 }
83 m_value = T();
84 } else
85 m_value = getValue(definition.value(key: QLatin1String("k")));
86 }
87
88 void setValue(const T& value)
89 {
90 m_value = value;
91 }
92
93 const T& value() const
94 {
95 return m_value;
96 }
97
98 virtual bool update(int frame)
99 {
100 if (!m_animated)
101 return false;
102
103 int adjustedFrame = qBound(min: m_startFrame, val: frame, max: m_endFrame);
104 if (const EasingSegment<T> *easing = getEasingSegment(frame: adjustedFrame)) {
105 qreal progress;
106 if (easing->endFrame == easing->startFrame)
107 progress = 1;
108 else
109 progress = ((adjustedFrame - easing->startFrame) * 1.0) /
110 (easing->endFrame - easing->startFrame);
111 qreal easedValue = easing->valueForProgress(progress);
112 m_value = easing->startValue + easedValue *
113 ((easing->endValue - easing->startValue));
114 return true;
115 }
116 return false;
117 }
118
119protected:
120 void addEasing(EasingSegment<T>& easing)
121 {
122 if (m_easingCurves.size()) {
123 EasingSegment<T> prevEase = m_easingCurves.last();
124 // The end value has to be hand picked to the
125 // previous easing segment, as the json data does
126 // not contain end values for segments
127 prevEase.endFrame = easing.startFrame - 1;
128 m_easingCurves.replace(m_easingCurves.size() - 1, prevEase);
129 }
130 m_easingCurves.push_back(easing);
131 }
132
133 const EasingSegment<T>* getEasingSegment(int frame)
134 {
135 // TODO: Improve with a faster search algorithm
136 const EasingSegment<T> *easing = m_currentEasing;
137 if (!easing || easing->startFrame < frame ||
138 easing->endFrame > frame) {
139 for (int i=0; i < m_easingCurves.size(); i++) {
140 if (m_easingCurves.at(i).startFrame <= frame &&
141 m_easingCurves.at(i).endFrame >= frame) {
142 m_currentEasing = &m_easingCurves.at(i);
143 break;
144 }
145 }
146 }
147
148 if (!m_currentEasing) {
149 qCWarning(lcLottieQtBodymovinParser)
150 << "Property is animated but easing cannot be found";
151 }
152 return m_currentEasing;
153 }
154
155 virtual EasingSegment<T> parseKeyframe(const QJsonObject keyframe, bool fromExpression)
156 {
157 Q_UNUSED(fromExpression);
158
159 EasingSegment<T> easing;
160
161 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
162
163 // AE exported Bodymovin file includes the last
164 // key frame but no other properties.
165 // No need to process in that case
166 if (!keyframe.contains(key: QLatin1String("s")) && !keyframe.contains(key: QLatin1String("e"))) {
167 // In this case start time is the last frame for the property
168 this->m_endFrame = startTime;
169 easing.startFrame = startTime;
170 easing.endFrame = startTime;
171 if (m_easingCurves.length()) {
172 easing.startValue = m_easingCurves.last().endValue;
173 easing.endValue = m_easingCurves.last().endValue;
174 }
175 return easing;
176 }
177
178 if (m_startFrame > startTime)
179 m_startFrame = startTime;
180
181 easing.startValue = getValue(keyframe.value(key: QLatin1String("s")).toArray());
182 easing.endValue = getValue(keyframe.value(key: QLatin1String("e")).toArray());
183 easing.startFrame = startTime;
184
185 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
186 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
187
188 qreal eix = easingIn.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
189 qreal eiy = easingIn.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
190
191 qreal eox = easingOut.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
192 qreal eoy = easingOut.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
193
194 QPointF c1 = QPointF(eox, eoy);
195 QPointF c2 = QPointF(eix, eiy);
196
197 easing.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
198
199 easing.complete = true;
200
201 return easing;
202 }
203
204 virtual EasingSegment<T> parseKeyframe(const QJsonObject keyframe,
205 const QJsonObject nextKeyframe, bool fromExpression)
206 {
207 Q_UNUSED(fromExpression);
208
209 EasingSegment<T> easing;
210
211 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
212
213 if (m_startFrame > startTime)
214 m_startFrame = startTime;
215
216 easing.startValue = getValue(keyframe.value(key: QLatin1String("s")).toArray());
217 easing.endValue = getValue(nextKeyframe.value(key: QLatin1String("s")).toArray());
218 easing.startFrame = startTime;
219
220 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
221 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
222
223 qreal eix = easingIn.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
224 qreal eiy = easingIn.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
225
226 qreal eox = easingOut.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
227 qreal eoy = easingOut.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
228
229 QPointF c1 = QPointF(eox, eoy);
230 QPointF c2 = QPointF(eix, eiy);
231
232 easing.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
233
234 easing.complete = true;
235
236 return easing;
237 }
238
239 virtual T getValue(const QJsonValue &value)
240 {
241 if (value.isArray())
242 return getValue(value.toArray());
243 else {
244 QVariant val = value.toVariant();
245 if (val.canConvert<T>()) {
246 T t = val.value<T>();
247 return t;
248 }
249 else
250 return T();
251 }
252 }
253
254 virtual T getValue(const QJsonArray &value)
255 {
256 QVariant val = value.at(i: 0).toVariant();
257 if (val.canConvert<T>()) {
258 T t = val.value<T>();
259 return t;
260 }
261 else
262 return T();
263 }
264
265protected:
266 bool m_animated = false;
267 QList<EasingSegment<T>> m_easingCurves;
268 const EasingSegment<T> *m_currentEasing = nullptr;
269 int m_startFrame = INT_MAX;
270 int m_endFrame = 0;
271 T m_value;
272};
273
274
275template <typename T>
276class BODYMOVIN_EXPORT BMProperty2D : public BMProperty<T>
277{
278protected:
279 T getValue(const QJsonArray &value) override
280 {
281 if (value.count() > 1)
282 return T(value.at(i: 0).toDouble(),
283 value.at(i: 1).toDouble());
284 else
285 return T();
286 }
287
288 EasingSegment<T> parseKeyframe(const QJsonObject keyframe, bool fromExpression) override
289 {
290 QJsonArray startValues = keyframe.value(key: QLatin1String("s")).toArray();
291 QJsonArray endValues = keyframe.value(key: QLatin1String("e")).toArray();
292 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
293
294 EasingSegment<T> easingCurve;
295 easingCurve.startFrame = startTime;
296
297 // AE exported Bodymovin file includes the last
298 // key frame but no other properties.
299 // No need to process in that case
300 if (startValues.isEmpty() && endValues.isEmpty()) {
301 // In this case start time is the last frame for the property
302 this->m_endFrame = startTime;
303 easingCurve.startFrame = startTime;
304 easingCurve.endFrame = startTime;
305 if (this->m_easingCurves.length()) {
306 easingCurve.startValue = this->m_easingCurves.last().endValue;
307 easingCurve.endValue = this->m_easingCurves.last().endValue;
308 }
309 return easingCurve;
310 }
311
312 if (this->m_startFrame > startTime)
313 this->m_startFrame = startTime;
314
315 qreal xs, ys, xe, ye;
316 // Keyframes originating from an expression use only scalar values.
317 // They must be expanded for both x and y coordinates
318 if (fromExpression) {
319 xs = startValues.at(i: 0).toDouble();
320 ys = startValues.at(i: 0).toDouble();
321 xe = endValues.at(i: 0).toDouble();
322 ye = endValues.at(i: 0).toDouble();
323 } else {
324 xs = startValues.at(i: 0).toDouble();
325 ys = startValues.at(i: 1).toDouble();
326 xe = endValues.at(i: 0).toDouble();
327 ye = endValues.at(i: 1).toDouble();
328 }
329 T s(xs, ys);
330 T e(xe, ye);
331
332 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
333 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
334
335 easingCurve.startFrame = startTime;
336 easingCurve.startValue = s;
337 easingCurve.endValue = e;
338
339 if (easingIn.value(key: QLatin1String("x")).isArray()) {
340 QJsonArray eixArr = easingIn.value(key: QLatin1String("x")).toArray();
341 QJsonArray eiyArr = easingIn.value(key: QLatin1String("y")).toArray();
342
343 QJsonArray eoxArr = easingOut.value(key: QLatin1String("x")).toArray();
344 QJsonArray eoyArr = easingOut.value(key: QLatin1String("y")).toArray();
345
346 while (!eixArr.isEmpty() && !eiyArr.isEmpty()) {
347 qreal eix = eixArr.takeAt(i: 0).toDouble();
348 qreal eiy = eiyArr.takeAt(i: 0).toDouble();
349
350 qreal eox = eoxArr.takeAt(i: 0).toDouble();
351 qreal eoy = eoyArr.takeAt(i: 0).toDouble();
352
353 QPointF c1 = QPointF(eox, eoy);
354 QPointF c2 = QPointF(eix, eiy);
355
356 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
357 }
358 } else {
359 qreal eix = easingIn.value(key: QLatin1String("x")).toDouble();
360 qreal eiy = easingIn.value(key: QLatin1String("y")).toDouble();
361
362 qreal eox = easingOut.value(key: QLatin1String("x")).toDouble();
363 qreal eoy = easingOut.value(key: QLatin1String("y")).toDouble();
364
365 QPointF c1 = QPointF(eox, eoy);
366 QPointF c2 = QPointF(eix, eiy);
367
368 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
369 }
370
371 easingCurve.complete = true;
372 return easingCurve;
373 }
374
375 EasingSegment<T> parseKeyframe(const QJsonObject keyframe, const QJsonObject nextKeyframe,
376 bool fromExpression) override
377 {
378 QJsonArray startValues = keyframe.value(key: QLatin1String("s")).toArray();
379 QJsonArray endValues = nextKeyframe.value(key: QLatin1String("s")).toArray();
380 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
381
382 EasingSegment<T> easingCurve;
383 easingCurve.startFrame = startTime;
384
385 if (this->m_startFrame > startTime)
386 this->m_startFrame = startTime;
387
388 qreal xs, ys, xe, ye;
389 // Keyframes originating from an expression use only scalar values.
390 // They must be expanded for both x and y coordinates
391 if (fromExpression) {
392 xs = startValues.at(i: 0).toDouble();
393 ys = startValues.at(i: 0).toDouble();
394 xe = endValues.at(i: 0).toDouble();
395 ye = endValues.at(i: 0).toDouble();
396 } else {
397 xs = startValues.at(i: 0).toDouble();
398 ys = startValues.at(i: 1).toDouble();
399 xe = endValues.at(i: 0).toDouble();
400 ye = endValues.at(i: 1).toDouble();
401 }
402 T s(xs, ys);
403 T e(xe, ye);
404
405 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
406 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
407
408 easingCurve.startFrame = startTime;
409 easingCurve.startValue = s;
410 easingCurve.endValue = e;
411
412 if (easingIn.value(key: QLatin1String("x")).isArray()) {
413 QJsonArray eixArr = easingIn.value(key: QLatin1String("x")).toArray();
414 QJsonArray eiyArr = easingIn.value(key: QLatin1String("y")).toArray();
415
416 QJsonArray eoxArr = easingOut.value(key: QLatin1String("x")).toArray();
417 QJsonArray eoyArr = easingOut.value(key: QLatin1String("y")).toArray();
418
419 while (!eixArr.isEmpty() && !eiyArr.isEmpty()) {
420 qreal eix = eixArr.takeAt(i: 0).toDouble();
421 qreal eiy = eiyArr.takeAt(i: 0).toDouble();
422
423 qreal eox =eoxArr.takeAt(i: 0).toDouble();
424 qreal eoy = eoyArr.takeAt(i: 0).toDouble();
425
426 QPointF c1 = QPointF(eox, eoy);
427 QPointF c2 = QPointF(eix, eiy);
428
429 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
430 }
431 }
432 else {
433 qreal eix = easingIn.value(key: QLatin1String("x")).toDouble();
434 qreal eiy = easingIn.value(key: QLatin1String("y")).toDouble();
435
436 qreal eox = easingOut.value(key: QLatin1String("x")).toDouble();
437 qreal eoy = easingOut.value(key: QLatin1String("y")).toDouble();
438
439 QPointF c1 = QPointF(eox, eoy);
440 QPointF c2 = QPointF(eix, eiy);
441
442 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
443 }
444
445 easingCurve.complete = true;
446 return easingCurve;
447 }
448};
449
450template <typename T>
451class BODYMOVIN_EXPORT BMProperty4D : public BMProperty<T>
452{
453public:
454 bool update(int frame) override
455 {
456 if (!this->m_animated)
457 return false;
458
459 int adjustedFrame = qBound(this->m_startFrame, frame, this->m_endFrame);
460 if (const EasingSegment<T> *easing = BMProperty<T>::getEasingSegment(adjustedFrame)) {
461 qreal progress = ((adjustedFrame - this->m_startFrame) * 1.0) /
462 (this->m_endFrame - this->m_startFrame);
463 qreal easedValue = easing->valueForProgress(progress);
464 // For the time being, 4D vectors are used only for colors, and
465 // the value must be restricted to between [0, 1]
466 easedValue = qBound(min: qreal(0.0), val: easedValue, max: qreal(1.0));
467 T sv = easing->startValue;
468 T ev = easing->endValue;
469 qreal x = sv.x() + easedValue * (ev.x() - sv.x());
470 qreal y = sv.y() + easedValue * (ev.y() - sv.y());
471 qreal z = sv.z() + easedValue * (ev.z() - sv.z());
472 qreal w = sv.w() + easedValue * (ev.w() - sv.w());
473 this->m_value = T(x, y, z, w);
474 }
475
476 return true;
477 }
478
479protected:
480 T getValue(const QJsonArray &value) override
481 {
482 if (value.count() > 3)
483 return T(value.at(i: 0).toDouble(), value.at(i: 1).toDouble(),
484 value.at(i: 2).toDouble(), value.at(i: 3).toDouble());
485 else
486 return T();
487 }
488};
489
490QT_END_NAMESPACE
491
492#endif // BMPROPERTY_P_H
493

source code of qtlottie/src/bodymovin/bmproperty_p.h