Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qlottievisitor_p.h"
#include <private/qquickgenerator_p.h>
#include <private/qquicknodeinfo_p.h>
#include <QtLottie/private/qlottieshape_p.h>
#include <QtLottie/private/qlottiestroke_p.h>
#include <QtLottie/private/qlottierect_p.h>
#include <QtLottie/private/qlottiepolystar_p.h>
#include <QtLottie/private/qlottieellipse_p.h>
#include <QtLottie/private/qlottiefreeformshape_p.h>
#include <QtLottie/private/qlottieshapetransform_p.h>
#include <QtLottie/private/qlottiegfill_p.h>
#include <QtLottie/private/qlottieround_p.h>
#include <QtLottie/private/qlottieroot_p.h>
#include <QtLottie/private/qlottieflatlayers_p.h>
#include <QtLottie/private/qlottieprecomplayer_p.h>
#include <QtLottie/private/qlottieimage_p.h>
#include <QtLottie/private/qlottieprecomposition_p.h>
#include <QtLottie/private/qlottietextlayer_p.h>
#include <QtGui/private/qfixed_p.h>
#include <QFile>
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcLottieQtVisitor, "qt.lottieqt.visitor")
using namespace Qt::Literals::StringLiterals;
#define QLOTTIEVISITOR_DEBUG \
qCDebug(lcLottieQtVisitor).noquote().nospace() \
<< QByteArray().fill(' ', m_savedPaintInfos.size() * 4) \
<< ((trimmingState() == Sequential) ? QByteArray("trimmed") : QByteArray{})
QLottieVisitor::QLottieVisitor(QQuickGenerator *generator)
: m_generator(generator)
{
}
void QLottieVisitor::render(const QLottieRoot &root)
{
m_currentFrameCounterIds.push(idForNode(&root));
m_currentFrameLimits.push({ root.startFrame(), root.endFrame() });
StructureNodeInfo info;
fillCommonNodeInfo(&root, &info);
m_frameRate = root.frameRate();
info.size = root.layerSize();
info.viewBox = QRectF(QPointF(0, 0), info.size);
QLOTTIEVISITOR_DEBUG << "[root viewbox=" << info.viewBox << ", frame rate=" << m_frameRate << "]";
info.stage = StructureNodeStage::Start;
info.nodeId = "_q_animation"_L1; // # centralize
TimelineInfo tlInfo;
tlInfo.startFrame = root.startFrame();
tlInfo.endFrame = root.endFrame();
tlInfo.frameRate = m_frameRate;
for (const QLottieRoot::Marker &marker : root.markers()) {
const QString name = scrub(marker.name);
if (!name.isEmpty())
tlInfo.markers.append({ name, marker.frame, marker.duration });
}
info.timelineInfo = tlInfo;
m_generator->generateRootNode(info);
m_precompositions = root.precompositions();
generatePrecompositions();
root.render(*this);
info.stage = StructureNodeStage::End;
m_generator->generateRootNode(info);
m_currentFrameLimits.pop();
m_currentFrameCounterIds.pop();
}
void QLottieVisitor::render(const QLottiePrecomposition &precomp)
{
// Instantiate (inside a PrecompLayer) a precomp that has been defined earlier
StructureNodeInfo info;
fillCommonNodeInfo(&precomp, &info);
info.nodeId = scrub(precomp.refId());
info.defsId = idForNode(m_precompositions.value(precomp.refId()));
if (m_currentBoundsIds.isEmpty())
info.bounds = QRect(QPoint(), precomp.layerSize());
else
info.boundsReferenceId = m_currentBoundsIds.top();
TimelineInfo tlInfo;
tlInfo.frameCounterReference = m_currentFrameCounterIds.top();
tlInfo.generateFrameCounter = true;
info.timelineInfo = tlInfo;
m_generator->generateDefsInstantiationNode(info);
info.stage = StructureNodeStage::End;
m_generator->generateDefsInstantiationNode(info);
}
QString QLottieVisitor::idForNode(const QLottieBase *node)
{
QString idForNull;
QString &id = node ? m_idForNodeId[node] : idForNull;
if (id.isNull())
id = QStringLiteral("_qt_node%1").arg(m_nodeIdCounter++);
return id;
}
QString QLottieVisitor::scrub(const QString &raw)
{
QString res(raw.left(80));
if (!res.isEmpty()) {
constexpr QLatin1StringView legalSymbols("_-.:/[](){}*| "); // No quot. mark or backslash!
qsizetype i = 0;
do {
if (res.at(i).isLetterOrNumber() || legalSymbols.contains(res.at(i)))
i++;
else
res.remove(i, 1);
} while (i < res.size());
}
return res;
}
void QLottieVisitor::fillCommonNodeInfo(const QLottieBase *node,
NodeInfo *info,
const QString &suffix)
{
info->id = idForNode(node) + suffix;
if (node != nullptr) {
info->nodeId = scrub(node->name());
info->typeName = QStringLiteral("Type%1").arg(node->type());
}
info->transform.setTimelineReferenceId(m_currentFrameCounterIds.top());
info->opacity.setTimelineReferenceId(m_currentFrameCounterIds.top());
info->motionPath.setTimelineReferenceId(m_currentFrameCounterIds.top());
}
void QLottieVisitor::fillAnimationNodeInfo(const QLottieBase *node, NodeInfo *info)
{
Q_UNUSED(node);
for (const PaintInfo::TransformAnimationInfo &animInfo : std::as_const(m_currentPaintInfo.transformAnimations)) {
if (!animInfo.frames.isEmpty()) {
const bool hasPaths = !animInfo.motionPath.isEmpty();
QQuickAnimatedProperty::PropertyAnimation animation;
animation.frames = animInfo.frames;
animation.easingPerFrame = animInfo.easingPerFrame;
polishPropertyAnimation(&animation);
if (animInfo.animationType == QTransform::TxTranslate && hasPaths) {
// Default value holds additional parameters
QVariantList params({ QVariant::fromValue(animInfo.motionPath),
QVariant(animInfo.isAutoOrienting) });
info->motionPath.setDefaultValue(params);
info->motionPath.addAnimation(animation);
} else {
animation.subtype = animInfo.animationType;
if (animInfo.animationType == QTransform::TxNone)
info->opacity.addAnimation(animation);
else
info->transform.addAnimation(animation);
}
}
}
}
void QLottieVisitor::saveState()
{
m_savedPaintInfos.append(m_currentPaintInfo);
}
void QLottieVisitor::restoreState()
{
Q_ASSERT(!m_savedPaintInfos.isEmpty());
m_currentPaintInfo = m_savedPaintInfos.takeLast();
}
void QLottieVisitor::generateMatteNode(const QLottieLayer *layer, StructureNodeStage stage)
{
const QString suffix(QStringLiteral("_box"));
if (layer->isMatteLayer()) {
StructureNodeInfo info;
fillCommonNodeInfo(layer, &info, suffix);
if (m_currentBoundsIds.isEmpty())
info.bounds = QRect(QPoint(), layer->layerSize());
else
info.boundsReferenceId = m_currentBoundsIds.top();
info.transformReferenceChildId = idForNode(layer);
if (stage == StructureNodeStage::Start) {
info.stage = StructureNodeStage::Start;
if (!m_generator->generateDefsNode(info))
return;
}
MaskNodeInfo maskInfo;
maskInfo.stage = stage;
fillCommonNodeInfo(layer, &maskInfo, suffix);
maskInfo.nodeId.clear();
maskInfo.typeName.clear();
if (m_currentBoundsIds.isEmpty())
maskInfo.bounds = QRect(QPoint(), layer->layerSize());
else
maskInfo.boundsReferenceId = m_currentBoundsIds.top();
maskInfo.maskRect = maskInfo.bounds;
maskInfo.keepMaskVisible = true;
m_generator->generateMaskNode(maskInfo);
if (stage != StructureNodeStage::Start) {
info.stage = StructureNodeStage::End;
if (!m_generator->generateDefsNode(info))
return;
}
} else if (layer->isUsingMatteLayer() && layer->parent()) {
StructureNodeInfo info;
info.stage = stage;
fillCommonNodeInfo(layer, &info, suffix);
info.nodeId.clear();
info.typeName.clear();
if (m_currentBoundsIds.isEmpty())
info.bounds = QRect(QPoint(), layer->layerSize());
else
info.boundsReferenceId = m_currentBoundsIds.top();
const QLottieLayer::MatteClipMode mode = layer->matteMode();
info.isMaskAlpha = (mode == QLottieLayer::Alpha || mode == QLottieLayer::InvertedAlpha);
info.isMaskInverted = (mode == QLottieLayer::InvertedAlpha || mode == QLottieLayer::InvertedLuminence);
// Find id of matte layer, assume preceding layer
const QList<QLottieBase *> &siblings = layer->parent()->children();
const QLottieBase *prevSibling = siblings.value(siblings.indexOf(layer) - 1);
const QLottieLayer *precedingLayer = QLottieLayer::checkedCast(prevSibling);
if (precedingLayer && precedingLayer->isMatteLayer()) {
info.maskId = idForNode(precedingLayer) + suffix;
m_generator->generateStructureNode(info);
}
QLOTTIEVISITOR_DEBUG << " matte link resolved to layer: " << info.maskId;
}
}
void QLottieVisitor::render(const QLottieLayer &layer)
{
QLOTTIEVISITOR_DEBUG << "[layer '" << layer.name() << "' type " << Qt::hex << layer.type()
<< (layer.isMatteLayer() ? " matte" : "") << "]";
m_currentFrameLimits.push({ layer.startFrame(), layer.endFrame() });
if (layer.isMatteLayer() || layer.isUsingMatteLayer())
generateMatteNode(&layer, StructureNodeStage::Start);
StructureNodeInfo info;
fillCommonNodeInfo(&layer, &info);
info.customItemType = QStringLiteral("LayerItem");
info.stage = StructureNodeStage::Start;
info.transform.setDefaultValue(QVariant::fromValue(m_currentPaintInfo.transform));
info.isDefaultTransform = m_currentPaintInfo.transform.isIdentity();
info.opacity.setDefaultValue(m_currentPaintInfo.opacity);
info.isDefaultOpacity = qFuzzyCompare(m_currentPaintInfo.opacity, 1.0);
if (layer.type() == LOTTIE_LAYER_PRECOMP_IX || layer.type() == LOTTIE_LAYER_SOLID_IX) {
info.bounds = QRectF({}, layer.layerSize());
m_currentBoundsIds.push(info.id);
}
bool setFrameCounterReference = false;
TimelineInfo tlInfo;
tlInfo.startFrame = layer.startFrame();
tlInfo.endFrame = layer.endFrame();
tlInfo.generateVisibility = true;
tlInfo.frameCounterReference = m_currentFrameCounterIds.top();
if (layer.type() == LOTTIE_LAYER_PRECOMP_IX) {
const QLottiePrecompLayer &pcLayer = static_cast<const QLottiePrecompLayer &>(layer);
registerScalarAnimation(&tlInfo.frameCounterMapper, pcLayer.timeRemapProperty(), m_frameRate);
qreal offset = pcLayer.frameOffset();
bool hasMultiplier = pcLayer.timeStretch() != 0 && pcLayer.timeStretch() != 1;
if (tlInfo.frameCounterMapper.isAnimated() || offset || hasMultiplier) {
tlInfo.generateFrameCounter = true;
setFrameCounterReference = true;
if (offset)
tlInfo.frameCounterOffset = -offset;
if (hasMultiplier)
tlInfo.frameCounterMultiplier = qreal(1) / pcLayer.timeStretch();
}
}
info.timelineInfo = tlInfo;
fillAnimationNodeInfo(&layer, &info);
if (layer.hasLinkedLayer() && layer.parent()) {
for (const QLottieBase *sibling : layer.parent()->children()) {
if (auto siblingLayer = QLottieLayer::checkedCast(sibling)) {
if (siblingLayer != &layer && siblingLayer->layerId() == layer.linkedLayerId()) {
info.transformReferenceId = idForNode(siblingLayer);
if (siblingLayer->isMatteLayer())
info.transformReferenceId += QStringLiteral("_box.item");
}
}
}
QLOTTIEVISITOR_DEBUG << " xf link resolved to layer: " << info.transformReferenceId;
}
m_generator->generateStructureNode(info);
m_currentPaintInfo = {};
if (setFrameCounterReference)
m_currentFrameCounterIds.push(info.id);
}
void QLottieVisitor::render(const QLottieSolidLayer &layer)
{
render(static_cast<const QLottieLayer &>(layer));
if (!layer.layerSize().isEmpty()) {
m_currentPaintInfo.fill = layer.color();
QPainterPath layerRect;
layerRect.addRect(QRect(QPoint(), layer.layerSize()));
setPathNodeStaticPath(layerRect);
generatePathNode(nullptr);
}
}
void QLottieVisitor::render(const QLottieTextLayer &layer)
{
QLOTTIEVISITOR_DEBUG << "[text layer '" << layer.name() << "']";
render(static_cast<const QLottieLayer &>(layer));
const QList<QPainterPath> glyphPaths = layer.buildGlyphPaths();
if (glyphPaths.isEmpty())
return;
m_currentPaintInfo.fill = layer.textDocument().fillColor;
m_currentPaintInfo.fillRule = Qt::WindingFill;
StructureNodeInfo info;
fillCommonNodeInfo(&layer, &info, "_glyphs"_L1);
info.stage = StructureNodeStage::Start;
info.isPathContainer = true;
m_generator->generateStructureNode(info);
m_currentStructElements.push(&layer);
for (const QPainterPath &path : glyphPaths) {
setPathNodeStaticPath(path);
generatePathNode(nullptr);
}
info.stage = StructureNodeStage::End;
m_generator->generateStructureNode(info);
m_currentStructElements.pop();
}
void QLottieVisitor::render(const QLottieGroup &group)
{
QLOTTIEVISITOR_DEBUG << "[group '" << group.name() << "' #children " << group.children().size() << "]";
if (group.children().isEmpty())
return;
bool hasPaths = false;
bool hasGroups = false;
for (const QLottieBase *child : group.children()) {
if (child->type() == LOTTIE_SHAPE_GROUP_IX)
hasGroups = true;
else if (child->isPathElement())
hasPaths = true;
}
// There are two cases where we want to generate a structure node for a LottieGroup
// 1) If the group contains concrete shapes, then we want a Shape node to contain the paths
// 2) If the group contains (sub)groups and has a transform. Since the subgroups will have
// their own transforms, this group's transform must be applied first with a structure node
const QLottieBase *groupXf = group.children().first();
if (groupXf->type() == LOTTIE_SHAPE_TRANS_IX) { // Always true in wellformed lottie file
// We must apply the group xf already here, so it will become the structure node's xf.
// If non-identity, m_currentStructElements is used to avoid re-applying it on normal visit.
render(*static_cast<const QLottieShapeTransform *>(groupXf));
}
const bool groupHasTransform = !m_currentPaintInfo.transform.isIdentity()
|| m_currentPaintInfo.transformAnimations.size() > 0;
if (hasPaths || (hasGroups && groupHasTransform)) {
StructureNodeInfo info;
fillCommonNodeInfo(&group, &info);
info.stage = StructureNodeStage::Start;
info.isPathContainer = hasPaths;
info.transform.setDefaultValue(QVariant::fromValue(m_currentPaintInfo.transform));
info.isDefaultTransform = m_currentPaintInfo.transform.isIdentity();
info.opacity.setDefaultValue(m_currentPaintInfo.opacity);
info.isDefaultOpacity = qFuzzyCompare(m_currentPaintInfo.opacity, 1.0);
fillAnimationNodeInfo(&group, &info);
m_generator->generateStructureNode(info);
m_currentStructElements.push(&group);
m_currentPaintInfo.transform.reset();
m_currentPaintInfo.transformAnimations.clear();
m_currentPaintInfo.opacity = 1.0;
}
}
void QLottieVisitor::finish(const QLottieLayer &layer)
{
QLOTTIEVISITOR_DEBUG << "[layer '" << layer.name() << "' finish]";
if (layer.type() == LOTTIE_LAYER_PRECOMP_IX) {
if (m_currentFrameCounterIds.top() == idForNode(&layer))
m_currentFrameCounterIds.pop();
}
StructureNodeInfo info;
info.stage = StructureNodeStage::End;
fillCommonNodeInfo(&layer, &info);
m_generator->generateStructureNode(info);
if (!m_currentBoundsIds.isEmpty() && m_currentBoundsIds.top() == idForNode(&layer))
m_currentBoundsIds.pop();
if (layer.isMatteLayer() || layer.isUsingMatteLayer())
generateMatteNode(&layer, StructureNodeStage::End);
m_currentFrameLimits.pop();
}
void QLottieVisitor::finish(const QLottieGroup &group)
{
QLOTTIEVISITOR_DEBUG << "[group '" << group.name() << "' finish]";
if (!m_currentStructElements.isEmpty() && m_currentStructElements.top() == &group) {
bool hasPaths = false;
for (const QLottieBase *child : group.children()) {
if (child->isPathElement()) {
hasPaths = true;
break;
}
}
StructureNodeInfo info;
info.stage = StructureNodeStage::End;
info.isPathContainer = hasPaths;
fillCommonNodeInfo(&group, &info);
m_generator->generateStructureNode(info);
m_currentStructElements.pop();
}
}
// Actual shape rendering handled by renderPathElements() instead:
void QLottieVisitor::render(const QLottieRect &rect)
{
QLOTTIEVISITOR_DEBUG << "[rect]";
Q_UNUSED(rect);
}
void QLottieVisitor::render(const QLottieEllipse &ellipse)
{
QLOTTIEVISITOR_DEBUG << "[ellipse]";
Q_UNUSED(ellipse);
}
void QLottieVisitor::render(const QLottiePolyStar &star)
{
QLOTTIEVISITOR_DEBUG << "[star]";
Q_UNUSED(star);
}
void QLottieVisitor::render(const QLottieFreeFormShape &shape)
{
QLOTTIEVISITOR_DEBUG << "[freeform]";
Q_UNUSED(shape);
}
void QLottieVisitor::render(const QLottieRound &round)
{
QLOTTIEVISITOR_DEBUG << "[round]";
// ### Not implemented: path rounding modifier
Q_UNUSED(round);
}
void QLottieVisitor::render(const QLottieFill &fill)
{
QLOTTIEVISITOR_DEBUG << "[fill color=" << fill.color() << ", opacity=" << fill.opacity() << "]";
QColor color = fill.color();
color.setAlphaF(color.alphaF() * (fill.opacity() / 100.0));
m_currentPaintInfo.fill = color;
m_currentPaintInfo.fillColorAnimation = makeColorAnimation(fill.colorProperty());
m_currentPaintInfo.fillOpacityAnimation = makeScalarAnimation(fill.opacityProperty(), qreal(1) / 100);
m_currentPaintInfo.fillRule = fill.fillRule();
m_currentPaintInfo.inverseFillTransform.reset();
}
void QLottieVisitor::render(const QLottieGFill &gradient)
{
QLOTTIEVISITOR_DEBUG << "[fill gradient]";
if (gradient.value() != nullptr)
m_currentPaintInfo.fill = *gradient.value();
m_currentPaintInfo.fillRule = gradient.fillRule();
m_currentPaintInfo.inverseFillTransform.reset();
}
void QLottieVisitor::render(const QLottieImage &image)
{
QLOTTIEVISITOR_DEBUG << "[image size=" << image.size() << "]";
ImageNodeInfo info;
fillCommonNodeInfo(&image, &info);
info.image = image.image();
info.rect = QRectF(QPointF(), image.size());
info.externalFileReference = image.url().toLocalFile();
m_generator->generateImageNode(info);
}
void QLottieVisitor::render(const QLottieStroke &stroke)
{
QLOTTIEVISITOR_DEBUG << "[stroke color=" << stroke.pen().color()
<< ", opacity=" << stroke.opacity() << "]";
const QPen pen = stroke.pen();
m_currentPaintInfo.stroke = pen;
if (m_currentPaintInfo.stroke.brush().gradient() == nullptr) {
QColor color = pen.color();
color.setAlphaF(color.alphaF() * (stroke.opacity() / 100.0));
m_currentPaintInfo.stroke.setColor(color);
}
m_currentPaintInfo.strokeColorAnimation = makeColorAnimation(stroke.colorProperty());
m_currentPaintInfo.strokeOpacityAnimation = makeScalarAnimation(stroke.opacityProperty(), qreal(1) / 100);
m_currentPaintInfo.strokeWidthAnimation = makeScalarAnimation(stroke.widthProperty());
m_currentPaintInfo.strokeDashOffsetAnimation = makeScalarAnimation(stroke.dashOffsetProperty(), qreal(1) / pen.widthF());
}
void QLottieVisitor::render(const QLottieBasicTransform &transform)
{
QLOTTIEVISITOR_DEBUG << "[basic transform s=" << transform.scale()
<< ", r=" << transform.rotation()
<< ", o=" << transform.opacity() << "]";
const bool isShapeTransform = false;
bool autoOrient = false;
if (const QLottieLayer *layer = QLottieLayer::checkedCast(transform.parent()))
autoOrient = layer->isAutoOrienting();
if (hasAnimations(&transform))
collectTransformAnimations(&transform, isShapeTransform, autoOrient);
else
applyTransform(&m_currentPaintInfo.transform, transform, false);
m_currentPaintInfo.opacity *= transform.opacity();
}
namespace {
template<typename T>
QLottieVisitor::PaintInfo::TransformAnimationInfo
collectAnimations(const T &property,
QTransform::TransformationType type,
std::function<void(qreal,
const QVariant &,
QLottieVisitor::PaintInfo::TransformAnimationInfo *,
std::optional<QBezier>)> storeAnimationFrame,
std::function<QVariantList(const QVariant &)> createParams)
{
const auto easingCurves = property.easingCurves();
QLottieVisitor::PaintInfo::TransformAnimationInfo info;
info.animationType = type;
if (easingCurves.isEmpty()) {
const QVariantList params = createParams(QVariant::fromValue(property.value()));
storeAnimationFrame(0, params, &info, std::nullopt);
} else {
std::optional<QBezier> easingBezier;
for (const auto &curve : easingCurves) {
const auto value = curve.startValue;
const QVariantList params = createParams(QVariant::fromValue(value));
storeAnimationFrame(curve.startFrame, params, &info, easingBezier);
easingBezier = curve.easing.bezier(); // belongs to generator's next keyframe
}
}
return info;
}
}
void QLottieVisitor::collectTransformAnimations(const QLottieBasicTransform *transform,
bool isShapeTransform, bool autoOrient)
{
Q_UNUSED(isShapeTransform);
const QLottieProperty<QPointF> anchorPoints = transform->anchorPointProperty();
const QLottieProperty<qreal> rotations = transform->rotationProperty();
const QLottieProperty<QPointF> scales = transform->scaleProperty();
const QLottieSpatialProperty positions = transform->positionProperty();
const QLottieProperty<qreal> opacities = transform->opacityProperty();
const QLottieProperty<qreal> xPositions = transform->xPosProperty();
const QLottieProperty<qreal> yPositions = transform->yPosProperty();
const bool positionHasCurves = positions.hasCurves();
auto storeAnimationFrame = [&](qreal lottieFrameNumber,
const QVariant &propertyValue,
PaintInfo::TransformAnimationInfo *info,
std::optional<QBezier> easingBezier = std::nullopt) {
const int timePoint = qRound(lottieFrameNumber);
info->frames[timePoint] = propertyValue;
if (easingBezier)
info->easingPerFrame[timePoint] = *easingBezier;
};
QLottieVisitor::PaintInfo::TransformAnimationInfo info;
if (!transform->splitPosition()) {
if (positionHasCurves || autoOrient) { // Use the motionPath property
const auto easingCurves = positions.easingCurves();
const auto &pathSegments = positions.subPaths();
Q_ASSERT(!pathSegments.isEmpty());
// Combine motion segments to one path; calculate segment lengths for progress steps
QPainterPath combinedPath;
QList<qreal> segmentLengths;
segmentLengths.reserve(pathSegments.size());
qreal totalLength = 0;
for (const QPainterPath &pathSegment : pathSegments) {
segmentLengths.append(pathSegment.length());
totalLength += segmentLengths.last();
combinedPath.connectPath(pathSegment);
}
QLottieVisitor::PaintInfo::TransformAnimationInfo info;
info.animationType = QTransform::TxTranslate;
info.motionPath = combinedPath;
info.isAutoOrienting = autoOrient;
qreal accumLength = 0;
std::optional<QBezier> easingBezier;
for (qsizetype i = 0; i < easingCurves.size(); ++i) {
const auto &curve = easingCurves.at(i);
qreal tValue = accumLength / totalLength;
storeAnimationFrame(curve.startFrame, tValue, &info, easingBezier);
accumLength += segmentLengths[i];
easingBezier = curve.easing.bezier(); // belongs to generator's next keyframe
}
m_currentPaintInfo.transformAnimations.append(info);
} else {
info = collectAnimations(positions,
QTransform::TxTranslate,
storeAnimationFrame,
[](const QVariant &v)
{
return QVariantList{ v };
});
}
m_currentPaintInfo.transformAnimations.append(info);
} else {
info = collectAnimations(xPositions,
QTransform::TxTranslate,
storeAnimationFrame,
[](const QVariant &v)
{
return QVariantList{ QVariant::fromValue(QPointF(v.toReal(), 0.0)) };
});
m_currentPaintInfo.transformAnimations.append(info);
info = collectAnimations(yPositions,
QTransform::TxTranslate,
storeAnimationFrame,
[](const QVariant &v)
{
return QVariantList{ QVariant::fromValue(QPointF(0.0, v.toReal())) };
});
m_currentPaintInfo.transformAnimations.append(info);
}
auto storeRotationParameter = [](const QVariant &v) {
return QVariantList{ QVariant::fromValue(QPointF(0, 0)), v };
};
info = collectAnimations(rotations,
QTransform::TxRotate,
storeAnimationFrame,
storeRotationParameter);
m_currentPaintInfo.transformAnimations.append(info);
if (isShapeTransform) {
const QLottieShapeTransform *shapeTransform =
static_cast<const QLottieShapeTransform *>(transform);
const QLottieProperty<qreal> skews = shapeTransform->skewProperty();
const QLottieProperty<qreal> skewAxes = shapeTransform->skewAxisProperty();
// Lottie shear transforms work by first rotating by skew axis angle, then applying
// the skewAngle as the shear along the X-axis, and then rotating back.
info = collectAnimations(skewAxes,
QTransform::TxRotate,
storeAnimationFrame,
[](const QVariant &v) {
return QVariantList{ QVariant::fromValue(QPointF(0, 0)),
QVariant::fromValue(-1.0 * v.toReal()) };
});
m_currentPaintInfo.transformAnimations.append(info);
info = collectAnimations(skews,
QTransform::TxShear,
storeAnimationFrame,
[](const QVariant &v) {
return QVariantList{ QVariant::fromValue(QPointF(-1.0 * v.toReal(), 0.0)) };
});
m_currentPaintInfo.transformAnimations.append(info);
info = collectAnimations(skewAxes,
QTransform::TxRotate,
storeAnimationFrame,
storeRotationParameter);
m_currentPaintInfo.transformAnimations.append(info);
}
info = collectAnimations(scales,
QTransform::TxScale,
storeAnimationFrame,
[](const QVariant &v) {
return QVariantList{ QVariant::fromValue(v.toPointF() / 100.0) };
});
m_currentPaintInfo.transformAnimations.append(info);
info = collectAnimations(anchorPoints,
QTransform::TxTranslate,
storeAnimationFrame,
[](const QVariant &v) {
return QVariantList{ QVariant::fromValue(v.toPointF() * -1.0) };
});
m_currentPaintInfo.transformAnimations.append(info);
{
const QList<EasingSegment<qreal> > easingCurves = opacities.easingCurves();
PaintInfo::TransformAnimationInfo info;
info.animationType = QTransform::TxNone;
std::optional<QBezier> easingBezier;
for (const auto &curve : easingCurves) {
const auto value = curve.startValue / 100;
const QVariant params = QVariant::fromValue(value);
storeAnimationFrame(curve.startFrame, params, &info, easingBezier);
easingBezier = curve.easing.bezier(); // For next keyframe
}
m_currentPaintInfo.transformAnimations.append(info);
}
}
void QLottieVisitor::registerScalarAnimation(QQuickAnimatedProperty *outProperty,
const QLottieProperty<qreal> &inProperty,
qreal scale)
{
if (outProperty->animationCount() > 0)
return; // Animation already registered on earlier visit
QQuickAnimatedProperty::PropertyAnimation animation = makeScalarAnimation(inProperty, scale);
if (!animation.frames.isEmpty()) {
outProperty->addAnimation(animation);
outProperty->setTimelineReferenceId(m_currentFrameCounterIds.top());
}
}
QQuickAnimatedProperty::PropertyAnimation QLottieVisitor::makeColorAnimation(const QLottieProperty4D<QVector4D> &colorProperty)
{
QQuickAnimatedProperty::PropertyAnimation animation;
std::optional<QBezier> easingBezier;
for (const auto &curve : colorProperty.easingCurves()) {
const QVector4D raw = curve.startValue;
QColor value = QColor::fromRgbF(raw.x(), raw.y(), raw.z(), raw.w());
int frameTime = qRound(curve.startFrame);
animation.frames[frameTime] = value;
if (easingBezier)
animation.easingPerFrame[frameTime] = *easingBezier;
easingBezier = curve.easing.bezier(); // For next keyframe
}
polishPropertyAnimation(&animation);
return animation;
}
QQuickAnimatedProperty::PropertyAnimation QLottieVisitor::makeScalarAnimation(
const QLottieProperty<qreal> &scalarProperty, qreal scale)
{
QQuickAnimatedProperty::PropertyAnimation animation;
std::optional<QBezier> easingBezier;
for (const auto &curve : scalarProperty.easingCurves()) {
qreal value = curve.startValue * scale;
int frameTime = qRound(curve.startFrame);
animation.frames[frameTime] = value;
if (easingBezier)
animation.easingPerFrame[frameTime] = *easingBezier;
easingBezier = curve.easing.bezier(); // For next keyframe
}
polishPropertyAnimation(&animation);
return animation;
}
void QLottieVisitor::polishPropertyAnimation(QQuickAnimatedProperty::PropertyAnimation *animation)
{
animation->flags |= QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd;
// Add boundary keyframe(s) as necessary to ensure correct behavior of generated code
if (animation && !animation->frames.isEmpty()) {
const int layerStartFrame = m_currentFrameLimits.top().first;
if (animation->frames.firstKey() > layerStartFrame)
animation->frames[layerStartFrame] = animation->frames.first();
}
}
bool QLottieVisitor::hasAnimations(const QLottieBasicTransform *transform, bool isShapeTransform)
{
bool hasAnimations =
transform->rotationProperty().startFrame() < transform->rotationProperty().endFrame()
|| transform->positionProperty().startFrame() < transform->positionProperty().endFrame()
|| transform->scaleProperty().startFrame() < transform->scaleProperty().endFrame()
|| transform->opacityProperty().startFrame() < transform->opacityProperty().endFrame()
|| transform->anchorPointProperty().startFrame()
< transform->anchorPointProperty().endFrame();
if (transform->splitPosition() && !hasAnimations) {
hasAnimations = transform->xPosProperty().startFrame() < transform->xPosProperty().endFrame()
|| transform->yPosProperty().startFrame() < transform->yPosProperty().endFrame();
}
if (isShapeTransform && !hasAnimations) {
const QLottieShapeTransform *shapeTransform = static_cast<const QLottieShapeTransform *>(transform);
hasAnimations = shapeTransform->skewProperty().startFrame() < shapeTransform->skewProperty().endFrame()
|| shapeTransform->skewAxisProperty().startFrame() < shapeTransform->skewAxisProperty().endFrame();
}
if (qEnvironmentVariableIntValue("QLOTTIEVISITOR_DISABLE_ANIMATIONS"))
return false;
return hasAnimations;
}
void QLottieVisitor::render(const QLottieShapeTransform &transform)
{
if (!m_currentStructElements.isEmpty() && transform.parent() == m_currentStructElements.top()) {
// This transform was already applied as part of a group structure node
return;
}
QLOTTIEVISITOR_DEBUG << "[shape transform p=" << transform.position()
<< ", s=" << transform.scale()
<< ", r=" << transform.rotation()
<< ", o=" << transform.opacity() << "]";
if (hasAnimations(&transform, true))
collectTransformAnimations(&transform, true);
else
applyTransform(&m_currentPaintInfo.transform, transform, true);
// A gradient fill defined in enclosing group scope must have its coords mapped back
if (m_currentPaintInfo.fill.gradient())
applyTransform(&m_currentPaintInfo.inverseFillTransform, transform, true);
m_currentPaintInfo.opacity *= transform.opacity();
}
void QLottieVisitor::render(const QLottieTrimPath &trim)
{
QLOTTIEVISITOR_DEBUG << "[trim, isParallel: " << trim.isParallel() << "]";
m_currentPaintInfo.trim.enabled = true;
m_currentPaintInfo.trim.start.setDefaultValue(trim.start() / 100.0);
m_currentPaintInfo.trim.end.setDefaultValue(trim.end() / 100.0);
m_currentPaintInfo.trim.offset.setDefaultValue(trim.offset() / 360.0);
registerScalarAnimation(&m_currentPaintInfo.trim.start, trim.startProperty(), 1.0 / 100.0);
registerScalarAnimation(&m_currentPaintInfo.trim.end, trim.endProperty(), 1.0 / 100.0);
registerScalarAnimation(&m_currentPaintInfo.trim.offset, trim.offsetProperty(), 1.0 / 360.0);
if (!trim.isParallel())
generatePathNode(&trim);
}
void QLottieVisitor::render(const QLottieFillEffect &effect)
{
QLOTTIEVISITOR_DEBUG << "[effect]";
// ### What are you?
Q_UNUSED(effect);
}
void QLottieVisitor::render(const QLottieRepeater &repeater)
{
QLOTTIEVISITOR_DEBUG << "[repeater]";
// ### Repeats the following shapes N times with different transforms
Q_UNUSED(repeater);
}
void QLottieVisitor::fillPathAnimationInfo(const QList<QLottieBase *> &pathElements)
{
QList<int> keyFrameNumbers;
QList<int> firstAnimNumbers;
QList<QBezier> firstAnimEasings;
auto registerAnimation = [&](const auto &property) {
if (!property.isAnimated())
return;
const bool isFirstAnim = firstAnimNumbers.isEmpty();
for (const auto &curve : property.easingCurves()) {
const int timePoint = qRound(curve.startFrame);
keyFrameNumbers.append(timePoint);
if (isFirstAnim) {
firstAnimNumbers.append(timePoint);
firstAnimEasings.append(curve.easing.bezier());
}
}
};
QList<QLottieShape *> elementCopies;
elementCopies.reserve(pathElements.size());
for (const QLottieBase *element : pathElements) {
switch (element->type()) {
case LOTTIE_SHAPE_SHAPE_IX: {
const auto *ffShape = static_cast<const QLottieFreeFormShape *>(element);
if (ffShape->isAnimated()) {
registerAnimation(ffShape->startPointProperty());
}
elementCopies.append(new QLottieFreeFormShape(*ffShape));
break;
}
case LOTTIE_SHAPE_RECT_IX: {
const auto *rect = static_cast<const QLottieRect *>(element);
if (rect->isAnimated()) {
registerAnimation(rect->sizeProperty());
registerAnimation(rect->positionProperty());
}
elementCopies.append(new QLottieRect(*rect));
break;
}
case LOTTIE_SHAPE_ELLIPSE_IX: {
const auto *ellipse = static_cast<const QLottieEllipse *>(element);
if (ellipse->isAnimated()) {
registerAnimation(ellipse->sizeProperty());
registerAnimation(ellipse->positionProperty());
}
elementCopies.append(new QLottieEllipse(*ellipse));
break;
}
case LOTTIE_SHAPE_STAR_IX: {
const auto *star = static_cast<const QLottiePolyStar *>(element);
if (star->isAnimated()) {
registerAnimation(star->outerRadiusProperty());
registerAnimation(star->innerRadiusProperty());
registerAnimation(star->positionProperty());
}
elementCopies.append(new QLottiePolyStar(*star));
break;
}
default:
continue;
}
}
std::sort(keyFrameNumbers.begin(), keyFrameNumbers.end());
keyFrameNumbers.erase(std::unique(keyFrameNumbers.begin(), keyFrameNumbers.end()),
keyFrameNumbers.end());
QQuickAnimatedProperty::PropertyAnimation pa;
for (int frame : std::as_const(keyFrameNumbers)) {
QPainterPath path;
for (QLottieShape *shape : elementCopies) {
if (shape->isAnimated())
shape->updateProperties(frame);
QPainterPath elementPath = shape->path();
if (elementPath.isEmpty())
elementPath = shape->fallbackPath();
path.addPath(elementPath);
}
pa.frames[frame] = QVariant::fromValue(path);
}
qDeleteAll(elementCopies);
elementCopies.clear();
if (keyFrameNumbers.size() == firstAnimNumbers.size()) {
// I.e. identical timepoints, so we can use the first anim's easings to the combined path
// (This is fully correct only if there is only one animation, or if all have same easings)
for (int i = 1; i < keyFrameNumbers.size(); i++)
pa.easingPerFrame[keyFrameNumbers[i]] = firstAnimEasings.value(i - 1);
}
polishPropertyAnimation(&pa);
m_currentPaintInfo.path.addAnimation(pa);
}
void QLottieVisitor::renderPathElements_helper(const QList<QLottieBase *> &pathElements)
{
QPainterPath defaultPath;
const QLottieShape *firstShape = nullptr;
bool hasAnimatedElements = false;
for (const QLottieBase *element : pathElements) {
if (!element->isPathElement())
continue;
const QLottieShape *shape = static_cast<const QLottieShape *>(element);
if (!firstShape)
firstShape = shape;
defaultPath.addPath(shape->path());
hasAnimatedElements = hasAnimatedElements || shape->isAnimated();
}
m_currentPaintInfo.path.setDefaultValue(QVariant::fromValue(defaultPath));
if (hasAnimatedElements)
fillPathAnimationInfo(pathElements);
if (trimmingState() != Sequential)
generatePathNode(firstShape);
}
void QLottieVisitor::renderPathElements(const QList<QLottieBase *> &pathElements)
{
QLOTTIEVISITOR_DEBUG << "[path elements, count = " << pathElements.size() << "]";
const bool needsIndividualPathNodes = (trimmingState() == Parallel);
if (!needsIndividualPathNodes) {
renderPathElements_helper(pathElements);
} else {
for (int i = 0; i < pathElements.size(); i++)
renderPathElements_helper(pathElements.sliced(i, 1));
}
}
void QLottieVisitor::generatePrecompositions()
{
// We need a relative, not absolute, reference to the instantiating item, since a precomp can
// be instantiated multiple times, possibly with different bounds and frameCounter offsets
QString instantiatiorRef = QStringLiteral("parent"); // ref to Loader, indirectly PrecompLayer
for (const auto &[key, precomp] : m_precompositions.asKeyValueRange()) {
// Generate the wrapping defs node start
StructureNodeInfo info;
info.stage = StructureNodeStage::Start;
fillCommonNodeInfo(precomp, &info);
info.nodeId = scrub(key);
info.boundsReferenceId = instantiatiorRef;
TimelineInfo tlInfo;
tlInfo.frameCounterReference = instantiatiorRef;
tlInfo.generateFrameCounter = true;
info.timelineInfo = tlInfo;
m_generator->generateDefsNode(info);
// Generate the precomp's layers
const QString precompId = info.id + QStringLiteral("_defs"); // defs node item
m_currentBoundsIds.push(precompId);
m_currentFrameCounterIds.push(precompId);
precomp->renderChildren(*this);
m_currentFrameCounterIds.pop();
m_currentBoundsIds.pop();
// End defs wrapper
info.stage = StructureNodeStage::End;
m_generator->generateDefsNode(info);
}
}
void QLottieVisitor::generatePathNode(const QLottieShape *shape)
{
QLOTTIEVISITOR_DEBUG << "[drawing shape with"
<< " stroke=" << m_currentPaintInfo.stroke
<< ", fill=" << m_currentPaintInfo.fill;
if (m_currentPaintInfo.path.defaultValue().value<QPainterPath>().isEmpty()
&& !m_currentPaintInfo.path.isAnimated())
return;
StructureNodeInfo info;
if (m_currentStructElements.isEmpty()) {
fillCommonNodeInfo(shape, &info);
info.stage = StructureNodeStage::Start;
info.isPathContainer = true;
info.transform.setDefaultValue(QVariant::fromValue(m_currentPaintInfo.transform));
info.isDefaultTransform = m_currentPaintInfo.transform.isIdentity();
info.opacity.setDefaultValue(m_currentPaintInfo.opacity);
info.isDefaultOpacity = qFuzzyCompare(m_currentPaintInfo.opacity, 1.0);
fillAnimationNodeInfo(shape, &info);
m_generator->generateStructureNode(info);
}
PathNodeInfo pathInfo;
fillCommonNodeInfo(shape, &pathInfo, QStringLiteral("_path"));
pathInfo.path = m_currentPaintInfo.path;
pathInfo.fillRule = m_currentPaintInfo.fillRule;
pathInfo.fillColor.setDefaultValue(QVariant::fromValue(m_currentPaintInfo.fill.color()));
if (!m_currentPaintInfo.fillColorAnimation.isConstant())
pathInfo.fillColor.addAnimation(m_currentPaintInfo.fillColorAnimation);
if (!m_currentPaintInfo.fillOpacityAnimation.isConstant())
pathInfo.fillOpacity.addAnimation(m_currentPaintInfo.fillOpacityAnimation);
if (m_currentPaintInfo.stroke.style() != Qt::NoPen) {
pathInfo.strokeStyle = StrokeStyle::fromPen(m_currentPaintInfo.stroke);
pathInfo.strokeStyle.cosmetic = false;
pathInfo.strokeStyle.color.setDefaultValue(QVariant::fromValue(m_currentPaintInfo.stroke.color()));
if (!m_currentPaintInfo.strokeColorAnimation.isConstant())
pathInfo.strokeStyle.color.addAnimation(m_currentPaintInfo.strokeColorAnimation);
if (!m_currentPaintInfo.strokeOpacityAnimation.isConstant())
pathInfo.strokeStyle.opacity.addAnimation(m_currentPaintInfo.strokeOpacityAnimation);
if (!m_currentPaintInfo.strokeWidthAnimation.isConstant())
pathInfo.strokeStyle.width.addAnimation(m_currentPaintInfo.strokeWidthAnimation);
if (!m_currentPaintInfo.strokeDashOffsetAnimation.isConstant())
pathInfo.strokeStyle.dashOffset.addAnimation(m_currentPaintInfo.strokeDashOffsetAnimation);
if (m_currentPaintInfo.stroke.brush().gradient() != nullptr)
pathInfo.strokeGrad = *m_currentPaintInfo.stroke.brush().gradient();
}
if (m_currentPaintInfo.fill.gradient() != nullptr)
pathInfo.grad = *m_currentPaintInfo.fill.gradient();
if (!m_currentPaintInfo.inverseFillTransform.isIdentity())
pathInfo.fillTransform = m_currentPaintInfo.inverseFillTransform.inverted();
if (trimmingState() != TrimmingState::Off)
pathInfo.trim = m_currentPaintInfo.trim;
pathInfo.path.setTimelineReferenceId(m_currentFrameCounterIds.top());
pathInfo.fillColor.setTimelineReferenceId(m_currentFrameCounterIds.top());
pathInfo.fillOpacity.setTimelineReferenceId(m_currentFrameCounterIds.top());
pathInfo.strokeStyle.color.setTimelineReferenceId(m_currentFrameCounterIds.top());
pathInfo.strokeStyle.opacity.setTimelineReferenceId(m_currentFrameCounterIds.top());
pathInfo.strokeStyle.width.setTimelineReferenceId(m_currentFrameCounterIds.top());
pathInfo.strokeStyle.dashOffset.setTimelineReferenceId(m_currentFrameCounterIds.top());
m_generator->generatePath(pathInfo);
if (m_currentStructElements.isEmpty()) {
info.stage = StructureNodeStage::End;
m_generator->generateStructureNode(info);
}
m_currentPaintInfo.path = QQuickAnimatedProperty(QVariant::fromValue(QPainterPath{ }));
}
void QLottieVisitor::setPathNodeStaticPath(const QPainterPath &path)
{
QLOTTIEVISITOR_DEBUG << "[path bounds=" << path.controlPointRect() << "]";
m_currentPaintInfo.path.setDefaultValue(QVariant::fromValue(path));
}
QT_END_NAMESPACE