1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qlottievisitor_p.h"
5#include <private/qquickgenerator_p.h>
6#include <private/qquicknodeinfo_p.h>
7#include <QtLottie/private/qlottieshape_p.h>
8#include <QtLottie/private/qlottiestroke_p.h>
9#include <QtLottie/private/qlottierect_p.h>
10#include <QtLottie/private/qlottiepolystar_p.h>
11#include <QtLottie/private/qlottieellipse_p.h>
12#include <QtLottie/private/qlottiefreeformshape_p.h>
13#include <QtLottie/private/qlottieshapetransform_p.h>
14#include <QtLottie/private/qlottiegfill_p.h>
15#include <QtLottie/private/qlottieround_p.h>
16#include <QtLottie/private/qlottieroot_p.h>
17#include <QtLottie/private/qlottieflatlayers_p.h>
18#include <QtLottie/private/qlottieimage_p.h>
19
20#include <QtGui/private/qfixed_p.h>
21
22#include <QFile>
23
24QT_BEGIN_NAMESPACE
25
26Q_STATIC_LOGGING_CATEGORY(lcLottieQtVisitor, "qt.lottieqt.visitor")
27
28using namespace Qt::Literals::StringLiterals;
29
30#define QLOTTIEVISITOR_DEBUG \
31 qCDebug(lcLottieQtVisitor).noquote().nospace() \
32 << QByteArray().fill(' ', m_savedPaintInfos.size() * 4) \
33 << ((trimmingState() == Sequential) ? QByteArray("trimmed") : QByteArray{})
34
35QLottieVisitor::QLottieVisitor(const QString lottieFileName, QQuickGenerator *generator)
36 : m_lottieFileName(lottieFileName), m_generator(generator)
37{
38}
39
40bool QLottieVisitor::nodeIsGraphicElement(const QLottieBase *node)
41{
42 return (node->type() >= LOTTIE_SHAPE_ELLIPSE_IX && node->type() <= LOTTIE_SHAPE_REPEATER_IX);
43}
44
45bool QLottieVisitor::nodeIsShape(const QLottieBase *node)
46{
47 switch (node->type()) {
48 case LOTTIE_SHAPE_ELLIPSE_IX: Q_FALLTHROUGH();
49 case LOTTIE_SHAPE_RECT_IX: Q_FALLTHROUGH();
50 case LOTTIE_SHAPE_SHAPE_IX: Q_FALLTHROUGH();
51 case LOTTIE_SHAPE_STAR_IX:
52 return true;
53 break;
54 default:
55 break;
56 }
57 return false;
58}
59
60void QLottieVisitor::render(const QLottieRoot &root)
61{
62 enumerateLayerChildren(node: &root);
63
64 StructureNodeInfo info;
65 const QJsonObject rootObj = root.definition();
66 info.size = QSize(rootObj.value(key: "w"_L1).toInt(), rootObj.value(key: "h"_L1).toInt());
67
68 int frameRate = rootObj.value(key: QLatin1String("fr")).toVariant().toInt();
69 if (frameRate > 0)
70 m_frameRate = frameRate;
71
72 m_duration = qRound(d: 1000 * rootObj.value(key: "op"_L1).toDouble(defaultValue: 100.0) / m_frameRate);
73 info.viewBox = QRectF(QPointF(0, 0), info.size);
74
75 QLOTTIEVISITOR_DEBUG << "[root viewbox=" << info.viewBox << ", frame rate=" << m_frameRate << ", duration=" << m_duration << "ms ]";
76
77 info.stage = StructureNodeStage::Start;
78 info.nodeId = u"_q_animation"_s; // # centralize
79
80 m_generator->generateRootNode(info);
81
82 root.render(renderer&: *this);
83
84 info.stage = StructureNodeStage::End;
85 m_generator->generateRootNode(info);
86}
87
88void QLottieVisitor::fillCommonNodeInfo(const QLottieBase *node, NodeInfo *info)
89{
90 Q_ASSERT(node);
91 Q_ASSERT(info);
92
93 info->typeName = QStringLiteral("Type%1").arg(a: node->type());
94}
95
96void QLottieVisitor::fillLayerAnimationInfo(const QLottieLayer *node, NodeInfo *info)
97{
98 int endTime = qRound(d: 1000 * node->endFrame() / qreal(m_frameRate));
99 if (node->startFrame() == 0 && endTime >= m_duration)
100 return;
101
102 QQuickAnimatedProperty::PropertyAnimation animation;
103 if (node->startFrame() > 0)
104 animation.frames[0] = QVariant::fromValue(value: false);
105 animation.frames[qRound(d: 1000 * node->startFrame() / qreal(m_frameRate))] = QVariant::fromValue(value: true);
106 if (endTime < m_duration)
107 animation.frames[endTime] = QVariant::fromValue(value: false);
108 animation.frames[m_duration] = animation.frames.last();
109 animation.flags |= QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd;
110
111 info->visibility.addAnimation(animation);
112}
113
114void QLottieVisitor::fillAnimationNodeInfo(const QLottieBase *node, NodeInfo *info)
115{
116 Q_UNUSED(node);
117 for (const PaintInfo::TransformAnimationInfo &animInfo : std::as_const(t&: m_currentPaintInfo.transformAnimations)) {
118 QQuickAnimatedProperty::PropertyAnimation animation;
119 animation.subtype = animInfo.animationType;
120 animation.frames = animInfo.frames;
121 animation.flags |= QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd;
122
123 if (!animation.frames.isEmpty()) {
124 animation.frames[0] = animation.frames.first();
125 animation.frames[m_duration] = animation.frames.last();
126
127 animation.easingPerFrame = animInfo.easingPerFrame;
128
129 if (animInfo.animationType == QTransform::TxNone)
130 info->opacity.addAnimation(animation);
131 else
132 info->transform.addAnimation(animation);
133 }
134 }
135}
136
137void QLottieVisitor::saveState()
138{
139 m_savedPaintInfos.append(t: m_currentPaintInfo);
140}
141
142void QLottieVisitor::restoreState()
143{
144 Q_ASSERT(!m_savedPaintInfos.isEmpty());
145 m_currentPaintInfo = m_savedPaintInfos.takeLast();
146}
147
148void QLottieVisitor::render(const QLottieLayer &layer)
149{
150 QLOTTIEVISITOR_DEBUG << "[layer '" << layer.name() << "' type " << Qt::hex << layer.type() << "]";
151
152 m_frameOffset += layer.frameOffset();
153
154 StructureNodeInfo info;
155 info.stage = StructureNodeStage::Start;
156 if (layer.type() == LOTTIE_LAYER_PRECOMP_IX)
157 info.nodeId = layer.definition().value(key: QLatin1String("refId")).toString();
158 else
159 info.nodeId = layer.name();
160 info.transform.setDefaultValue(QVariant::fromValue(value: m_currentPaintInfo.transform));
161 info.isDefaultTransform = m_currentPaintInfo.transform.isIdentity();
162
163 fillCommonNodeInfo(node: &layer, info: &info);
164 fillAnimationNodeInfo(node: &layer, info: &info);
165 fillLayerAnimationInfo(node: &layer, info: &info);
166 info.layerNum = m_layers.indexOf(t: &layer);
167 if (layer.hasLinkedLayer() && layer.parent()) {
168 for (const QLottieBase *sibling : layer.parent()->children()) {
169 if (auto siblingLayer = QLottieLayer::checkedCast(node: sibling)) {
170 if (siblingLayer != &layer && siblingLayer->layerId() == layer.linkedLayerId())
171 info.transformReferenceLayerNum = m_layers.indexOf(t: siblingLayer);
172 }
173 }
174 QLOTTIEVISITOR_DEBUG << " xf link resolved to layernum" << info.transformReferenceLayerNum;
175 }
176
177 m_generator->generateStructureNode(info);
178
179 m_currentPaintInfo = {};
180}
181
182void QLottieVisitor::render(const QLottieSolidLayer &layer)
183{
184 render(layer: static_cast<const QLottieLayer &>(layer));
185 if (!layer.size().isEmpty()) {
186 m_currentPaintInfo.fill = layer.color();
187 QPainterPath layerRect;
188 layerRect.addRect(rect: QRect(QPoint(), layer.size()));
189 processShape(shape: nullptr, path: layerRect);
190 }
191}
192
193void QLottieVisitor::render(const QLottieGroup &group)
194{
195 QLOTTIEVISITOR_DEBUG << "[group '" << group.name() << "' #children " << group.children().size() << "]";
196
197 bool hasGroups = false;
198 for (const QLottieBase *child : group.children()) {
199 if (child->type() == LOTTIE_SHAPE_GROUP_IX) {
200 hasGroups = true;
201 break;
202 }
203 }
204
205 // Child groups have their own transforms, so we need a structure node for correct xf ordering.
206 if (hasGroups) {
207 // We must apply the group xf already here, so it will become the structure node's xf.
208 // If non-identity, m_currentStructElements is used to avoid re-applying it on normal visit.
209 const QLottieBase *groupXf = group.children().first();
210 if (groupXf->type() == LOTTIE_SHAPE_TRANS_IX) // Always true in wellformed lottie file
211 render(transform: *static_cast<const QLottieShapeTransform *>(groupXf));
212 const bool groupHasTransform = !m_currentPaintInfo.transform.isIdentity()
213 || m_currentPaintInfo.transformAnimations.size() > 0;
214 if (groupHasTransform) {
215 StructureNodeInfo info;
216 info.stage = StructureNodeStage::Start;
217 info.nodeId = group.name();
218 info.transform.setDefaultValue(QVariant::fromValue(value: m_currentPaintInfo.transform));
219 info.isDefaultTransform = m_currentPaintInfo.transform.isIdentity();
220
221 fillCommonNodeInfo(node: &group, info: &info);
222 fillAnimationNodeInfo(node: &group, info: &info);
223
224 m_generator->generateStructureNode(info);
225 m_currentStructElements.push(t: &group);
226
227 m_currentPaintInfo.transform.reset();
228 m_currentPaintInfo.transformAnimations.clear();
229 }
230 }
231}
232
233void QLottieVisitor::finish(const QLottieLayer &layer)
234{
235 QLOTTIEVISITOR_DEBUG << "[layer '" << layer.name() << "' finish]";
236
237 m_frameOffset -= layer.frameOffset();
238
239 StructureNodeInfo info;
240 info.stage = StructureNodeStage::End;
241
242 fillCommonNodeInfo(node: &layer, info: &info);
243 m_generator->generateStructureNode(info);
244}
245
246void QLottieVisitor::finish(const QLottieGroup &group)
247{
248 QLOTTIEVISITOR_DEBUG << "[group '" << group.name() << "' finish]";
249
250 if (!m_currentStructElements.isEmpty() && m_currentStructElements.top() == &group) {
251 StructureNodeInfo info;
252 info.stage = StructureNodeStage::End;
253
254 fillCommonNodeInfo(node: &group, info: &info);
255 m_generator->generateStructureNode(info);
256 m_currentStructElements.pop();
257 }
258}
259
260void QLottieVisitor::render(const QLottieRect &rect)
261{
262 QLOTTIEVISITOR_DEBUG << "[rect]";
263
264 processShape(shape: &rect);
265}
266
267void QLottieVisitor::render(const QLottieEllipse &ellipse)
268{
269 QLOTTIEVISITOR_DEBUG << "[ellipse]";
270
271 processShape(shape: &ellipse);
272}
273
274void QLottieVisitor::render(const QLottiePolyStar &star)
275{
276 QLOTTIEVISITOR_DEBUG << "[star]";
277
278 processShape(shape: &star);
279}
280
281void QLottieVisitor::render(const QLottieRound &round)
282{
283 QLOTTIEVISITOR_DEBUG << "[round]";
284
285 processShape(shape: &round);
286}
287
288void QLottieVisitor::render(const QLottieFill &fill)
289{
290 QLOTTIEVISITOR_DEBUG << "[fill color=" << fill.color() << ", opacity=" << fill.opacity() << "]";
291
292 QColor color = fill.color();
293 color.setAlphaF(color.alphaF() * (fill.opacity() / 100.0));
294 m_currentPaintInfo.fill = color;
295
296 bool isEvenOdd = (fill.definition().value(key: QLatin1String("r")).toInt() == 2);
297 m_currentPaintInfo.fillRule = isEvenOdd ? Qt::OddEvenFill : Qt::WindingFill;
298}
299
300void QLottieVisitor::render(const QLottieGFill &gradient)
301{
302 QLOTTIEVISITOR_DEBUG << "[fill gradient]";
303
304 if (gradient.value() != nullptr)
305 m_currentPaintInfo.fill = *gradient.value();
306 bool isEvenOdd = (gradient.definition().value(key: QLatin1String("r")).toInt() == 2);
307 m_currentPaintInfo.fillRule = isEvenOdd ? Qt::OddEvenFill : Qt::WindingFill;
308}
309
310void QLottieVisitor::render(const QLottieImage &image)
311{
312 QLOTTIEVISITOR_DEBUG << "[image size=" << image.size() << "]";
313
314 ImageNodeInfo info;
315 fillCommonNodeInfo(node: &image, info: &info);
316 info.image = image.image();
317 info.rect = QRectF(QPointF(), image.size());
318 info.externalFileReference = image.url().toLocalFile();
319
320 m_generator->generateImageNode(info);
321}
322
323void QLottieVisitor::render(const QLottieStroke &stroke)
324{
325 QLOTTIEVISITOR_DEBUG << "[stroke color=" << stroke.pen().color()
326 << ", opacity=" << stroke.opacity() << "]";
327
328 const QPen pen = stroke.pen();
329 m_currentPaintInfo.stroke = pen;
330
331 QColor color = pen.color();
332 color.setAlphaF(color.alphaF() * (stroke.opacity() / 100.0));
333 m_currentPaintInfo.stroke.setColor(color);
334}
335
336void QLottieVisitor::render(const QLottieBasicTransform &transform)
337{
338 QLOTTIEVISITOR_DEBUG << "[basic transform s=" << transform.scale()
339 << ", r=" << transform.rotation()
340 << ", o=" << transform.opacity() << "]";
341 if (hasAnimations(transform: &transform))
342 collectTransformAnimations(transform: &transform);
343 else
344 applyTransform(xf: &m_currentPaintInfo.transform, lottieXf: transform, isShapeTransform: false);
345
346 m_currentPaintInfo.opacity *= transform.opacity();
347}
348
349namespace {
350 template<typename T>
351 QLottieVisitor::PaintInfo::TransformAnimationInfo
352 collectAnimations(const T &property,
353 QTransform::TransformationType type,
354 std::function<void(qreal,
355 const QVariant &,
356 QLottieVisitor::PaintInfo::TransformAnimationInfo *,
357 std::optional<QBezier>)> storeAnimationFrame,
358 std::function<QVariantList(const QVariant &)> createParams)
359 {
360 const auto easingCurves = property.easingCurves();
361 QLottieVisitor::PaintInfo::TransformAnimationInfo info;
362 info.animationType = type;
363 bool firstFrame = true;
364 if (easingCurves.isEmpty()) {
365 const QVariantList params = createParams(QVariant::fromValue(property.value()));
366 storeAnimationFrame(0, params, &info, std::nullopt);
367 } else {
368 for (const auto &curve : easingCurves) {
369 if (firstFrame) {
370 const auto startValue = curve.startValue;
371 const QVariantList startParams = createParams(QVariant::fromValue(startValue));
372 storeAnimationFrame(0, startParams, &info, std::nullopt);
373 if (curve.startFrame > 0)
374 storeAnimationFrame(curve.startFrame, startParams, &info, std::nullopt);
375 firstFrame = false;
376 }
377 const auto endValue = curve.endValue;
378 const QVariantList endParams = createParams(endValue);
379 storeAnimationFrame(curve.endFrame, endParams, &info, curve.easing.bezier());
380 }
381 }
382
383 return info;
384 }
385}
386
387void QLottieVisitor::collectTransformAnimations(const QLottieBasicTransform *transform,
388 bool isShapeTransform)
389{
390 Q_UNUSED(isShapeTransform);
391 const QLottieProperty<QPointF> anchorPoints = transform->anchorPointProperty();
392 const QLottieProperty<qreal> rotations = transform->rotationProperty();
393 const QLottieProperty<QPointF> scales = transform->scaleProperty();
394 const QLottieSpatialProperty positions = transform->positionProperty();
395 const QLottieProperty<qreal> opacities = transform->opacityProperty();
396 const QLottieProperty<qreal> xPositions = transform->xPosProperty();
397 const QLottieProperty<qreal> yPositions = transform->yPosProperty();
398
399 auto storeAnimationFrame = [&](qreal lottieFrameNumber,
400 const QVariant &propertyValue,
401 PaintInfo::TransformAnimationInfo *info,
402 std::optional<QBezier> easingBezier = std::nullopt) {
403 const int timePointMs = qRound(d: 1000 * (m_frameOffset + lottieFrameNumber) / m_frameRate);
404 info->frames[timePointMs] = propertyValue;
405 if (easingBezier)
406 info->easingPerFrame[timePointMs] = *easingBezier;
407 };
408
409 QLottieVisitor::PaintInfo::TransformAnimationInfo info;
410 if (!transform->splitPosition()) {
411 info = collectAnimations(property: positions,
412 type: QTransform::TxTranslate,
413 storeAnimationFrame,
414 createParams: [](const QVariant &v)
415 {
416 return QVariantList{ v };
417 });
418 m_currentPaintInfo.transformAnimations.append(t: info);
419 } else {
420 info = collectAnimations(property: xPositions,
421 type: QTransform::TxTranslate,
422 storeAnimationFrame,
423 createParams: [](const QVariant &v)
424 {
425 return QVariantList{ QVariant::fromValue(value: QPointF(v.toReal(), 0.0)) };
426 });
427 m_currentPaintInfo.transformAnimations.append(t: info);
428
429 info = collectAnimations(property: yPositions,
430 type: QTransform::TxTranslate,
431 storeAnimationFrame,
432 createParams: [](const QVariant &v)
433 {
434 return QVariantList{ QVariant::fromValue(value: QPointF(0.0, v.toReal())) };
435 });
436 m_currentPaintInfo.transformAnimations.append(t: info);
437 }
438
439 auto storeRotationParameter = [](const QVariant &v) {
440 return QVariantList{ QVariant::fromValue(value: QPointF(0, 0)), v };
441 };
442 info = collectAnimations(property: rotations,
443 type: QTransform::TxRotate,
444 storeAnimationFrame,
445 createParams: storeRotationParameter);
446 m_currentPaintInfo.transformAnimations.append(t: info);
447
448 if (isShapeTransform) {
449 const QLottieShapeTransform *shapeTransform =
450 static_cast<const QLottieShapeTransform *>(transform);
451
452 const QLottieProperty<qreal> skews = shapeTransform->skewProperty();
453 const QLottieProperty<qreal> skewAxes = shapeTransform->skewAxisProperty();
454
455 // Lottie shear transforms work by first rotating by skew axis angle, then applying
456 // the skewAngle as the shear along the X-axis, and then rotating back.
457 info = collectAnimations(property: skewAxes,
458 type: QTransform::TxRotate,
459 storeAnimationFrame,
460 createParams: [](const QVariant &v) {
461 return QVariantList{ QVariant::fromValue(value: QPointF(0, 0)),
462 QVariant::fromValue(value: -1.0 * v.toReal()) };
463 });
464 m_currentPaintInfo.transformAnimations.append(t: info);
465
466 info = collectAnimations(property: skews,
467 type: QTransform::TxShear,
468 storeAnimationFrame,
469 createParams: [](const QVariant &v) {
470 return QVariantList{ QVariant::fromValue(value: QPointF(-1.0 * v.toReal(), 0.0)) };
471 });
472 m_currentPaintInfo.transformAnimations.append(t: info);
473
474 info = collectAnimations(property: skewAxes,
475 type: QTransform::TxRotate,
476 storeAnimationFrame,
477 createParams: storeRotationParameter);
478 m_currentPaintInfo.transformAnimations.append(t: info);
479 }
480
481 info = collectAnimations(property: scales,
482 type: QTransform::TxScale,
483 storeAnimationFrame,
484 createParams: [](const QVariant &v) {
485 return QVariantList{ QVariant::fromValue(value: v.toPointF() / 100.0) };
486 });
487 m_currentPaintInfo.transformAnimations.append(t: info);
488
489 info = collectAnimations(property: anchorPoints,
490 type: QTransform::TxTranslate,
491 storeAnimationFrame,
492 createParams: [](const QVariant &v) {
493 return QVariantList{ QVariant::fromValue(value: v.toPointF() * -1.0) };
494 });
495 m_currentPaintInfo.transformAnimations.append(t: info);
496
497 {
498 const QList<EasingSegment<qreal> > easingCurves = opacities.easingCurves();
499 PaintInfo::TransformAnimationInfo info;
500 info.animationType = QTransform::TxNone;
501 bool firstFrame = true;
502 for (const auto &curve : easingCurves) {
503 if (firstFrame) {
504 const qreal startValue = curve.startValue / 100;
505 const QVariant startParams = QVariant::fromValue(value: startValue);
506 storeAnimationFrame(0, startParams, &info, std::nullopt);
507 if (curve.startFrame > 0)
508 storeAnimationFrame(curve.startFrame, startParams, &info);
509 firstFrame = false;
510 }
511
512 const qreal endValue = curve.endValue / 100;
513 const QVariant endParams = QVariant::fromValue(value: endValue);
514 storeAnimationFrame(curve.endFrame, endParams, &info, curve.easing.bezier());
515 }
516 m_currentPaintInfo.transformAnimations.append(t: info);
517 }
518}
519
520void QLottieVisitor::enumerateLayerChildren(const QLottieBase *node)
521{
522 if (auto layer = QLottieLayer::checkedCast(node))
523 m_layers.append(t: layer);
524 for (const QLottieBase *child : node->children())
525 enumerateLayerChildren(node: child);
526}
527
528bool QLottieVisitor::hasAnimations(const QLottieBasicTransform *transform, bool isShapeTransform)
529{
530 bool hasAnimations = transform->rotationProperty().startFrame() < transform->rotationProperty().endFrame()
531 || transform->positionProperty().startFrame() < transform->positionProperty().endFrame()
532 || transform->scaleProperty().startFrame() < transform->scaleProperty().endFrame()
533 || transform->opacityProperty().startFrame() < transform->opacityProperty().endFrame();
534
535 if (transform->splitPosition() && !hasAnimations) {
536 hasAnimations = transform->xPosProperty().startFrame() < transform->xPosProperty().endFrame()
537 || transform->yPosProperty().startFrame() < transform->yPosProperty().endFrame();
538 }
539
540 if (isShapeTransform && !hasAnimations) {
541 const QLottieShapeTransform *shapeTransform = static_cast<const QLottieShapeTransform *>(transform);
542 hasAnimations = shapeTransform->skewProperty().startFrame() < shapeTransform->skewProperty().endFrame()
543 || shapeTransform->skewAxisProperty().startFrame() < shapeTransform->skewAxisProperty().endFrame();
544 }
545
546 if (qEnvironmentVariableIntValue(varName: "QLOTTIEVISITOR_DISABLE_ANIMATIONS"))
547 return false;
548
549 return hasAnimations;
550}
551
552void QLottieVisitor::render(const QLottieShapeTransform &transform)
553{
554 if (!m_currentStructElements.isEmpty() && transform.parent() == m_currentStructElements.top()) {
555 // This transform was already applied as part of a group structure node
556 return;
557 }
558
559 QLOTTIEVISITOR_DEBUG << "[shape transform s=" << transform.scale()
560 << ", r=" << transform.rotation()
561 << ", o=" << transform.opacity() << "]";
562
563 if (hasAnimations(transform: &transform, isShapeTransform: true))
564 collectTransformAnimations(transform: &transform, isShapeTransform: true);
565 else
566 applyTransform(xf: &m_currentPaintInfo.transform, lottieXf: transform, isShapeTransform: true);
567
568 m_currentPaintInfo.opacity *= transform.opacity();
569}
570
571void QLottieVisitor::render(const QLottieFreeFormShape &shape)
572{
573 QLOTTIEVISITOR_DEBUG << "[freeform]";
574
575 processShape(shape: &shape);
576}
577
578void QLottieVisitor::render(const QLottieTrimPath &trim)
579{
580 QLOTTIEVISITOR_DEBUG << "[trim]";
581
582 m_currentPaintInfo.trim.enabled = true;
583 m_currentPaintInfo.trim.start.setDefaultValue(trim.start() / 100.0);
584 m_currentPaintInfo.trim.end.setDefaultValue(trim.end() / 100.0);
585 m_currentPaintInfo.trim.offset.setDefaultValue(trim.offset() / 360.0);
586
587 auto registerAnimations = [&](QQuickAnimatedProperty *outProperty,
588 const QLottieProperty<qreal> &inProperty,
589 qreal scale) {
590 const QList<EasingSegment<qreal> > easingCurves = inProperty.easingCurves();
591
592 QQuickAnimatedProperty::PropertyAnimation animation;
593 animation.flags |= QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd;
594
595 for (const auto &curve : easingCurves) {
596 const qreal startValue = curve.startValue * scale;
597 const qreal endValue = curve.endValue * scale;
598
599 int startFrameTime = QFixed::fromReal(r: 1000 * curve.startFrame / qreal(m_frameRate)).round().toInt();
600 int endFrameTime = QFixed::fromReal(r: 1000 * curve.endFrame / qreal(m_frameRate)).round().toInt();
601
602 animation.frames[startFrameTime] = startValue;
603 animation.frames[endFrameTime] = endValue;
604
605 animation.easingPerFrame[endFrameTime] = curve.easing.bezier();
606 }
607 if (!animation.frames.isEmpty()) {
608 animation.frames[0] = outProperty->defaultValue();
609 animation.frames[m_duration] = animation.frames.last();
610 outProperty->addAnimation(animation);
611 }
612 };
613
614 registerAnimations(&m_currentPaintInfo.trim.start, trim.startProperty(), 1.0 / 100.0);
615 registerAnimations(&m_currentPaintInfo.trim.end, trim.endProperty(), 1.0 / 100.0);
616 registerAnimations(&m_currentPaintInfo.trim.offset, trim.offsetProperty(), 1.0 / 360.0);
617
618 if (!trim.isParallel())
619 processShape(shape: &trim, path: m_currentPaintInfo.unitedPath);
620}
621
622void QLottieVisitor::render(const QLottieFillEffect &effect)
623{
624 QLOTTIEVISITOR_DEBUG << "[effect]";
625
626 // ### What are you?
627 Q_UNUSED(effect);
628}
629
630void QLottieVisitor::render(const QLottieRepeater &repeater)
631{
632 QLOTTIEVISITOR_DEBUG << "[repeater]";
633
634 // ### Repeats the following shapes N times with different transforms
635 Q_UNUSED(repeater);
636}
637
638void QLottieVisitor::processShape(const QLottieShape *shape, const QPainterPath &path)
639{
640 QLOTTIEVISITOR_DEBUG << "[drawing shape with"
641 << " stroke=" << m_currentPaintInfo.stroke
642 << ", fill=" << m_currentPaintInfo.fill;
643
644 if (path.isEmpty())
645 return;
646 StructureNodeInfo info;
647 info.stage = StructureNodeStage::Start;
648 info.isPathContainer = true;
649
650 info.transform.setDefaultValue(QVariant::fromValue(value: m_currentPaintInfo.transform));
651 info.isDefaultTransform = m_currentPaintInfo.transform.isIdentity();
652 info.opacity.setDefaultValue(m_currentPaintInfo.opacity);
653 info.isDefaultOpacity = qFuzzyCompare(p1: m_currentPaintInfo.opacity, p2: 1.0);
654
655 if (shape) {
656 fillCommonNodeInfo(node: shape, info: &info);
657 fillAnimationNodeInfo(node: shape, info: &info);
658 }
659
660 m_generator->generateStructureNode(info);
661
662 PathNodeInfo pathInfo;
663
664 pathInfo.painterPath = path;
665 pathInfo.fillRule = m_currentPaintInfo.fillRule;
666 pathInfo.fillColor.setDefaultValue(QVariant::fromValue(value: m_currentPaintInfo.fill.color()));
667 pathInfo.strokeStyle = StrokeStyle::fromPen(p: m_currentPaintInfo.stroke);
668 pathInfo.strokeStyle.color.setDefaultValue(QVariant::fromValue(value: m_currentPaintInfo.stroke.color()));
669 if (m_currentPaintInfo.fill.gradient() != nullptr)
670 pathInfo.grad = *m_currentPaintInfo.fill.gradient();
671 if (trimmingState() != TrimmingState::Off)
672 pathInfo.trim = m_currentPaintInfo.trim;
673 m_generator->generatePath(info: pathInfo);
674
675 info.stage = StructureNodeStage::End;
676 m_generator->generateStructureNode(info);
677}
678
679void QLottieVisitor::processShape(const QLottieShape *shape)
680{
681 QLOTTIEVISITOR_DEBUG << "[shape bounds=" << shape->path().controlPointRect() << "]";
682
683 if (trimmingState() == Sequential) {
684 QPainterPath p = m_currentPaintInfo.transform.map(p: shape->path());
685 p.addPath(path: m_currentPaintInfo.unitedPath);
686 m_currentPaintInfo.unitedPath = p;
687 // } else if (m_buildingClipRegion) { ###
688 } else {
689 processShape(shape, path: shape->path());
690 }
691}
692
693QT_END_NAMESPACE
694

source code of qtlottie/src/lottiegenerator/qlottievisitor.cpp