1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "lottieanimation.h"
5
6#include <QQuickPaintedItem>
7#include <QJsonDocument>
8#include <QJsonObject>
9#include <QJsonArray>
10#include <QJsonValue>
11#include <QFile>
12#include <QPointF>
13#include <QPainter>
14#include <QImage>
15#include <QTimer>
16#include <QMetaObject>
17#include <QLoggingCategory>
18#include <QThread>
19#include <QQmlContext>
20#include <QQmlFile>
21#include <math.h>
22
23#include <QtBodymovin/private/bmbase_p.h>
24#include <QtBodymovin/private/bmlayer_p.h>
25
26#include "rasterrenderer/batchrenderer.h"
27#include "rasterrenderer/lottierasterrenderer.h"
28
29QT_BEGIN_NAMESPACE
30
31Q_LOGGING_CATEGORY(lcLottieQtBodymovinRender, "qt.lottieqt.bodymovin.render");
32Q_LOGGING_CATEGORY(lcLottieQtBodymovinParser, "qt.lottieqt.bodymovin.parser");
33
34/*!
35 \qmltype LottieAnimation
36 \inqmlmodule Qt.labs.lottieqt
37 \since 5.13
38 \inherits Item
39 \brief A Bodymovin player for Qt.
40
41 The LottieAnimation type shows Bodymovin format files.
42
43 LottieAnimation is used to load and render Bodymovin files exported
44 from Adobe After Effects. Currently, only subset of the full Bodymovin
45 specification is supported. Most notable deviations are:
46
47 \list
48 \li Only Shape layer supported
49 \li Only integer frame-mode of a timeline supported
50 (real frame numbers and time are rounded to the nearest integer)
51 \li Expressions are not supported
52 \endlist
53
54 For the full list of devations, please see see the \l{Limitations}
55 section.
56
57 \section1 Example Usage
58
59 The following example shows a simple usage of the LottieAnimation type
60
61 \qml
62 LottieAnimation {
63 loops: 2
64 quality: LottieAnimation.MediumQuality
65 source: "animation.json"
66 autoPlay: false
67 onStatusChanged: {
68 if (status === LottieAnimation.Ready) {
69 // any acvities needed before
70 // playing starts go here
71 gotoAndPlay(startFrame);
72 }
73 }
74 onFinished: {
75 console.log("Finished playing")
76 }
77 }
78 \endqml
79
80 \note Changing width or height of the element does not change the size
81 of the animation within. Also, it is not possible to align the the content
82 inside of a \c LottieAnimation element. To achieve this, position the
83 animation inside e.g. an \c Item.
84
85 \section1 Rendering Performance
86
87 Internally, the rendered frame data is cached to improve performance. You
88 can control the memory usage by setting the QLOTTIE_RENDER_CACHE_SIZE
89 environment variable (default value is 2).
90
91 You can monitor the rendering performance by turning on two logging categories:
92
93 \list
94 \li \c qt.lottieqt.bodymovin.render - Provides information how the animation
95 is rendered
96 \li \c qt.lottieqt.bodymovin.render.thread - Provides information how the
97 rendering process proceeds.
98 \endlist
99
100 Specifically, you can monitor does the frame cache gets constantly full, or
101 does the rendering process have to wait for frames to become ready. The
102 first case implies that the animation is too complex, and the rendering
103 cannot keep up the pace. Try making the animation simpler, or optimize
104 the QML scene.
105*/
106
107/*!
108 \qmlproperty bool LottieAnimation::autoPlay
109
110 Defines whether the player will start playing animation automatically after
111 the animation file has been loaded.
112
113 The default value is \c true.
114*/
115
116/*!
117 \qmlproperty int LottieAnimation::loops
118
119 This property holds the number of loops the player will repeat.
120 The value \c LottieAnimation.Infinite means that the the player repeats
121 the animation continuously.
122
123 The default value is \c 1.
124*/
125
126/*!
127 \qmlsignal LottieAnimation::finished()
128
129 This signal is emitted when the player has finished playing. In case of
130 looping, the signal is emitted when the last loop has been finished.
131*/
132
133LottieAnimation::LottieAnimation(QQuickItem *parent)
134 : QQuickPaintedItem(parent)
135{
136 m_frameAdvance = new QTimer(this);
137 m_frameAdvance->setInterval(1000 / m_frameRate);
138 m_frameAdvance->setSingleShot(false);
139 connect (sender: m_frameAdvance, signal: &QTimer::timeout, context: this, slot: &LottieAnimation::renderNextFrame);
140
141 m_frameRenderThread = BatchRenderer::instance();
142
143 qRegisterMetaType<LottieAnimation*>();
144
145 setAntialiasing(m_quality == HighQuality);
146}
147
148LottieAnimation::~LottieAnimation()
149{
150 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "deregisterAnimator", Q_ARG(LottieAnimation*, this));
151}
152
153void LottieAnimation::componentComplete()
154{
155 QQuickPaintedItem::componentComplete();
156
157 if (m_source.isValid())
158 load();
159}
160
161void LottieAnimation::paint(QPainter *painter)
162{
163 BMBase* bmTree = m_frameRenderThread->getFrame(animator: this, frameNumber: m_currentFrame);
164
165 if (!bmTree) {
166 qCDebug(lcLottieQtBodymovinRender) << "LottieAnimation::paint: Got empty element tree."
167 "Cannot draw (Animator:" << static_cast<void*>(this) << ")";
168 return;
169 }
170
171 LottieRasterRenderer renderer(painter);
172
173 qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this) << "Start to paint frame" << m_currentFrame;
174
175 for (BMBase *elem : bmTree->children()) {
176 if (elem->active(frame: m_currentFrame))
177 elem->render(renderer);
178 else
179 qCDebug(lcLottieQtBodymovinRender) << "Element '" << elem->name() << "' inactive. No need to paint";
180 }
181
182 m_frameRenderThread->frameRendered(animator: this, frameNumber: m_currentFrame);
183
184 m_currentFrame += m_direction;
185
186 if (m_currentFrame < m_startFrame || m_currentFrame > m_endFrame) {
187 m_currentLoop += (m_loops > 0 ? 1 : 0);
188 }
189
190 if ((m_loops - m_currentLoop) != 0) {
191 m_currentFrame = m_currentFrame < m_startFrame ? m_endFrame :
192 m_currentFrame > m_endFrame ? m_startFrame : m_currentFrame;
193 }
194}
195
196/*!
197 \qmlproperty enumeration LottieAnimation::status
198
199 This property holds the current status of the LottieAnimation element.
200
201 \value LottieAnimation.Null
202 An initial value that is used when the source is not defined
203 (Default)
204
205 \value LottieAnimation.Loading
206 The player is loading a Bodymovin file
207
208 \value LottieAnimation.Ready
209 Loading has finished successfully and the player is ready to play
210 the animation
211
212 \value LottieAnimation.Error
213 An error occurred while loading the animation
214
215 For example, you could implement \c onStatusChanged signal
216 handler to monitor progress of loading an animation as follows:
217
218 \qml
219 LottieAnimation {
220 source: "animation.json"
221 autoPlay: false
222 onStatusChanged: {
223 if (status === LottieAnimation.Ready)
224 start();
225 }
226 \endqml
227*/
228LottieAnimation::Status LottieAnimation::status() const
229{
230 return m_status;
231}
232
233void LottieAnimation::setStatus(LottieAnimation::Status status)
234{
235 if (Q_UNLIKELY(m_status == status))
236 return;
237
238 m_status = status;
239 emit statusChanged();
240}
241
242/*!
243 \qmlproperty url LottieAnimation::source
244
245 The source of the Bodymovin asset that LottieAnimation plays.
246
247 LottieAnimation can handle any URL scheme supported by Qt.
248 The URL may be absolute, or relative to the URL of the component.
249
250 Setting the source property starts loading the animation asynchronously.
251 To monitor progress of loading, connect to the \l status change signal.
252*/
253QUrl LottieAnimation::source() const
254{
255 return m_source;
256}
257
258void LottieAnimation::setSource(const QUrl &source)
259{
260 if (m_source != source) {
261 m_source = source;
262 emit sourceChanged();
263
264 if (isComponentComplete())
265 load();
266 }
267}
268
269/*!
270 \qmlproperty int LottieAnimation::startFrame
271 \readonly
272
273 Frame number of the start of the animation. The value
274 is available after the animation has been loaded and
275 ready to play.
276*/
277int LottieAnimation::startFrame() const
278{
279 return m_startFrame;
280}
281
282void LottieAnimation::setStartFrame(int startFrame)
283{
284 if (Q_UNLIKELY(m_startFrame == startFrame))
285 return;
286
287 m_startFrame = startFrame;
288 emit startFrameChanged();
289}
290
291/*!
292 \qmlproperty int LottieAnimation::endFrame
293 \readonly
294
295 Frame number of the end of the animation. The value
296 is available after the animation has been loaded and
297 ready to play.
298*/
299int LottieAnimation::endFrame() const
300{
301 return m_endFrame;
302}
303
304void LottieAnimation::setEndFrame(int endFrame)
305{
306 if (Q_UNLIKELY(m_endFrame == endFrame))
307 return;
308
309 m_endFrame = endFrame;
310 emit endFrameChanged();
311}
312
313int LottieAnimation::currentFrame() const
314{
315 return m_currentFrame;
316}
317
318QVersionNumber LottieAnimation::version() const
319{
320 return m_version;
321}
322
323/*!
324 \qmlproperty int LottieAnimation::frameRate
325
326 This property holds the frame rate value of the Bodymovin animation.
327
328 \c frameRate changes after the asset has been loaded. Changing the
329 frame rate does not have effect before that, as the value defined in the
330 asset overrides the value. To change the frame rate, you can write:
331
332 \qml
333 LottieAnimation {
334 source: "animation.json"
335 onStatusChanged: {
336 if (status === LottieAnimation.Ready)
337 frameRate = 60;
338 }
339 \endqml
340*/
341int LottieAnimation::frameRate() const
342{
343 return m_frameRate;
344}
345
346void LottieAnimation::setFrameRate(int frameRate)
347{
348 if (Q_UNLIKELY(m_frameRate == frameRate || frameRate <= 0))
349 return;
350
351 m_frameRate = frameRate;
352 emit frameRateChanged();
353
354 m_frameAdvance->setInterval(1000 / m_frameRate);
355}
356
357void LottieAnimation::resetFrameRate()
358{
359 setFrameRate(m_animFrameRate);
360}
361
362/*!
363 \qmlproperty enumeration LottieAnimation::quality
364
365 Speficies the rendering quality of the bodymovin player.
366 If \c LowQuality is selected the rendering will happen into a frame
367 buffer object, whereas with other options, the rendering will be done
368 onto \c QImage (which in turn will be rendered on the screen).
369
370 \value LottieAnimation.LowQuality
371 Antialiasing or a smooth pixmap transformation algorithm are not
372 used
373
374 \value LottieAnimation.MediumQuality
375 Smooth pixmap transformation algorithm is used but no antialiasing
376 (Default)
377
378 \value LottieAnimation.HighQuality
379 Antialiasing and a smooth pixmap tranformation algorithm are both
380 used
381*/
382LottieAnimation::Quality LottieAnimation::quality() const
383{
384 return m_quality;
385}
386
387void LottieAnimation::setQuality(LottieAnimation::Quality quality)
388{
389 if (m_quality != quality) {
390 m_quality = quality;
391 if (quality == LowQuality)
392 setRenderTarget(QQuickPaintedItem::FramebufferObject);
393 else
394 setRenderTarget(QQuickPaintedItem::Image);
395 setSmooth(quality != LowQuality);
396 setAntialiasing(quality == HighQuality);
397 emit qualityChanged();
398 }
399}
400
401void LottieAnimation::reset()
402{
403 m_currentFrame = m_direction > 0 ? m_startFrame : m_endFrame;
404 m_currentLoop = 0;
405 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
406 Q_ARG(LottieAnimation*, this),
407 Q_ARG(int, m_currentFrame));
408}
409
410/*!
411 \qmlmethod void LottieAnimation::start()
412
413 Starts playing the animation from the beginning.
414*/
415void LottieAnimation::start()
416{
417 reset();
418 m_frameAdvance->start();
419}
420
421/*!
422 \qmlmethod void LottieAnimation::play()
423
424 Starts or continues playing from the current position.
425*/
426void LottieAnimation::play()
427{
428 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
429 Q_ARG(LottieAnimation*, this),
430 Q_ARG(int, m_currentFrame));
431 m_frameAdvance->start();
432}
433
434/*!
435 \qmlmethod void LottieAnimation::pause()
436
437 Pauses the playback.
438*/
439void LottieAnimation::pause()
440{
441 m_frameAdvance->stop();
442 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
443 Q_ARG(LottieAnimation*, this),
444 Q_ARG(int, m_currentFrame));
445}
446
447/*!
448 \qmlmethod void LottieAnimation::togglePause()
449
450 Toggles the status of player between playing and paused states.
451*/
452void LottieAnimation::togglePause()
453{
454 if (m_frameAdvance->isActive()) {
455 pause();
456 } else {
457 play();
458 }
459}
460
461/*!
462 \qmlmethod void LottieAnimation::stop()
463
464 Stops the playback and returns to startFrame.
465*/
466void LottieAnimation::stop()
467{
468 m_frameAdvance->stop();
469 reset();
470 renderNextFrame();
471}
472
473/*!
474 \qmlmethod void LottieAnimation::gotoAndPlay(int frame)
475
476 Plays the asset from the given \a frame.
477*/
478void LottieAnimation::gotoAndPlay(int frame)
479{
480 gotoFrame(frame);
481 m_currentLoop = 0;
482 m_frameAdvance->start();
483}
484
485/*!
486 \qmlmethod bool LottieAnimation::gotoAndPlay(string frameMarker)
487
488 Plays the asset from the frame that has a marker with the given \a frameMarker.
489 Returns \c true if the frameMarker was found, \c false otherwise.
490*/
491bool LottieAnimation::gotoAndPlay(const QString &frameMarker)
492{
493 if (m_markers.contains(key: frameMarker)) {
494 gotoAndPlay(frame: m_markers.value(key: frameMarker));
495 return true;
496 } else
497 return false;
498}
499
500/*!
501 \qmlmethod void LottieAnimation::gotoAndStop(int frame)
502
503 Moves the playhead to the given \a frame and stops.
504*/
505void LottieAnimation::gotoAndStop(int frame)
506{
507 gotoFrame(frame);
508 m_frameAdvance->stop();
509 renderNextFrame();
510}
511
512/*!
513 \qmlmethod bool LottieAnimation::gotoAndStop(string frameMarker)
514
515 Moves the playhead to the given marker and stops.
516 Returns \c true if \a frameMarker was found, \c false otherwise.
517*/
518bool LottieAnimation::gotoAndStop(const QString &frameMarker)
519{
520 if (m_markers.contains(key: frameMarker)) {
521 gotoAndStop(frame: m_markers.value(key: frameMarker));
522 return true;
523 } else
524 return false;
525}
526
527void LottieAnimation::gotoFrame(int frame)
528{
529 m_currentFrame = qMax(a: m_startFrame, b: qMin(a: frame, b: m_endFrame));
530 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "gotoFrame",
531 Q_ARG(LottieAnimation*, this),
532 Q_ARG(int, m_currentFrame));
533}
534
535/*!
536 \qmlmethod double LottieAnimation::getDuration(bool inFrames)
537
538 Returns the duration of the currently playing asset.
539
540 If a given \a inFrames is \c true, the return value is the duration in
541 number of frames. Otherwise, returns the duration in seconds.
542*/
543double LottieAnimation::getDuration(bool inFrames)
544{
545 return (m_endFrame - m_startFrame) /
546 static_cast<double>(inFrames ? 1 : m_frameRate);
547}
548
549/*!
550 \qmlproperty enumeration LottieAnimation::direction
551
552 This property holds the direction of rendering.
553
554 \value LottieAnimation.Forward
555 Forward direction (Default)
556
557 \value LottieAnimation.Reverse
558 Reverse direction
559*/
560LottieAnimation::Direction LottieAnimation::direction() const
561{
562 return static_cast<Direction>(m_direction);
563}
564
565void LottieAnimation::setDirection(LottieAnimation::Direction direction)
566{
567 if (Q_UNLIKELY(static_cast<Direction>(m_direction) == direction))
568 return;
569
570 m_direction = direction;
571 m_currentLoop = 0;
572 emit directionChanged();
573
574 m_frameRenderThread->gotoFrame(animator: this, frame: m_currentFrame);
575}
576
577void LottieAnimation::load()
578{
579 setStatus(Loading);
580
581 const QQmlContext *context = qmlContext(this);
582 const QUrl loadUrl = context ? context->resolvedUrl(m_source) : m_source;
583 m_file.reset(other: new QQmlFile(qmlEngine(this), loadUrl));
584 if (m_file->isLoading())
585 m_file->connectFinished(this, SLOT(loadFinished()));
586 else
587 loadFinished();
588}
589
590void LottieAnimation::loadFinished()
591{
592 if (Q_UNLIKELY(m_file->isError())) {
593 m_file.reset();
594 setStatus(Error);
595 return;
596 }
597
598 Q_ASSERT(m_file->isReady());
599 const QByteArray json = m_file->dataByteArray();
600 m_file.reset();
601
602 if (Q_UNLIKELY(parse(json) == -1)) {
603 setStatus(Error);
604 return;
605 }
606
607 QMetaObject::invokeMethod(obj: m_frameRenderThread, member: "registerAnimator", Q_ARG(LottieAnimation*, this));
608
609 if (m_autoPlay)
610 start();
611
612 m_frameRenderThread->start();
613
614 setStatus(Ready);
615}
616
617QByteArray LottieAnimation::jsonSource() const
618{
619 return m_jsonSource;
620}
621
622void LottieAnimation::renderNextFrame()
623{
624 if (m_currentFrame >= m_startFrame && m_currentFrame <= m_endFrame) {
625 if (m_frameRenderThread->getFrame(animator: this, frameNumber: m_currentFrame)) {
626 update();
627 } else if (!m_waitForFrameConn) {
628 qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this)
629 << "Frame cache was empty for frame" << m_currentFrame;
630 m_waitForFrameConn = connect(sender: m_frameRenderThread, signal: &BatchRenderer::frameReady,
631 context: this, slot: [this](LottieAnimation *target, int frameNumber) {
632 if (target != this)
633 return;
634 qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this)
635 << "Frame ready" << frameNumber;
636 disconnect(m_waitForFrameConn);
637 update();
638 });
639 }
640 } else if (m_loops == m_currentLoop) {
641 if ( m_loops != Infinite)
642 m_frameAdvance->stop();
643 emit finished();
644 }
645}
646
647int LottieAnimation::parse(QByteArray jsonSource)
648{
649 m_jsonSource = jsonSource;
650
651 QJsonParseError error;
652 QJsonDocument doc = QJsonDocument::fromJson(json: m_jsonSource, error: &error);
653 if (Q_UNLIKELY(error.error != QJsonParseError::NoError)) {
654 qCWarning(lcLottieQtBodymovinParser)
655 << "JSON parse error:" << error.errorString();
656 return -1;
657 }
658
659 QJsonObject rootObj = doc.object();
660 if (Q_UNLIKELY(rootObj.empty()))
661 return -1;
662
663 QStringList versionString = rootObj.value(key: QLatin1String("v")).toString().split(sep: u'.');
664 QList<int> version;
665 foreach (QString v, versionString)
666 version.append(t: v.toInt());
667 m_version = QVersionNumber(version);
668
669 int startFrame = rootObj.value(key: QLatin1String("ip")).toVariant().toInt();
670 int endFrame = rootObj.value(key: QLatin1String("op")).toVariant().toInt();
671 m_animFrameRate = rootObj.value(key: QLatin1String("fr")).toVariant().toInt();
672 m_animWidth = rootObj.value(key: QLatin1String("w")).toVariant().toReal();
673 m_animHeight = rootObj.value(key: QLatin1String("h")).toVariant().toReal();
674
675 QJsonArray markerArr = rootObj.value(key: QLatin1String("markers")).toArray();
676 QJsonArray::const_iterator markerIt = markerArr.constBegin();
677 while (markerIt != markerArr.constEnd()) {
678 QString marker = (*markerIt).toObject().value(key: QLatin1String("cm")).toString();
679 int frame = (*markerIt).toObject().value(key: QLatin1String("tm")).toInt();
680 m_markers.insert(key: marker, value: frame);
681
682 if ((*markerIt).toObject().value(key: QLatin1String("dr")).toInt())
683 qCWarning(lcLottieQtBodymovinParser)
684 << "property 'dr' not support in a marker";
685 ++markerIt;
686 }
687
688 if (rootObj.value(key: QLatin1String("chars")).toArray().count())
689 qCWarning(lcLottieQtBodymovinParser) << "chars not supported";
690
691 setWidth(m_animWidth);
692 setHeight(m_animHeight);
693 setStartFrame(startFrame);
694 setEndFrame(endFrame);
695 setFrameRate(m_animFrameRate);
696
697 return 0;
698}
699
700QT_END_NAMESPACE
701

source code of qtlottie/src/imports/lottieanimation.cpp