Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qlottieanimationbuilder_p.h"
++
++ #include <private/qquicktimeline_p.h>
++ #include <private/qquicklayeritem_p.h>
++ #include <private/qquickanimationrootitem_p.h>
++
++ #include <QtGui/private/qbezier_p.h>
++ #include <QtQml/qqmlparserstatus.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ Q_STATIC_LOGGING_CATEGORY(lcLottieQtAnimationBuilder, "qt.lottieqt.animationbuilder")
++
++ static void completeParserStatus(QObject *obj)
++ {
++ if (auto *ps = qobject_cast<QQmlParserStatus *>(obj))
++ ps->componentComplete();
++ }
++
++ static int processAnimationTime(int timeMs)
++ {
++ static const qreal multiplier =
++ qEnvironmentVariable("QT_QUICKVECTORIMAGE_TIME_DILATION", QStringLiteral("1.0"))
++ .toDouble();
++ return qRound(multiplier * timeMs);
++ }
++
++ static QEasingCurve easingForFrame(const QQuickAnimatedProperty::PropertyAnimation &anim, int frame,
++ QMap<std::array<qreal, 4>, QEasingCurve> &cache)
++ {
++ QEasingCurve easing;
++ auto it = anim.easingPerFrame.constFind(frame);
++ if (it == anim.easingPerFrame.constEnd())
++ return easing;
++
++ const QBezier &bezier = it.value();
++ const QPointF c1 = bezier.pt2();
++ const QPointF c2 = bezier.pt3();
++ if (c1 == c1.transposed() && c2 == c2.transposed())
++ return easing; // linear
++
++ const std::array<qreal, 4> key{ c1.x(), c1.y(), c2.x(), c2.y() };
++ auto cacheIt = cache.constFind(key);
++ if (cacheIt != cache.constEnd())
++ return cacheIt.value();
++
++ easing.setType(QEasingCurve::BezierSpline);
++ easing.addCubicBezierSegment(c1, c2, QPointF(1, 1));
++ cache.insert(key, easing);
++ return easing;
++ }
++
++ QQuickAbstractAnimation *LottieAnimationBuilder::enterTimelineScope(QQuickItem *item,
++ const StructureNodeInfo &info)
++ {
++ Q_ASSERT(info.timelineInfo);
++ const TimelineInfo &timelineInfo = *info.timelineInfo;
++ QQuickTimeline *timeline = nullptr;
++ QQuickTimelineAnimation *masterAnimation = nullptr;
++ bool ownsTimeline = true;
++ auto *root = qobject_cast<QQuickAnimationRootItem *>(item);
++ QQuickTimeline *referencedTimeline = nullptr;
++
++ if (!m_scopes.isEmpty()) {
++ referencedTimeline = timelineForReference(timelineInfo.frameCounterReference);
++ if (!referencedTimeline) {
++ qCWarning(lcLottieQtAnimationBuilder) << "enterTimelineScope: no timeline for reference"
++ << timelineInfo.frameCounterReference
++ << "- falling back to the enclosing scope";
++ referencedTimeline = m_scopes.top().timeline;
++ }
++ }
++
++ if (m_scopes.isEmpty()) {
++ timeline = new QQuickTimeline(item);
++ timeline->setStartFrame(timelineInfo.startFrame);
++ timeline->setEndFrame(timelineInfo.endFrame);
++
++ masterAnimation = new QQuickTimelineAnimation(item);
++ masterAnimation->setObjectName(u"_qt_frameCounterAnimation"_s);
++ masterAnimation->setTargetObject(timeline);
++ masterAnimation->setProperty(u"currentFrame"_s);
++ masterAnimation->setFrom(timelineInfo.startFrame);
++ masterAnimation->setTo(timelineInfo.endFrame);
++ const int durationMs = int(1000.0 * qAbs(timelineInfo.endFrame - timelineInfo.startFrame)
++ / qMax(timelineInfo.frameRate, 1));
++ masterAnimation->setDuration(processAnimationTime(durationMs));
++ auto animsProp = timeline->animations();
++ animsProp.append(&animsProp, masterAnimation);
++
++ if (root)
++ root->setFrameCounter(timelineInfo.startFrame);
++ else
++ item->setProperty("frameCounter", timelineInfo.startFrame);
++ QObject::connect(timeline, &QQuickTimeline::currentFrameChanged, item,
++ [item, timeline, root]() {
++ if (root)
++ root->setFrameCounter(timeline->currentFrame());
++ else
++ item->setProperty("frameCounter", timeline->currentFrame());
++ });
++ } else if (!timelineInfo.generateFrameCounter) {
++ timeline = referencedTimeline;
++ ownsTimeline = false;
++ } else {
++ QQuickTimeline *parentTimeline = referencedTimeline;
++ timeline = new QQuickTimeline(item);
++ timeline->setStartFrame(timelineInfo.startFrame);
++ timeline->setEndFrame(timelineInfo.endFrame);
++ if (timelineInfo.frameCounterMapper.isAnimated()) {
++ addKeyframeGroup(parentTimeline, timeline, "currentFrame",
++ timelineInfo.frameCounterMapper.animation(0));
++ } else {
++ const qreal offset = timelineInfo.frameCounterOffset;
++ const qreal multiplier = timelineInfo.frameCounterMultiplier
++ ? timelineInfo.frameCounterMultiplier
++ : qreal(1);
++ timeline->setCurrentFrame((parentTimeline->currentFrame() + offset) * multiplier);
++ QObject::connect(parentTimeline, &QQuickTimeline::currentFrameChanged, timeline,
++ [timeline, parentTimeline, offset, multiplier]() {
++ timeline->setCurrentFrame(
++ (parentTimeline->currentFrame() + offset) * multiplier);
++ });
++ }
++ }
++
++ if (timelineInfo.generateVisibility) {
++ auto *visibilityTimeline = referencedTimeline ? referencedTimeline : timeline;
++
++ const qreal startF = timelineInfo.startFrame;
++ const qreal endF = timelineInfo.endFrame;
++ item->setVisible(visibilityTimeline->currentFrame() >= startF
++ && visibilityTimeline->currentFrame() < endF);
++ QObject::connect(visibilityTimeline, &QQuickTimeline::currentFrameChanged, item,
++ [item, visibilityTimeline, startF, endF]() {
++ const qreal v = visibilityTimeline->currentFrame();
++ item->setVisible(v >= startF && v < endF);
++ });
++ }
++
++ m_scopes.push({ timeline, masterAnimation, ownsTimeline, info.id });
++ return masterAnimation;
++ }
++
++ void LottieAnimationBuilder::enterTimelineScope(const QString &scopeId, const QString &referenceId)
++ {
++ if (m_scopes.isEmpty())
++ return;
++
++ QQuickTimeline *timeline = timelineForReference(referenceId);
++ if (!timeline) {
++ qCWarning(lcLottieQtAnimationBuilder)
++ << "enterTimelineScope: no timeline for reference" << referenceId
++ << "- falling back to the enclosing scope for" << scopeId;
++ timeline = m_scopes.top().timeline;
++ }
++
++ m_scopes.push({ timeline, nullptr, false, scopeId });
++ }
++
++ void LottieAnimationBuilder::exitTimelineScope()
++ {
++ if (m_scopes.isEmpty())
++ return;
++
++ const Scope scope = m_scopes.pop();
++ if (!scope.ownsTimeline)
++ return;
++
++ // Enabling the timeline evaluates its keyframes and starting the animation registers it with
++ // the animation timer of the current thread, so this has to happen on the GUI thread
++ deferToGuiThread([timeline = scope.timeline, masterAnimation = scope.masterAnimation]() {
++ completeParserStatus(timeline);
++ timeline->setEnabled(true);
++
++ if (masterAnimation) {
++ completeParserStatus(masterAnimation);
++ masterAnimation->setRunning(true);
++ }
++ });
++ }
++
++ void LottieAnimationBuilder::bindProperty(QObject *target, const QByteArray &propertyName,
++ const QQuickAnimatedProperty &property)
++ {
++ if (m_scopes.isEmpty() || !target || property.animationCount() == 0)
++ return;
++
++ const QString referenceId = property.timelineReferenceId();
++ QQuickTimeline *timeline = timelineForReference(referenceId);
++ if (!timeline) {
++ qCWarning(lcLottieQtAnimationBuilder)
++ << "bindProperty: no timeline for reference" << referenceId
++ << "- falling back to the enclosing scope for" << target << propertyName;
++ timeline = m_scopes.top().timeline;
++ }
++
++ addKeyframeGroup(timeline, target, propertyName, property.animation(0));
++ }
++
++ QQuickTimeline *LottieAnimationBuilder::timelineForReference(const QString &referenceId) const
++ {
++ if (referenceId.isEmpty())
++ return nullptr;
++
++ for (auto it = m_scopes.crbegin(); it != m_scopes.crend(); ++it) {
++ if (it->referenceId == referenceId)
++ return it->timeline;
++ }
++ return nullptr;
++ }
++
++ void LottieAnimationBuilder::addKeyframeGroup(QQuickTimeline *timeline, QObject *target,
++ const QByteArray &propertyName,
++ const QQuickAnimatedProperty::PropertyAnimation &anim)
++ {
++ if (anim.frames.isEmpty())
++ return;
++
++ if (anim.repeatCount != 1 || anim.startOffset
++ || anim.flags != QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd) {
++ qCWarning(lcLottieQtAnimationBuilder)
++ << "addKeyframeGroup: animation feature not implemented in timeline mode, for"
++ << target << propertyName;
++ }
++
++ auto *group = new QQuickKeyframeGroup(timeline);
++ group->setTargetObject(target);
++ group->setProperty(QString::fromUtf8(propertyName));
++
++ auto keyframesProp = group->keyframes();
++ for (auto it = anim.frames.constBegin(); it != anim.frames.constEnd(); ++it) {
++ auto *keyframe = new QQuickKeyframe(group);
++ keyframe->setFrame(it.key());
++ keyframe->setValue(it.value());
++ keyframe->setEasing(easingForFrame(anim, it.key(), m_easingCache));
++ keyframesProp.append(&keyframesProp, keyframe);
++ }
++ // Completing the group resolves the target property through the QML engine
++ deferToGuiThread([timeline, group]() {
++ completeParserStatus(group);
++
++ auto groupsProp = timeline->keyframeGroups();
++ groupsProp.append(&groupsProp, group);
++ });
++ }
++
++ QQuickItem *LottieAnimationBuilder::createCustomItem(const QString &type)
++ {
++ if (type == "LayerItem"_L1)
++ return new QQuickLayerItem();
++ return nullptr;
++ }
++
++ QT_END_NAMESPACE