1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#ifndef QLOTTIEPROPERTY_P_H
5#define QLOTTIEPROPERTY_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 <QtLottie/private/qlottieconstants_p.h>
28#include <QtLottie/private/qlottielayer_p.h>
29#include <QtLottie/private/qbeziereasing_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 QBezierEasing easing;
41 qreal valueForProgress(qreal progress) const {
42 return complete ? easing.valueForProgress(progress) : 1;
43 }
44};
45
46template<typename T>
47class QLottieProperty
48{
49public:
50 virtual ~QLottieProperty() = default;
51
52 virtual void construct(const QJsonObject &definition)
53 {
54 if (definition.value(key: QLatin1String("s")).toVariant().toInt())
55 qCInfo(lcLottieQtLottieParser)
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 const bool schemaChanged = keyframes.last().toObject().contains(key: QLatin1String("s"));
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 inline int startFrame() const
99 {
100 return m_startFrame;
101 }
102
103 inline int endFrame() const
104 {
105 return m_endFrame;
106 }
107
108 QList<EasingSegment<T> > easingCurves() const
109 {
110 return m_easingCurves;
111 }
112
113 virtual bool update(int frame)
114 {
115 if (!m_animated)
116 return false;
117
118 int adjustedFrame = qBound(min: m_startFrame, val: frame, max: m_endFrame);
119 if (const EasingSegment<T> *easing = getEasingSegment(frame: adjustedFrame)) {
120 qreal progress;
121 if (easing->endFrame == easing->startFrame)
122 progress = 1;
123 else
124 progress = ((adjustedFrame - easing->startFrame) * 1.0) /
125 (easing->endFrame - easing->startFrame);
126 qreal easedValue = easing->valueForProgress(progress);
127 m_value = easing->startValue + easedValue *
128 ((easing->endValue - easing->startValue));
129 return true;
130 }
131 return false;
132 }
133
134protected:
135 void addEasing(EasingSegment<T>& easing)
136 {
137 if (m_easingCurves.size()) {
138 EasingSegment<T> prevEase = m_easingCurves.last();
139 // The end value has to be hand picked to the
140 // previous easing segment, as the json data does
141 // not contain end values for segments
142 prevEase.endFrame = easing.startFrame - 1;
143 m_easingCurves.replace(m_easingCurves.size() - 1, prevEase);
144 }
145 m_easingCurves.push_back(easing);
146 }
147
148 const EasingSegment<T>* getEasingSegment(int frame)
149 {
150 // TODO: Improve with a faster search algorithm
151 const EasingSegment<T> *easing = m_currentEasing;
152 if (!easing || easing->startFrame < frame ||
153 easing->endFrame > frame) {
154 for (int i=0; i < m_easingCurves.size(); i++) {
155 if (m_easingCurves.at(i).startFrame <= frame &&
156 m_easingCurves.at(i).endFrame >= frame) {
157 m_currentEasing = &m_easingCurves.at(i);
158 break;
159 }
160 }
161 }
162
163 if (!m_currentEasing) {
164 qCWarning(lcLottieQtLottieParser)
165 << "Property is animated but easing cannot be found";
166 }
167 return m_currentEasing;
168 }
169
170 virtual EasingSegment<T> parseKeyframe(const QJsonObject keyframe, bool fromExpression)
171 {
172 Q_UNUSED(fromExpression);
173
174 EasingSegment<T> easing;
175
176 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
177
178 // AE exported Lottie file includes the last
179 // key frame but no other properties.
180 // No need to process in that case
181 if (!keyframe.contains(key: QLatin1String("s")) && !keyframe.contains(key: QLatin1String("e"))) {
182 // In this case start time is the last frame for the property
183 this->m_endFrame = startTime;
184 easing.startFrame = startTime;
185 easing.endFrame = startTime;
186 if (m_easingCurves.length()) {
187 easing.startValue = m_easingCurves.last().endValue;
188 easing.endValue = m_easingCurves.last().endValue;
189 }
190 return easing;
191 }
192
193 if (m_startFrame > startTime)
194 m_startFrame = startTime;
195
196 easing.startValue = getValue(keyframe.value(key: QLatin1String("s")).toArray());
197 easing.endValue = getValue(keyframe.value(key: QLatin1String("e")).toArray());
198 easing.startFrame = startTime;
199
200 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
201 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
202
203 qreal eix = easingIn.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
204 qreal eiy = easingIn.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
205
206 qreal eox = easingOut.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
207 qreal eoy = easingOut.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
208
209 QPointF c1 = QPointF(eox, eoy);
210 QPointF c2 = QPointF(eix, eiy);
211
212 easing.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
213
214 easing.complete = true;
215
216 return easing;
217 }
218
219 virtual EasingSegment<T> parseKeyframe(const QJsonObject keyframe,
220 const QJsonObject nextKeyframe, bool fromExpression)
221 {
222 Q_UNUSED(fromExpression);
223
224 EasingSegment<T> easing;
225
226 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
227
228 if (m_startFrame > startTime)
229 m_startFrame = startTime;
230
231 easing.startValue = getValue(keyframe.value(key: QLatin1String("s")).toArray());
232 easing.endValue = getValue(nextKeyframe.value(key: QLatin1String("s")).toArray());
233 easing.startFrame = startTime;
234
235 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
236 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
237
238 qreal eix = easingIn.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
239 qreal eiy = easingIn.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
240
241 qreal eox = easingOut.value(key: QLatin1String("x")).toArray().at(i: 0).toDouble();
242 qreal eoy = easingOut.value(key: QLatin1String("y")).toArray().at(i: 0).toDouble();
243
244 QPointF c1 = QPointF(eox, eoy);
245 QPointF c2 = QPointF(eix, eiy);
246
247 easing.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
248
249 easing.complete = true;
250
251 return easing;
252 }
253
254 virtual T getValue(const QJsonValue &value)
255 {
256 if (value.isArray())
257 return getValue(value.toArray());
258 else {
259 QVariant val = value.toVariant();
260 if (val.canConvert<T>()) {
261 T t = val.value<T>();
262 return t;
263 }
264 else
265 return T();
266 }
267 }
268
269 virtual T getValue(const QJsonArray &value)
270 {
271 QVariant val = value.at(i: 0).toVariant();
272 if (val.canConvert<T>()) {
273 T t = val.value<T>();
274 return t;
275 }
276 else
277 return T();
278 }
279
280protected:
281 bool m_animated = false;
282 QList<EasingSegment<T>> m_easingCurves;
283 const EasingSegment<T> *m_currentEasing = nullptr;
284 int m_startFrame = INT_MAX;
285 int m_endFrame = 0;
286 T m_value;
287};
288
289
290template <typename T>
291class QLottieProperty2D : public QLottieProperty<T>
292{
293protected:
294 T getValue(const QJsonArray &value) override
295 {
296 if (value.count() > 1)
297 return T(value.at(i: 0).toDouble(),
298 value.at(i: 1).toDouble());
299 else
300 return T();
301 }
302
303 EasingSegment<T> parseKeyframe(const QJsonObject keyframe, bool fromExpression) override
304 {
305 QJsonArray startValues = keyframe.value(key: QLatin1String("s")).toArray();
306 QJsonArray endValues = keyframe.value(key: QLatin1String("e")).toArray();
307 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
308
309 EasingSegment<T> easingCurve;
310 easingCurve.startFrame = startTime;
311
312 // AE exported Lottie file includes the last
313 // key frame but no other properties.
314 // No need to process in that case
315 if (startValues.isEmpty() && endValues.isEmpty()) {
316 // In this case start time is the last frame for the property
317 this->m_endFrame = startTime;
318 easingCurve.startFrame = startTime;
319 easingCurve.endFrame = startTime;
320 if (this->m_easingCurves.length()) {
321 easingCurve.startValue = this->m_easingCurves.last().endValue;
322 easingCurve.endValue = this->m_easingCurves.last().endValue;
323 }
324 return easingCurve;
325 }
326
327 if (this->m_startFrame > startTime)
328 this->m_startFrame = startTime;
329
330 qreal xs, ys, xe, ye;
331 // Keyframes originating from an expression use only scalar values.
332 // They must be expanded for both x and y coordinates
333 if (fromExpression) {
334 xs = startValues.at(i: 0).toDouble();
335 ys = startValues.at(i: 0).toDouble();
336 xe = endValues.at(i: 0).toDouble();
337 ye = endValues.at(i: 0).toDouble();
338 } else {
339 xs = startValues.at(i: 0).toDouble();
340 ys = startValues.at(i: 1).toDouble();
341 xe = endValues.at(i: 0).toDouble();
342 ye = endValues.at(i: 1).toDouble();
343 }
344 T s(xs, ys);
345 T e(xe, ye);
346
347 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
348 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
349
350 easingCurve.startFrame = startTime;
351 easingCurve.startValue = s;
352 easingCurve.endValue = e;
353
354 if (easingIn.value(key: QLatin1String("x")).isArray()) {
355 QJsonArray eixArr = easingIn.value(key: QLatin1String("x")).toArray();
356 QJsonArray eiyArr = easingIn.value(key: QLatin1String("y")).toArray();
357
358 QJsonArray eoxArr = easingOut.value(key: QLatin1String("x")).toArray();
359 QJsonArray eoyArr = easingOut.value(key: QLatin1String("y")).toArray();
360
361 // Doc: "For multi-dimensional animated properties, [x and y] are arrays, with one
362 // element per dimension so you can have different easing curves per dimension."
363 // Not currently supported; the easing curve for the first vector element is used.
364 qreal eix = eixArr.at(i: 0).toDouble();
365 qreal eiy = eiyArr.at(i: 0).toDouble();
366
367 qreal eox = eoxArr.at(i: 0).toDouble();
368 qreal eoy = eoyArr.at(i: 0).toDouble();
369
370 QPointF c1 = QPointF(eox, eoy);
371 QPointF c2 = QPointF(eix, eiy);
372
373 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
374 } else {
375 qreal eix = easingIn.value(key: QLatin1String("x")).toDouble();
376 qreal eiy = easingIn.value(key: QLatin1String("y")).toDouble();
377
378 qreal eox = easingOut.value(key: QLatin1String("x")).toDouble();
379 qreal eoy = easingOut.value(key: QLatin1String("y")).toDouble();
380
381 QPointF c1 = QPointF(eox, eoy);
382 QPointF c2 = QPointF(eix, eiy);
383
384 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
385 }
386
387 easingCurve.complete = true;
388 return easingCurve;
389 }
390
391 EasingSegment<T> parseKeyframe(const QJsonObject keyframe, const QJsonObject nextKeyframe,
392 bool fromExpression) override
393 {
394 QJsonArray startValues = keyframe.value(key: QLatin1String("s")).toArray();
395 QJsonArray endValues = nextKeyframe.value(key: QLatin1String("s")).toArray();
396 int startTime = keyframe.value(key: QLatin1String("t")).toVariant().toInt();
397
398 EasingSegment<T> easingCurve;
399 easingCurve.startFrame = startTime;
400
401 if (this->m_startFrame > startTime)
402 this->m_startFrame = startTime;
403
404 qreal xs, ys, xe, ye;
405 // Keyframes originating from an expression use only scalar values.
406 // They must be expanded for both x and y coordinates
407 if (fromExpression) {
408 xs = startValues.at(i: 0).toDouble();
409 ys = startValues.at(i: 0).toDouble();
410 xe = endValues.at(i: 0).toDouble();
411 ye = endValues.at(i: 0).toDouble();
412 } else {
413 xs = startValues.at(i: 0).toDouble();
414 ys = startValues.at(i: 1).toDouble();
415 xe = endValues.at(i: 0).toDouble();
416 ye = endValues.at(i: 1).toDouble();
417 }
418 T s(xs, ys);
419 T e(xe, ye);
420
421 QJsonObject easingIn = keyframe.value(key: QLatin1String("i")).toObject();
422 QJsonObject easingOut = keyframe.value(key: QLatin1String("o")).toObject();
423
424 easingCurve.startFrame = startTime;
425 easingCurve.startValue = s;
426 easingCurve.endValue = e;
427
428 if (easingIn.value(key: QLatin1String("x")).isArray()) {
429 QJsonArray eixArr = easingIn.value(key: QLatin1String("x")).toArray();
430 QJsonArray eiyArr = easingIn.value(key: QLatin1String("y")).toArray();
431
432 QJsonArray eoxArr = easingOut.value(key: QLatin1String("x")).toArray();
433 QJsonArray eoyArr = easingOut.value(key: QLatin1String("y")).toArray();
434
435 // Doc: "For multi-dimensional animated properties, [x and y] are arrays, with one
436 // element per dimension so you can have different easing curves per dimension."
437 // Not currently supported; the easing curve for the first vector element is used.
438 qreal eix = eixArr.takeAt(i: 0).toDouble();
439 qreal eiy = eiyArr.takeAt(i: 0).toDouble();
440
441 qreal eox =eoxArr.takeAt(i: 0).toDouble();
442 qreal eoy = eoyArr.takeAt(i: 0).toDouble();
443
444 QPointF c1 = QPointF(eox, eoy);
445 QPointF c2 = QPointF(eix, eiy);
446
447 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
448 }
449 else {
450 qreal eix = easingIn.value(key: QLatin1String("x")).toDouble();
451 qreal eiy = easingIn.value(key: QLatin1String("y")).toDouble();
452
453 qreal eox = easingOut.value(key: QLatin1String("x")).toDouble();
454 qreal eoy = easingOut.value(key: QLatin1String("y")).toDouble();
455
456 QPointF c1 = QPointF(eox, eoy);
457 QPointF c2 = QPointF(eix, eiy);
458
459 easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
460 }
461
462 easingCurve.complete = true;
463 return easingCurve;
464 }
465};
466
467template <typename T>
468class QLottieProperty4D : public QLottieProperty<T>
469{
470public:
471 bool update(int frame) override
472 {
473 if (!this->m_animated)
474 return false;
475
476 int adjustedFrame = qBound(this->m_startFrame, frame, this->m_endFrame);
477 if (const EasingSegment<T> *easing = QLottieProperty<T>::getEasingSegment(adjustedFrame)) {
478 qreal progress = ((adjustedFrame - this->m_startFrame) * 1.0) /
479 (this->m_endFrame - this->m_startFrame);
480 qreal easedValue = easing->valueForProgress(progress);
481 // For the time being, 4D vectors are used only for colors, and
482 // the value must be restricted to between [0, 1]
483 easedValue = qBound(min: qreal(0.0), val: easedValue, max: qreal(1.0));
484 T sv = easing->startValue;
485 T ev = easing->endValue;
486 qreal x = sv.x() + easedValue * (ev.x() - sv.x());
487 qreal y = sv.y() + easedValue * (ev.y() - sv.y());
488 qreal z = sv.z() + easedValue * (ev.z() - sv.z());
489 qreal w = sv.w() + easedValue * (ev.w() - sv.w());
490 this->m_value = T(x, y, z, w);
491 }
492
493 return true;
494 }
495
496protected:
497 T getValue(const QJsonArray &value) override
498 {
499 if (value.count() >= 3) {
500 // Assuming color value, so limit values to [0, 1] and default alpha to 1.
501 qreal x = qBound(min: qreal(0), val: value.at(i: 0).toDouble(), max: qreal(1));
502 qreal y = qBound(min: qreal(0), val: value.at(i: 1).toDouble(), max: qreal(1));
503 qreal z = qBound(min: qreal(0), val: value.at(i: 2).toDouble(), max: qreal(1));
504 qreal w = value.count() > 3 ? qBound(min: qreal(0), val: value.at(i: 3).toDouble(), max: qreal(1)) : 1;
505 return T(x, y, z, w);
506 } else {
507 return T();
508 }
509 }
510};
511
512QT_END_NAMESPACE
513
514#endif // QLOTTIEPROPERTY_P_H
515

source code of qtlottie/src/lottie/qlottieproperty_p.h