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

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