| /**************************************************************************** |
| ** |
| ** Copyright (C) 2018 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the lottie-qt module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "lottieanimation.h" |
| |
| #include <QQuickPaintedItem> |
| #include <QJsonDocument> |
| #include <QJsonObject> |
| #include <QJsonArray> |
| #include <QJsonValue> |
| #include <QFile> |
| #include <QPointF> |
| #include <QPainter> |
| #include <QImage> |
| #include <QTimer> |
| #include <QMetaObject> |
| #include <QLoggingCategory> |
| #include <QThread> |
| #include <QQmlFile> |
| #include <math.h> |
| |
| #include <QtBodymovin/private/bmbase_p.h> |
| #include <QtBodymovin/private/bmlayer_p.h> |
| |
| #include "rasterrenderer/batchrenderer.h" |
| #include "rasterrenderer/lottierasterrenderer.h" |
| |
| QT_BEGIN_NAMESPACE |
| |
| Q_LOGGING_CATEGORY(lcLottieQtBodymovinRender, "qt.lottieqt.bodymovin.render"); |
| Q_LOGGING_CATEGORY(lcLottieQtBodymovinParser, "qt.lottieqt.bodymovin.parser"); |
| |
| /*! |
| \qmltype LottieAnimation |
| \inqmlmodule Qt.labs.lottieqt |
| \since 5.13 |
| \inherits Item |
| \brief A Bodymovin player for Qt. |
| |
| The LottieAnimation type shows Bodymovin format files. |
| |
| LottieAnimation is used to load and render Bodymovin files exported |
| from Adobe After Effects. Currently, only subset of the full Bodymovin |
| specification is supported. Most notable deviations are: |
| |
| \list |
| \li Only Shape layer supported |
| \li Only integer frame-mode of a timeline supported |
| (real frame numbers and time are rounded to the nearest integer) |
| \li Expressions are not supported |
| \endlist |
| |
| For the full list of devations, please refer to the file |
| \c unsupported_features.txt in the source code. |
| |
| |
| \section1 Example Usage |
| |
| The following example shows a simple usage of the LottieAnimation type |
| |
| \qml |
| LottieAnimation { |
| loops: 2 |
| quality: LottieAnimation.MediumQuality |
| source: "animation.json" |
| autoPlay: false |
| onStatusChanged: { |
| if (status === LottieAnimation.Ready) { |
| // any acvities needed before |
| // playing starts go here |
| gotoAndPlay(startFrame); |
| } |
| } |
| onFinished: { |
| console.log("Finished playing") |
| } |
| } |
| \endqml |
| |
| \note Changing width or height of the element does not change the size |
| of the animation within. Also, it is not possible to align the the content |
| inside of a \c LottieAnimation element. To achieve this, position the |
| animation inside e.g. an \c Item. |
| |
| \section1 Rendering Performance |
| |
| Internally, the rendered frame data is cached to improve performance. You |
| can control the memory usage by setting the QLOTTIE_RENDER_CACHE_SIZE |
| environment variable (default value is 2). |
| |
| You can monitor the rendering performance by turning on two logging categories: |
| |
| \list |
| \li \c qt.lottieqt.bodymovin.render - Provides information how the animation |
| is rendered |
| \li \c qt.lottieqt.bodymovin.render.thread - Provides information how the |
| rendering process proceeds. |
| \endlist |
| |
| Specifically, you can monitor does the frame cache gets constantly full, or |
| does the rendering process have to wait for frames to become ready. The |
| first case implies that the animation is too complex, and the rendering |
| cannot keep up the pace. Try making the animation simpler, or optimize |
| the QML scene. |
| */ |
| |
| /*! |
| \qmlproperty bool LottieAnimation::autoPlay |
| |
| Defines whether the player will start playing animation automatically after |
| the animation file has been loaded. |
| |
| The default value is \c true. |
| */ |
| |
| /*! |
| \qmlproperty int LottieAnimation::loops |
| |
| This property holds the number of loops the player will repeat. |
| The value \c LottieAnimation.Infinite means that the the player repeats |
| the animation continuously. |
| |
| The default value is \c 1. |
| */ |
| |
| /*! |
| \qmlsignal LottieAnimation::finished() |
| |
| This signal is emitted when the player has finished playing. In case of |
| looping, the signal is emitted when the last loop has been finished. |
| */ |
| |
| LottieAnimation::LottieAnimation(QQuickItem *parent) |
| : QQuickPaintedItem(parent) |
| { |
| m_frameAdvance = new QTimer(this); |
| m_frameAdvance->setInterval(1000 / m_frameRate); |
| m_frameAdvance->setSingleShot(false); |
| connect (m_frameAdvance, &QTimer::timeout, this, &LottieAnimation::renderNextFrame); |
| |
| m_frameRenderThread = BatchRenderer::instance(); |
| |
| qRegisterMetaType<LottieAnimation*>(); |
| } |
| |
| LottieAnimation::~LottieAnimation() |
| { |
| QMetaObject::invokeMethod(m_frameRenderThread, "deregisterAnimator", Q_ARG(LottieAnimation*, this)); |
| } |
| |
| void LottieAnimation::componentComplete() |
| { |
| QQuickPaintedItem::componentComplete(); |
| |
| if (m_source.isValid()) |
| load(); |
| } |
| |
| void LottieAnimation::paint(QPainter *painter) |
| { |
| BMBase* bmTree = m_frameRenderThread->getFrame(this, m_currentFrame); |
| |
| if (!bmTree) { |
| qCDebug(lcLottieQtBodymovinRender) << "LottieAnimation::paint: Got empty element tree." |
| "Cannot draw (Animator:" << static_cast<void*>(this) << ")"; |
| return; |
| } |
| |
| LottieRasterRenderer renderer(painter); |
| |
| qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this) << "Start to paint frame" << m_currentFrame; |
| |
| for (BMBase *elem : bmTree->children()) { |
| if (elem->active(m_currentFrame)) |
| elem->render(renderer); |
| else |
| qCDebug(lcLottieQtBodymovinRender) << "Element '" << elem->name() << "' inactive. No need to paint"; |
| } |
| |
| m_frameRenderThread->frameRendered(this, m_currentFrame); |
| |
| m_currentFrame += m_direction; |
| |
| if (m_currentFrame < m_startFrame || m_currentFrame > m_endFrame) { |
| m_currentLoop += (m_loops > 0 ? 1 : 0); |
| } |
| |
| if ((m_loops - m_currentLoop) != 0) { |
| m_currentFrame = m_currentFrame < m_startFrame ? m_endFrame : |
| m_currentFrame > m_endFrame ? m_startFrame : m_currentFrame; |
| } |
| } |
| |
| /*! |
| \qmlproperty enumeration LottieAnimation::status |
| |
| This property holds the current status of the LottieAnimation element. |
| |
| \value LottieAnimation.Null |
| An initial value that is used when the source is not defined |
| (Default) |
| |
| \value LottieAnimation.Loading |
| The player is loading a Bodymovin file |
| |
| \value LottieAnimation.Ready |
| Loading has finished successfully and the player is ready to play |
| the animation |
| |
| \value LottieAnimation.Error |
| An error occurred while loading the animation |
| |
| For example, you could implement \c onStatusChanged signal |
| handler to monitor progress of loading an animation as follows: |
| |
| \qml |
| LottieAnimation { |
| source: "animation.json" |
| autoPlay: false |
| onStatusChanged: { |
| if (status === LottieAnimation.Ready) |
| start(); |
| } |
| \endqml |
| */ |
| LottieAnimation::Status LottieAnimation::status() const |
| { |
| return m_status; |
| } |
| |
| void LottieAnimation::setStatus(LottieAnimation::Status status) |
| { |
| if (Q_UNLIKELY(m_status == status)) |
| return; |
| |
| m_status = status; |
| emit statusChanged(); |
| } |
| |
| /*! |
| \qmlproperty url LottieAnimation::source |
| |
| The source of the Bodymovin asset that LottieAnimation plays. |
| |
| LottieAnimation can handle any URL scheme supported by Qt. |
| The URL may be absolute, or relative to the URL of the component. |
| |
| Setting the source property starts loading the animation asynchronously. |
| To monitor progress of loading, connect to the \l status change signal. |
| */ |
| QUrl LottieAnimation::source() const |
| { |
| return m_source; |
| } |
| |
| void LottieAnimation::setSource(const QUrl &source) |
| { |
| if (m_source != source) { |
| m_source = source; |
| emit sourceChanged(); |
| |
| if (isComponentComplete()) |
| load(); |
| } |
| } |
| |
| /*! |
| \qmlproperty int LottieAnimation::startFrame |
| \readonly |
| |
| Frame number of the start of the animation. The value |
| is available after the animation has been loaded and |
| ready to play. |
| */ |
| int LottieAnimation::startFrame() const |
| { |
| return m_startFrame; |
| } |
| |
| void LottieAnimation::setStartFrame(int startFrame) |
| { |
| if (Q_UNLIKELY(m_startFrame == startFrame)) |
| return; |
| |
| m_startFrame = startFrame; |
| emit startFrameChanged(); |
| } |
| |
| /*! |
| \qmlproperty int LottieAnimation::endFrame |
| \readonly |
| |
| Frame number of the end of the animation. The value |
| is available after the animation has been loaded and |
| ready to play. |
| */ |
| int LottieAnimation::endFrame() const |
| { |
| return m_endFrame; |
| } |
| |
| void LottieAnimation::setEndFrame(int endFrame) |
| { |
| if (Q_UNLIKELY(m_endFrame == endFrame)) |
| return; |
| |
| m_endFrame = endFrame; |
| emit endFrameChanged(); |
| } |
| |
| int LottieAnimation::currentFrame() const |
| { |
| return m_currentFrame; |
| } |
| |
| /*! |
| \qmlproperty int LottieAnimation::frameRate |
| |
| This property holds the frame rate value of the Bodymovin animation. |
| |
| \c frameRate changes after the asset has been loaded. Changing the |
| frame rate does not have effect before that, as the value defined in the |
| asset overrides the value. To change the frame rate, you can write: |
| |
| \qml |
| LottieAnimation { |
| source: "animation.json" |
| onStatusChanged: { |
| if (status === LottieAnimation.Ready) |
| frameRate = 60; |
| } |
| \endqml |
| */ |
| int LottieAnimation::frameRate() const |
| { |
| return m_frameRate; |
| } |
| |
| void LottieAnimation::setFrameRate(int frameRate) |
| { |
| if (Q_UNLIKELY(m_frameRate == frameRate || frameRate <= 0)) |
| return; |
| |
| m_frameRate = frameRate; |
| emit frameRateChanged(); |
| |
| m_frameAdvance->setInterval(1000 / m_frameRate); |
| } |
| |
| void LottieAnimation::resetFrameRate() |
| { |
| setFrameRate(m_animFrameRate); |
| } |
| |
| /*! |
| \qmlproperty enumeration LottieAnimation::quality |
| |
| Speficies the rendering quality of the bodymovin player. |
| If \c LowQuality is selected the rendering will happen into a frame |
| buffer object, whereas with other options, the rendering will be done |
| onto \c QImage (which in turn will be rendered on the screen). |
| |
| \value LottieAnimation.LowQuality |
| Antialiasing or a smooth pixmap transformation algorithm are not |
| used |
| |
| \value LottieAnimation.MediumQuality |
| Smooth pixmap transformation algorithm is used but no antialiasing |
| (Default) |
| |
| \value LottieAnimation.HighQuality |
| Antialiasing and a smooth pixmap tranformation algorithm are both |
| used |
| */ |
| LottieAnimation::Quality LottieAnimation::quality() const |
| { |
| return m_quality; |
| } |
| |
| void LottieAnimation::setQuality(LottieAnimation::Quality quality) |
| { |
| if (m_quality != quality) { |
| m_quality = quality; |
| if (quality == LowQuality) |
| setRenderTarget(QQuickPaintedItem::FramebufferObject); |
| else |
| setRenderTarget(QQuickPaintedItem::Image); |
| setSmooth(quality != LowQuality); |
| setAntialiasing(quality == HighQuality); |
| emit qualityChanged(); |
| } |
| } |
| |
| void LottieAnimation::reset() |
| { |
| m_currentFrame = m_direction > 0 ? m_startFrame : m_endFrame; |
| m_currentLoop = 0; |
| QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame", |
| Q_ARG(LottieAnimation*, this), |
| Q_ARG(int, m_currentFrame)); |
| } |
| |
| /*! |
| \qmlmethod void LottieAnimation::start() |
| |
| Starts playing the animation from the beginning. |
| */ |
| void LottieAnimation::start() |
| { |
| reset(); |
| m_frameAdvance->start(); |
| } |
| |
| /*! |
| \qmlmethod void LottieAnimation::play() |
| |
| Starts or continues playing from the current position. |
| */ |
| void LottieAnimation::play() |
| { |
| QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame", |
| Q_ARG(LottieAnimation*, this), |
| Q_ARG(int, m_currentFrame)); |
| m_frameAdvance->start(); |
| } |
| |
| /*! |
| \qmlmethod void LottieAnimation::pause() |
| |
| Pauses the playback. |
| */ |
| void LottieAnimation::pause() |
| { |
| m_frameAdvance->stop(); |
| QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame", |
| Q_ARG(LottieAnimation*, this), |
| Q_ARG(int, m_currentFrame)); |
| } |
| |
| /*! |
| \qmlmethod void LottieAnimation::togglePause() |
| |
| Toggles the status of player between playing and paused states. |
| */ |
| void LottieAnimation::togglePause() |
| { |
| if (m_frameAdvance->isActive()) { |
| pause(); |
| } else { |
| play(); |
| } |
| } |
| |
| /*! |
| \qmlmethod void LottieAnimation::stop() |
| |
| Stops the playback and returns to startFrame. |
| */ |
| void LottieAnimation::stop() |
| { |
| m_frameAdvance->stop(); |
| reset(); |
| renderNextFrame(); |
| } |
| |
| /*! |
| \qmlmethod void LottieAnimation::gotoAndPlay(int frame) |
| |
| Plays the asset from the given \a frame. |
| */ |
| void LottieAnimation::gotoAndPlay(int frame) |
| { |
| gotoFrame(frame); |
| m_currentLoop = 0; |
| m_frameAdvance->start(); |
| } |
| |
| /*! |
| \qmlmethod bool LottieAnimation::gotoAndPlay(string frameMarker) |
| |
| Plays the asset from the frame that has a marker with the given \a frameMarker. |
| Returns \c true if the frameMarker was found, \c false otherwise. |
| */ |
| bool LottieAnimation::gotoAndPlay(const QString &frameMarker) |
| { |
| if (m_markers.contains(frameMarker)) { |
| gotoAndPlay(m_markers.value(frameMarker)); |
| return true; |
| } else |
| return false; |
| } |
| |
| /*! |
| \qmlmethod void LottieAnimation::gotoAndStop(int frame) |
| |
| Moves the playhead to the given \a frame and stops. |
| */ |
| void LottieAnimation::gotoAndStop(int frame) |
| { |
| gotoFrame(frame); |
| m_frameAdvance->stop(); |
| renderNextFrame(); |
| } |
| |
| /*! |
| \qmlmethod bool LottieAnimation::gotoAndStop(string frameMarker) |
| |
| Moves the playhead to the given marker and stops. |
| Returns \c true if \a frameMarker was found, \c false otherwise. |
| */ |
| bool LottieAnimation::gotoAndStop(const QString &frameMarker) |
| { |
| if (m_markers.contains(frameMarker)) { |
| gotoAndStop(m_markers.value(frameMarker)); |
| return true; |
| } else |
| return false; |
| } |
| |
| void LottieAnimation::gotoFrame(int frame) |
| { |
| m_currentFrame = qMax(m_startFrame, qMin(frame, m_endFrame)); |
| QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame", |
| Q_ARG(LottieAnimation*, this), |
| Q_ARG(int, m_currentFrame)); |
| } |
| |
| /*! |
| \qmlmethod double LottieAnimation::getDuration(bool inFrames) |
| |
| Returns the duration of the currently playing asset. |
| |
| If a given \a inFrames is \c true, the return value is the duration in |
| number of frames. Otherwise, returns the duration in seconds. |
| */ |
| double LottieAnimation::getDuration(bool inFrames) |
| { |
| return (m_endFrame - m_startFrame) / |
| static_cast<double>(inFrames ? 1 : m_frameRate); |
| } |
| |
| /*! |
| \qmlproperty enumeration LottieAnimation::direction |
| |
| This property holds the direction of rendering. |
| |
| \value LottieAnimation.Forward |
| Forward direction (Default) |
| |
| \value LottieAnimation.Reverse |
| Reverse direction |
| */ |
| LottieAnimation::Direction LottieAnimation::direction() const |
| { |
| return static_cast<Direction>(m_direction); |
| } |
| |
| void LottieAnimation::setDirection(LottieAnimation::Direction direction) |
| { |
| if (Q_UNLIKELY(static_cast<Direction>(m_direction) == direction)) |
| return; |
| |
| m_direction = direction; |
| emit directionChanged(); |
| |
| m_frameRenderThread->gotoFrame(this, m_currentFrame); |
| } |
| |
| void LottieAnimation::load() |
| { |
| setStatus(Loading); |
| |
| m_file.reset(new QQmlFile(qmlEngine(this), m_source)); |
| if (m_file->isLoading()) |
| m_file->connectFinished(this, SLOT(loadFinished())); |
| else |
| loadFinished(); |
| } |
| |
| void LottieAnimation::loadFinished() |
| { |
| if (Q_UNLIKELY(m_file->isError())) { |
| m_file.reset(); |
| setStatus(Error); |
| return; |
| } |
| |
| Q_ASSERT(m_file->isReady()); |
| const QByteArray json = m_file->dataByteArray(); |
| m_file.reset(); |
| |
| if (Q_UNLIKELY(parse(json) == -1)) { |
| setStatus(Error); |
| return; |
| } |
| |
| QMetaObject::invokeMethod(m_frameRenderThread, "registerAnimator", Q_ARG(LottieAnimation*, this)); |
| |
| if (m_autoPlay) |
| start(); |
| |
| m_frameRenderThread->start(); |
| |
| setStatus(Ready); |
| } |
| |
| QByteArray LottieAnimation::jsonSource() const |
| { |
| return m_jsonSource; |
| } |
| |
| void LottieAnimation::renderNextFrame() |
| { |
| if (m_currentFrame >= m_startFrame && m_currentFrame <= m_endFrame) { |
| if (m_frameRenderThread->getFrame(this, m_currentFrame)) { |
| update(); |
| } else if (!m_waitForFrameConn) { |
| qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this) |
| << "Frame cache was empty for frame" << m_currentFrame; |
| m_waitForFrameConn = connect(m_frameRenderThread, &BatchRenderer::frameReady, |
| this, [this](LottieAnimation *target, int frameNumber) { |
| if (target != this) |
| return; |
| qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this) |
| << "Frame ready" << frameNumber; |
| disconnect(m_waitForFrameConn); |
| update(); |
| }); |
| } |
| } else if (m_loops == m_currentLoop) { |
| if ( m_loops != Infinite) |
| m_frameAdvance->stop(); |
| emit finished(); |
| } |
| } |
| |
| int LottieAnimation::parse(QByteArray jsonSource) |
| { |
| m_jsonSource = jsonSource; |
| |
| QJsonParseError error; |
| QJsonDocument doc = QJsonDocument::fromJson(m_jsonSource, &error); |
| if (Q_UNLIKELY(error.error != QJsonParseError::NoError)) { |
| qCWarning(lcLottieQtBodymovinParser) |
| << "JSON parse error:" << error.errorString(); |
| return -1; |
| } |
| |
| QJsonObject rootObj = doc.object(); |
| if (Q_UNLIKELY(rootObj.empty())) |
| return -1; |
| |
| int startFrame = rootObj.value(QLatin1String("ip")).toVariant().toInt(); |
| int endFrame = rootObj.value(QLatin1String("op")).toVariant().toInt(); |
| m_animFrameRate = rootObj.value(QLatin1String("fr")).toVariant().toInt(); |
| m_animWidth = rootObj.value(QLatin1String("w")).toVariant().toReal(); |
| m_animHeight = rootObj.value(QLatin1String("h")).toVariant().toReal(); |
| |
| QJsonArray markerArr = rootObj.value(QLatin1String("markers")).toArray(); |
| QJsonArray::const_iterator markerIt = markerArr.constBegin(); |
| while (markerIt != markerArr.constEnd()) { |
| QString marker = (*markerIt).toObject().value(QLatin1String("cm")).toString(); |
| int frame = (*markerIt).toObject().value(QLatin1String("tm")).toInt(); |
| m_markers.insert(marker, frame); |
| |
| if ((*markerIt).toObject().value(QLatin1String("dr")).toInt()) |
| qCWarning(lcLottieQtBodymovinParser) |
| << "property 'dr' not support in a marker"; |
| ++markerIt; |
| } |
| |
| if (rootObj.value(QLatin1String("assets")).toArray().count()) |
| qCWarning(lcLottieQtBodymovinParser) << "assets not supported"; |
| |
| if (rootObj.value(QLatin1String("chars")).toArray().count()) |
| qCWarning(lcLottieQtBodymovinParser) << "chars not supported"; |
| |
| setWidth(m_animWidth); |
| setHeight(m_animHeight); |
| setStartFrame(startFrame); |
| setEndFrame(endFrame); |
| setFrameRate(m_animFrameRate); |
| |
| return 0; |
| } |
| |
| QT_END_NAMESPACE |