// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qquickqmlgenerator_p.h" #include "qquicknodeinfo_p.h" #include "utils_p.h" #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE QQuickQmlGenerator::QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName) : QQuickGenerator(fileName, flags) , outputFileName(outFileName) { m_result.open(QIODevice::ReadWrite); } QQuickQmlGenerator::~QQuickQmlGenerator() { } bool QQuickQmlGenerator::save() { bool res = true; if (!outputFileName.isEmpty()) { QFileInfo fileInfo(outputFileName); QDir dir(fileInfo.absolutePath()); if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { qCWarning(lcQuickVectorImage) << "Failed to create path" << dir.absolutePath(); res = false; } else { QFile outFile(outputFileName); if (outFile.open(QIODevice::WriteOnly)) { outFile.write(m_result.data()); outFile.close(); } else { qCWarning(lcQuickVectorImage) << "Failed to write to file" << outFile.fileName(); res = false; } } } if (lcQuickVectorImage().isDebugEnabled()) qCDebug(lcQuickVectorImage).noquote() << m_result.data().left(300); return res; } void QQuickQmlGenerator::setShapeTypeName(const QString &name) { m_shapeTypeName = name.toLatin1(); } QString QQuickQmlGenerator::shapeTypeName() const { return QString::fromLatin1(m_shapeTypeName); } void QQuickQmlGenerator::setCommentString(const QString commentString) { m_commentString = commentString; } QString QQuickQmlGenerator::commentString() const { return m_commentString; } QString QQuickQmlGenerator::generateNodeBase(const NodeInfo &info) { if (!info.nodeId.isEmpty()) stream() << "objectName: \"" << info.nodeId << "\""; QString idString = info.id; if (!idString.isEmpty()) { // If input contains multiple items with the same id, then this is invalid. However we // shouldn't generate invalid QML from it. So we add a suffix if this is detected. if (m_generatedIds.contains(idString)) idString += QStringLiteral("_%1").arg(m_generatedIds.size()); m_generatedIds.insert(idString); stream() << "id: " << idString; } if (!info.bounds.isNull()) { stream() << "property var originalBounds: Qt.rect(" << info.bounds.x() << ", " << info.bounds.y() << ", " << info.bounds.width() << ", " << info.bounds.height() << ")"; stream() << "implicitWidth: originalBounds.width"; stream() << "implicitHeight: originalBounds.height"; } if (!info.isDefaultOpacity) stream() << "opacity: " << info.opacity.defaultValue().toReal(); stream() << "transformOrigin: Item.TopLeft"; if (!info.maskId.isEmpty()) { stream() << "layer.enabled: true"; stream() << "visible: false"; stream() << "layer.textureSize: " << info.maskId << "_" << info.id << "_mask.textureSize"; stream() << "layer.sourceRect: " << info.maskId << ".maskRect(" << info.id << ")"; } else { generateItemAnimations(idString, info); } return idString; } void QQuickQmlGenerator::generateNodeEnd(const NodeInfo &info) { m_indentLevel--; stream() << "}"; if (!info.maskId.isEmpty()) generateMaskUse(info); } void QQuickQmlGenerator::generateItemAnimations(const QString &idString, const NodeInfo &info) { const bool hasTransform = info.transform.isAnimated() || !info.maskId.isEmpty() || !info.isDefaultTransform || !info.transformReferenceId.isEmpty() || info.motionPath.isAnimated(); if (hasTransform) { stream() << "transform: TransformGroup {"; m_indentLevel++; bool hasNonConstantTransform = false; int earliestOverrideGroup = -1; if (!idString.isEmpty()) { stream() << "id: " << idString << "_transform_base_group"; if (!info.maskId.isEmpty()) stream() << "Translate { x: " << idString << ".sourceX; y: " << idString << ".sourceY }"; if (info.transform.isAnimated()) { for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) { stream() << "TransformGroup {"; m_indentLevel++; if (!idString.isEmpty()) stream() << "id: " << idString << "_transform_group_" << groupIndex; int animationStart = info.transform.animationGroup(groupIndex); int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount() ? info.transform.animationGroup(groupIndex + 1) : info.transform.animationCount(); const QQuickAnimatedProperty::PropertyAnimation &firstAnimation = info.transform.animation(animationStart); const bool replace = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::ReplacePreviousAnimations; if (replace && earliestOverrideGroup < 0) earliestOverrideGroup = groupIndex; for (int i = nextAnimationStart - 1; i >= animationStart; --i) { const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i); if (animation.frames.isEmpty()) continue; const QVariantList ¶meters = animation.frames.first().value(); switch (animation.subtype) { case QTransform::TxTranslate: if (animation.isConstant()) { const QPointF translation = parameters.value(0).value(); if (!translation.isNull()) stream() << "Translate { x: " << translation.x() << "; y: " << translation.y() << " }"; } else { hasNonConstantTransform = true; stream() << "Translate { id: " << idString << "_transform_" << groupIndex << "_" << i << " }"; } break; case QTransform::TxScale: if (animation.isConstant()) { const QPointF scale = parameters.value(0).value(); if (scale != QPointF(1, 1)) stream() << "Scale { xScale: " << scale.x() << "; yScale: " << scale.y() << " }"; } else { hasNonConstantTransform = true; stream() << "Scale { id: " << idString << "_transform_" << groupIndex << "_" << i << "}"; } break; case QTransform::TxRotate: if (animation.isConstant()) { const QPointF center = parameters.value(0).value(); const qreal angle = parameters.value(1).toReal(); if (!qFuzzyIsNull(angle)) stream() << "Rotation { angle: " << angle << "; origin.x: " << center.x() << "; origin.y: " << center.y() << " }"; //### center relative to what? } else { hasNonConstantTransform = true; stream() << "Rotation { id: " << idString << "_transform_" << groupIndex << "_" << i << " }"; } break; case QTransform::TxShear: if (animation.isConstant()) { const QPointF skew = parameters.value(0).value(); if (!skew.isNull()) stream() << "Shear { xAngle: " << skew.x() << "; yAngle: " << skew.y() << " }"; } else { hasNonConstantTransform = true; stream() << "Shear { id: " << idString << "_transform_" << groupIndex << "_" << i << " }"; } break; default: Q_UNREACHABLE(); } } m_indentLevel--; stream() << "}"; } } if (info.motionPath.isAnimated()) { QVariantPair defaultProps = info.motionPath.defaultValue().value(); const bool adaptAngle = defaultProps.first.toBool(); const qreal baseRotation = defaultProps.second.toReal(); if (adaptAngle || !qFuzzyIsNull(baseRotation)) { stream() << "Rotation {"; m_indentLevel++; if (adaptAngle) { stream() << "angle: " << idString << "_motion_animation.currentInterpolator.angle"; if (!qFuzzyIsNull(baseRotation)) stream(SameLine) << " + " << baseRotation; } else { stream() << "angle: " << baseRotation; } m_indentLevel--; stream() << "}"; } stream() << "Translate {"; m_indentLevel++; stream() << "x: " << idString << "_motion_animation.currentInterpolator.x"; stream() << "y: " << idString << "_motion_animation.currentInterpolator.y"; m_indentLevel--; stream() << "}"; } } if (!info.isDefaultTransform) { QTransform xf = info.transform.defaultValue().value(); if (xf.type() <= QTransform::TxTranslate) { stream() << "Translate { x: " << xf.dx() << "; y: " << xf.dy() << "}"; } else { stream() << "Matrix4x4 { matrix: "; generateTransform(xf); stream(SameLine) << "}"; } } if (!info.transformReferenceId.isEmpty()) stream() << "Matrix4x4 { matrix: " << info.transformReferenceId << ".transformMatrix }"; m_indentLevel--; stream() << "}"; if (hasNonConstantTransform) { generateAnimateTransform(idString, info); } else if (info.transform.isAnimated() && earliestOverrideGroup >= 0) { // We have animations, but they are all constant? Then we still need to respect the // override flag of the animations stream() << "Component.onCompleted: {"; m_indentLevel++; stream() << idString << "_transform_base_group.activateOverride(" << idString << "_transform_group_" << earliestOverrideGroup << ")"; m_indentLevel--; stream() << "}"; } } generateAnimateMotionPath(idString, info.motionPath); generatePropertyAnimation(info.opacity, idString, QStringLiteral("opacity")); generatePropertyAnimation(info.visibility, idString, QStringLiteral("visible")); } void QQuickQmlGenerator::generateMaskUse(const NodeInfo &info) { Q_ASSERT(!info.maskId.isEmpty()); // Shader effect source for the mask itself stream() << "ShaderEffectSource {"; m_indentLevel++; const QString maskId = info.maskId + QStringLiteral("_") + info.id + QStringLiteral("_mask"); stream() << "id: " << maskId; stream() << "sourceItem: " << info.maskId; stream() << "visible: false"; stream() << "ItemSpy {"; m_indentLevel++; stream() << "id: " << maskId << "_itemspy"; stream() << "anchors.fill: parent"; m_indentLevel--; stream() << "}"; stream() << "textureSize: " << maskId << "_itemspy.requiredTextureSize"; stream() << "sourceRect: " << info.maskId << ".maskRect(" << info.id << ")"; stream() << "width: sourceRect.width"; stream() << "height: sourceRect.height"; m_indentLevel--; stream() << "}"; stream() << "ShaderEffect {"; m_indentLevel++; const QString maskShaderId = maskId + QStringLiteral("_se"); stream() << "id:" << maskShaderId; stream() << "property real sourceX: " << maskId << ".sourceRect.x"; stream() << "property real sourceY: " << maskId << ".sourceRect.y"; stream() << "width: " << maskId << ".sourceRect.width"; stream() << "height: " << maskId << ".sourceRect.height"; stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/genericmask.frag.qsb\""; stream() << "property var source: " << info.id; stream() << "property var maskSource: " << maskId; stream() << "property bool isAlpha: " << (info.isMaskAlpha ? "true" : "false"); stream() << "property bool isInverted: " << (info.isMaskInverted ? "true" : "false"); generateItemAnimations(maskShaderId, info); m_indentLevel--; stream() << "}"; } bool QQuickQmlGenerator::generateDefsNode(const NodeInfo &info) { Q_UNUSED(info) return false; } void QQuickQmlGenerator::generateImageNode(const ImageNodeInfo &info) { if (!isNodeVisible(info)) return; const QFileInfo outputFileInfo(outputFileName); const QDir outputDir(outputFileInfo.absolutePath()); QString filePath; if (!m_retainFilePaths || info.externalFileReference.isEmpty()) { filePath = m_assetFileDirectory; if (filePath.isEmpty()) filePath = outputDir.absolutePath(); if (!filePath.isEmpty() && !filePath.endsWith(u'/')) filePath += u'/'; QDir fileDir(filePath); if (!fileDir.exists()) { if (!fileDir.mkpath(QStringLiteral("."))) qCWarning(lcQuickVectorImage) << "Failed to create image resource directory:" << filePath; } filePath += QStringLiteral("%1%2.png").arg(m_assetFilePrefix.isEmpty() ? QStringLiteral("svg_asset_") : m_assetFilePrefix) .arg(info.image.cacheKey()); if (!info.image.save(filePath)) qCWarning(lcQuickVectorImage) << "Unabled to save image resource" << filePath; qCDebug(lcQuickVectorImage) << "Saving copy of IMAGE" << filePath; } else { filePath = info.externalFileReference; } const QFileInfo assetFileInfo(filePath); stream() << "Image {"; m_indentLevel++; generateNodeBase(info); stream() << "x: " << info.rect.x(); stream() << "y: " << info.rect.y(); stream() << "width: " << info.rect.width(); stream() << "height: " << info.rect.height(); stream() << "source: \"" << m_urlPrefix << outputDir.relativeFilePath(assetFileInfo.absoluteFilePath()) <<"\""; generateNodeEnd(info); } void QQuickQmlGenerator::generatePath(const PathNodeInfo &info, const QRectF &overrideBoundingRect) { if (!isNodeVisible(info)) return; if (m_inShapeItemLevel > 0) { if (!info.isDefaultTransform) qWarning() << "Skipped transform for node" << info.nodeId << "type" << info.typeName << "(this is not supposed to happen)"; optimizePaths(info, overrideBoundingRect); } else { m_inShapeItemLevel++; stream() << shapeName() << " {"; m_indentLevel++; generateNodeBase(info); if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer)) stream() << "preferredRendererType: Shape.CurveRenderer"; optimizePaths(info, overrideBoundingRect); //qCDebug(lcQuickVectorGraphics) << *node->qpath(); generateNodeEnd(info); m_inShapeItemLevel--; } } void QQuickQmlGenerator::generateGradient(const QGradient *grad) { if (grad->type() == QGradient::LinearGradient) { auto *linGrad = static_cast(grad); stream() << "fillGradient: LinearGradient {"; m_indentLevel++; QRectF gradRect(linGrad->start(), linGrad->finalStop()); stream() << "x1: " << gradRect.left(); stream() << "y1: " << gradRect.top(); stream() << "x2: " << gradRect.right(); stream() << "y2: " << gradRect.bottom(); for (auto &stop : linGrad->stops()) stream() << "GradientStop { position: " << QString::number(stop.first, 'g', 7) << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }"; m_indentLevel--; stream() << "}"; } else if (grad->type() == QGradient::RadialGradient) { auto *radGrad = static_cast(grad); stream() << "fillGradient: RadialGradient {"; m_indentLevel++; stream() << "centerX: " << radGrad->center().x(); stream() << "centerY: " << radGrad->center().y(); stream() << "centerRadius: " << radGrad->radius(); stream() << "focalX:" << radGrad->focalPoint().x(); stream() << "focalY:" << radGrad->focalPoint().y(); for (auto &stop : radGrad->stops()) stream() << "GradientStop { position: " << QString::number(stop.first, 'g', 7) << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }"; m_indentLevel--; stream() << "}"; } } void QQuickQmlGenerator::generateAnimationBindings() { QString prefix; if (Q_UNLIKELY(!isRuntimeGenerator())) prefix = QStringLiteral(".animations"); stream() << "loops: " << m_topLevelIdString << prefix << ".loops"; stream() << "paused: " << m_topLevelIdString << prefix << ".paused"; stream() << "running: true"; // We need to reset the animation when the loop count changes stream() << "onLoopsChanged: { if (running) { restart() } }"; } void QQuickQmlGenerator::generateEasing(const QQuickAnimatedProperty::PropertyAnimation &animation, int time) { if (animation.easingPerFrame.contains(time)) { QBezier bezier = animation.easingPerFrame.value(time); QPointF c1 = bezier.pt2(); QPointF c2 = bezier.pt3(); bool isLinear = (c1 == c1.transposed() && c2 == c2.transposed()); if (!isLinear) { int nextIdx = m_easings.size(); QString &id = m_easings[{c1.x(), c1.y(), c2.x(), c2.y()}]; if (id.isNull()) id = QString(QLatin1String("easing_%1")).arg(nextIdx, 2, 10, QLatin1Char('0')); stream() << "easing: " << m_topLevelIdString << "." << id; } } } void QQuickQmlGenerator::generatePropertyAnimation(const QQuickAnimatedProperty &property, const QString &targetName, const QString &propertyName, AnimationType animationType) { if (!property.isAnimated()) return; QString mainAnimationId = targetName + QStringLiteral("_") + propertyName + QStringLiteral("_animation"); mainAnimationId.replace(QLatin1Char('.'), QLatin1Char('_')); QString prefix; if (Q_UNLIKELY(!isRuntimeGenerator())) prefix = QStringLiteral(".animations"); stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }"; stream() << "ParallelAnimation {"; m_indentLevel++; stream() << "id: " << mainAnimationId; generateAnimationBindings(); for (int i = 0; i < property.animationCount(); ++i) { const QQuickAnimatedProperty::PropertyAnimation &animation = property.animation(i); stream() << "SequentialAnimation {"; m_indentLevel++; const int startOffset = animation.startOffset; if (startOffset > 0) stream() << "PauseAnimation { duration: " << startOffset << " }"; stream() << "SequentialAnimation {"; m_indentLevel++; const int repeatCount = animation.repeatCount; if (repeatCount < 0) stream() << "loops: Animation.Infinite"; else stream() << "loops: " << repeatCount; int previousTime = 0; QVariant previousValue; for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) { const int time = it.key(); const int frameTime = time - previousTime; const QVariant &value = it.value(); if (previousValue.isValid() && previousValue == value) { if (frameTime > 0) stream() << "PauseAnimation { duration: " << frameTime << " }"; } else if (animationType == AnimationType::Auto && value.typeId() == QMetaType::Bool) { // We special case bools, with PauseAnimation and then a setter at the end if (frameTime > 0) stream() << "PauseAnimation { duration: " << frameTime << " }"; stream() << "ScriptAction {"; m_indentLevel++; stream() << "script:" << targetName << "." << propertyName << " = " << value.toString(); m_indentLevel--; stream() << "}"; } else { generateAnimatedPropertySetter(targetName, propertyName, value, animation, frameTime, time, animationType); } previousTime = time; previousValue = value; } if (!(animation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd)) { stream() << "ScriptAction {"; m_indentLevel++; stream() << "script: "; switch (animationType) { case AnimationType::Auto: stream(SameLine) << targetName << "." << propertyName << " = "; break; case AnimationType::ColorOpacity: stream(SameLine) << targetName << "." << propertyName << ".a = "; break; }; QVariant value = property.defaultValue(); if (value.typeId() == QMetaType::QColor) stream(SameLine) << "\"" << value.toString() << "\""; else stream(SameLine) << value.toReal(); m_indentLevel--; stream() << "}"; } m_indentLevel--; stream() << "}"; m_indentLevel--; stream() << "}"; } m_indentLevel--; stream() << "}"; } void QQuickQmlGenerator::generateTransform(const QTransform &xf) { if (xf.isAffine()) { stream(SameLine) << "PlanarTransform.fromAffineMatrix(" << xf.m11() << ", " << xf.m12() << ", " << xf.m21() << ", " << xf.m22() << ", " << xf.dx() << ", " << xf.dy() << ")"; } else { QMatrix4x4 m(xf); stream(SameLine) << "Qt.matrix4x4("; m_indentLevel += 3; const auto *data = m.data(); for (int i = 0; i < 4; i++) { stream() << data[i] << ", " << data[i+4] << ", " << data[i+8] << ", " << data[i+12]; if (i < 3) stream(SameLine) << ", "; } stream(SameLine) << ")"; m_indentLevel -= 3; } } void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainterPath *painterPath, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect) { Q_UNUSED(pathSelector) Q_ASSERT(painterPath || quadPath); const QColor strokeColor = info.strokeStyle.color.defaultValue().value(); const bool noPen = strokeColor == QColorConstants::Transparent && !info.strokeStyle.color.isAnimated() && !info.strokeStyle.opacity.isAnimated(); if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen) return; const QColor fillColor = info.fillColor.defaultValue().value(); const bool noFill = info.grad.type() == QGradient::NoGradient && fillColor == QColorConstants::Transparent && !info.fillColor.isAnimated() && !info.fillOpacity.isAnimated(); if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill) return; if (noPen && noFill) return; auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule()); stream() << "ShapePath {"; m_indentLevel++; QString shapePathId = info.id; if (pathSelector & QQuickVectorImageGenerator::FillPath) shapePathId += QStringLiteral("_fill"); if (pathSelector & QQuickVectorImageGenerator::StrokePath) shapePathId += QStringLiteral("_stroke"); stream() << "id: " << shapePathId; if (!info.nodeId.isEmpty()) { switch (pathSelector) { case QQuickVectorImageGenerator::FillPath: stream() << "objectName: \"svg_fill_path:" << info.nodeId << "\""; break; case QQuickVectorImageGenerator::StrokePath: stream() << "objectName: \"svg_stroke_path:" << info.nodeId << "\""; break; case QQuickVectorImageGenerator::FillAndStroke: stream() << "objectName: \"svg_path:" << info.nodeId << "\""; break; } } if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) { stream() << "strokeColor: \"transparent\""; } else { stream() << "strokeColor: \"" << strokeColor.name(QColor::HexArgb) << "\""; stream() << "strokeWidth: " << info.strokeStyle.width; stream() << "capStyle: " << QQuickVectorImageGenerator::Utils::strokeCapStyleString(info.strokeStyle.lineCapStyle); stream() << "joinStyle: " << QQuickVectorImageGenerator::Utils::strokeJoinStyleString(info.strokeStyle.lineJoinStyle); stream() << "miterLimit: " << info.strokeStyle.miterLimit; if (info.strokeStyle.dashArray.length() != 0) { stream() << "strokeStyle: " << "ShapePath.DashLine"; stream() << "dashPattern: " << QQuickVectorImageGenerator::Utils::listString(info.strokeStyle.dashArray); stream() << "dashOffset: " << info.strokeStyle.dashOffset; } } QTransform fillTransform = info.fillTransform; if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) { stream() << "fillColor: \"transparent\""; } else if (info.grad.type() != QGradient::NoGradient) { generateGradient(&info.grad); if (info.grad.coordinateMode() == QGradient::ObjectMode) { QTransform objectToUserSpace; objectToUserSpace.translate(boundingRect.x(), boundingRect.y()); objectToUserSpace.scale(boundingRect.width(), boundingRect.height()); fillTransform *= objectToUserSpace; } } else { stream() << "fillColor: \"" << fillColor.name(QColor::HexArgb) << "\""; } if (!fillTransform.isIdentity()) { const QTransform &xf = fillTransform; stream() << "fillTransform: "; if (info.fillTransform.type() == QTransform::TxTranslate) stream(SameLine) << "PlanarTransform.fromTranslate(" << xf.dx() << ", " << xf.dy() << ")"; else if (info.fillTransform.type() == QTransform::TxScale && !xf.dx() && !xf.dy()) stream(SameLine) << "PlanarTransform.fromScale(" << xf.m11() << ", " << xf.m22() << ")"; else generateTransform(xf); } if (info.trim.enabled) { stream() << "trim.start: " << info.trim.start.defaultValue().toReal(); stream() << "trim.end: " << info.trim.end.defaultValue().toReal(); stream() << "trim.offset: " << info.trim.offset.defaultValue().toReal(); } if (fillRule == QQuickShapePath::WindingFill) stream() << "fillRule: ShapePath.WindingFill"; else stream() << "fillRule: ShapePath.OddEvenFill"; QString hintStr; if (quadPath) hintStr = QQuickVectorImageGenerator::Utils::pathHintString(*quadPath); if (!hintStr.isEmpty()) stream() << hintStr; QQuickAnimatedProperty pathFactor(QVariant::fromValue(0)); QString pathId = shapePathId + "_ip"_L1; if (!info.path.isAnimated()) { QString svgPathString = painterPath ? QQuickVectorImageGenerator::Utils::toSvgString(*painterPath) : QQuickVectorImageGenerator::Utils::toSvgString(*quadPath); stream() << "PathSvg { path: \"" << svgPathString << "\" }"; } else { stream() << "PathInterpolated {"; m_indentLevel++; stream() << "id: " << pathId; stream() << "svgPaths: ["; m_indentLevel++; QQuickAnimatedProperty::PropertyAnimation pathFactorAnim = info.path.animation(0); auto &frames = pathFactorAnim.frames; int pathIdx = 0; for (auto it = frames.begin(); it != frames.end(); ++it) { QString svg = QQuickVectorImageGenerator::Utils::toSvgString(it->value()); stream() << "\"" << svg << "\""; if (pathIdx < frames.size() - 1) stream(SameLine) << ","; *it = QVariant::fromValue(pathIdx++); } pathFactor.addAnimation(pathFactorAnim); m_indentLevel--; stream() << "]"; m_indentLevel--; stream() << "}"; } m_indentLevel--; stream() << "}"; if (pathFactor.isAnimated()) generatePropertyAnimation(pathFactor, pathId, "factor"_L1); if (info.trim.enabled) { generatePropertyAnimation(info.trim.start, shapePathId + QStringLiteral(".trim"), QStringLiteral("start")); generatePropertyAnimation(info.trim.end, shapePathId + QStringLiteral(".trim"), QStringLiteral("end")); generatePropertyAnimation(info.trim.offset, shapePathId + QStringLiteral(".trim"), QStringLiteral("offset")); } generatePropertyAnimation(info.strokeStyle.color, shapePathId, QStringLiteral("strokeColor")); generatePropertyAnimation(info.strokeStyle.opacity, shapePathId, QStringLiteral("strokeColor"), AnimationType::ColorOpacity); generatePropertyAnimation(info.fillColor, shapePathId, QStringLiteral("fillColor")); generatePropertyAnimation(info.fillOpacity, shapePathId, QStringLiteral("fillColor"), AnimationType::ColorOpacity); } void QQuickQmlGenerator::generateNode(const NodeInfo &info) { if (!isNodeVisible(info)) return; stream() << "// Missing Implementation for SVG Node: " << info.typeName; stream() << "// Adding an empty Item and skipping"; stream() << "Item {"; m_indentLevel++; generateNodeBase(info); generateNodeEnd(info); } void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info) { if (!isNodeVisible(info)) return; static int counter = 0; stream() << "Item {"; m_indentLevel++; generateNodeBase(info); if (!info.isTextArea) stream() << "Item { id: textAlignItem_" << counter << "; x: " << info.position.x() << "; y: " << info.position.y() << "}"; stream() << "Text {"; m_indentLevel++; const QString textItemId = QStringLiteral("_qt_textItem_%1").arg(counter); stream() << "id: " << textItemId; generatePropertyAnimation(info.fillColor, textItemId, QStringLiteral("color")); generatePropertyAnimation(info.fillOpacity, textItemId, QStringLiteral("color"), AnimationType::ColorOpacity); generatePropertyAnimation(info.strokeColor, textItemId, QStringLiteral("styleColor")); generatePropertyAnimation(info.strokeOpacity, textItemId, QStringLiteral("styleColor"), AnimationType::ColorOpacity); if (info.isTextArea) { stream() << "x: " << info.position.x(); stream() << "y: " << info.position.y(); if (info.size.width() > 0) stream() << "width: " << info.size.width(); if (info.size.height() > 0) stream() << "height: " << info.size.height(); stream() << "wrapMode: Text.Wrap"; // ### WordWrap? verify with SVG standard stream() << "clip: true"; //### Not exactly correct: should clip on the text level, not the pixel level } else { QString hAlign = QStringLiteral("left"); stream() << "anchors.baseline: textAlignItem_" << counter << ".top"; switch (info.alignment) { case Qt::AlignHCenter: hAlign = QStringLiteral("horizontalCenter"); break; case Qt::AlignRight: hAlign = QStringLiteral("right"); break; default: qCDebug(lcQuickVectorImage) << "Unexpected text alignment" << info.alignment; Q_FALLTHROUGH(); case Qt::AlignLeft: break; } stream() << "anchors." << hAlign << ": textAlignItem_" << counter << ".left"; } counter++; stream() << "color: \"" << info.fillColor.defaultValue().value().name(QColor::HexArgb) << "\""; stream() << "textFormat:" << (info.needsRichText ? "Text.RichText" : "Text.StyledText"); QString s = info.text; s.replace(QLatin1Char('"'), QLatin1String("\\\"")); stream() << "text: \"" << s << "\""; stream() << "font.family: \"" << info.font.family() << "\""; if (info.font.pixelSize() > 0) stream() << "font.pixelSize:" << info.font.pixelSize(); else if (info.font.pointSize() > 0) stream() << "font.pixelSize:" << info.font.pointSizeF(); if (info.font.underline()) stream() << "font.underline: true"; if (info.font.weight() != QFont::Normal) stream() << "font.weight: " << int(info.font.weight()); if (info.font.italic()) stream() << "font.italic: true"; switch (info.font.hintingPreference()) { case QFont::PreferFullHinting: stream() << "font.hintingPreference: Font.PreferFullHinting"; break; case QFont::PreferVerticalHinting: stream() << "font.hintingPreference: Font.PreferVerticalHinting"; break; case QFont::PreferNoHinting: stream() << "font.hintingPreference: Font.PreferNoHinting"; break; case QFont::PreferDefaultHinting: stream() << "font.hintingPreference: Font.PreferDefaultHinting"; break; }; const QColor strokeColor = info.strokeColor.defaultValue().value(); if (strokeColor != QColorConstants::Transparent || info.strokeColor.isAnimated()) { stream() << "styleColor: \"" << strokeColor.name(QColor::HexArgb) << "\""; stream() << "style: Text.Outline"; } m_indentLevel--; stream() << "}"; generateNodeEnd(info); } void QQuickQmlGenerator::generateUseNode(const UseNodeInfo &info) { if (!isNodeVisible(info)) return; if (info.stage == StructureNodeStage::Start) { stream() << "Item {"; m_indentLevel++; generateNodeBase(info); } else { generateNodeEnd(info); } } void QQuickQmlGenerator::generatePathContainer(const StructureNodeInfo &info) { Q_UNUSED(info); stream() << shapeName() <<" {"; m_indentLevel++; if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer)) stream() << "preferredRendererType: Shape.CurveRenderer"; m_indentLevel--; m_inShapeItemLevel++; } void QQuickQmlGenerator::generateAnimateMotionPath(const QString &targetName, const QQuickAnimatedProperty &property) { if (!property.isAnimated()) return; Q_ASSERT(property.animationCount() == 1); const auto &animation = property.animation(0); qsizetype count = 0; for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it, ++count) { QPainterPath path = it.value().value(); // If the animation starts with an empty path (a pause animation), then we get the first // valid path and use this, just to get the correct position and angle at the beginning. // This path is never actually interpolated over, because we use a PauseAnimation later // instead of a PropertyAnimation, but it is used as the default start position qreal angle = 0.0; QPointF position; if (path.isEmpty() && it == animation.frames.constBegin()) { for (auto jt = std::next(it); jt != animation.frames.constEnd(); ++jt) { const QPainterPath &nextPath = jt.value().value(); if (!nextPath.isEmpty()) { position = nextPath.pointAtPercent(0.0); angle = -nextPath.angleAtPercent(0.0); break; } } stream() << "QtObject {"; m_indentLevel++; stream() << "id: " << targetName << "_pathInterpolator_" << count; stream() << "property real x: " << position.x(); stream() << "property real y: " << position.y(); stream() << "property real angle: " << angle; m_indentLevel--; stream() << "}"; } else if (!path.isEmpty()) { stream() << "PathInterpolator {"; m_indentLevel++; stream() << "id: " << targetName << "_pathInterpolator_" << count; const QString svgPathString = QQuickVectorImageGenerator::Utils::toSvgString(path); stream() << "path: Path { PathSvg { path: \"" << svgPathString << "\" } }"; m_indentLevel--; stream() << "}"; } } const QString mainAnimationId = targetName + QStringLiteral("_motion_animation"); QString prefix; if (Q_UNLIKELY(!isRuntimeGenerator())) prefix = QStringLiteral(".animations"); stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }"; stream() << "SequentialAnimation {"; m_indentLevel++; stream() << "id: " << mainAnimationId; stream() << "property var currentInterpolator: " << targetName << "_pathInterpolator_0"; generateAnimationBindings(); Q_ASSERT(property.animationCount() == 1); int previousTime = 0; count = 0; for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it, ++count) { const int time = it.key(); const int frameTime = time - previousTime; const QPainterPath path = it.value().value(); if (frameTime > 0) { if (path.isEmpty()) { stream() << "PauseAnimation { duration: " << frameTime << " }"; } else { stream() << "ScriptAction {"; m_indentLevel++; stream() << "script: {"; m_indentLevel++; stream() << mainAnimationId << ".currentInterpolator = " << targetName << "_pathInterpolator_" << count; m_indentLevel--; stream() << "}"; m_indentLevel--; stream() << "}"; stream() << "PropertyAnimation {"; m_indentLevel++; stream() << "id: " << targetName << "_motionAnimation_" << count; stream() << "duration: " << frameTime; stream() << "target: " << targetName << "_pathInterpolator_" << count; stream() << "property: \"progress\""; stream() << "from: 0; to: 1"; generateEasing(animation, time); m_indentLevel--; stream() << "}"; } } previousTime = time; } m_indentLevel--; stream() << "}"; } void QQuickQmlGenerator::generateAnimatedPropertySetter(const QString &targetName, const QString &propertyName, const QVariant &value, const QQuickAnimatedProperty::PropertyAnimation &animation, int frameTime, int time, AnimationType animationType) { if (frameTime > 0) { switch (animationType) { case AnimationType::Auto: if (value.typeId() == QMetaType::QColor) stream() << "ColorAnimation {"; else stream() << "PropertyAnimation {"; break; case AnimationType::ColorOpacity: stream() << "ColorOpacityAnimation {"; break; }; m_indentLevel++; stream() << "duration: " << frameTime; stream() << "target: " << targetName; stream() << "property: \"" << propertyName << "\""; stream() << "to: "; if (value.typeId() == QMetaType::QVector3D) { const QVector3D &v = value.value(); stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")"; } else if (value.typeId() == QMetaType::QColor) { stream(SameLine) << "\"" << value.toString() << "\""; } else { stream(SameLine) << value.toReal(); } generateEasing(animation, time); m_indentLevel--; stream() << "}"; } else { stream() << "ScriptAction {"; m_indentLevel++; stream() << "script:" << targetName << "." << propertyName; if (animationType == AnimationType::ColorOpacity) stream(SameLine) << ".a"; stream(SameLine) << " = "; if (value.typeId() == QMetaType::QVector3D) { const QVector3D &v = value.value(); stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")"; } else if (value.typeId() == QMetaType::QColor) { stream(SameLine) << "\"" << value.toString() << "\""; } else { stream(SameLine) << value.toReal(); } m_indentLevel--; stream() << "}"; } } void QQuickQmlGenerator::generateAnimateTransform(const QString &targetName, const NodeInfo &info) { if (!info.transform.isAnimated()) return; const QString mainAnimationId = targetName + QStringLiteral("_transform_animation"); QString prefix; if (Q_UNLIKELY(!isRuntimeGenerator())) prefix = QStringLiteral(".animations"); stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }"; stream() << "ParallelAnimation {"; m_indentLevel++; stream() << "id:" << mainAnimationId; generateAnimationBindings(); for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) { int animationStart = info.transform.animationGroup(groupIndex); int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount() ? info.transform.animationGroup(groupIndex + 1) : info.transform.animationCount(); // The first animation in the group holds the shared properties for the whole group const QQuickAnimatedProperty::PropertyAnimation &firstAnimation = info.transform.animation(animationStart); const bool freeze = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd; const bool replace = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::ReplacePreviousAnimations; stream() << "SequentialAnimation {"; m_indentLevel++; const int startOffset = firstAnimation.startOffset; if (startOffset > 0) stream() << "PauseAnimation { duration: " << startOffset << " }"; const int repeatCount = firstAnimation.repeatCount; if (repeatCount < 0) stream() << "loops: Animation.Infinite"; else stream() << "loops: " << repeatCount; if (replace) { stream() << "ScriptAction {"; m_indentLevel++; stream() << "script: " << targetName << "_transform_base_group" << ".activateOverride(" << targetName << "_transform_group_" << groupIndex << ")"; m_indentLevel--; stream() << "}"; } stream() << "ParallelAnimation {"; m_indentLevel++; for (int i = animationStart; i < nextAnimationStart; ++i) { const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i); if (animation.isConstant()) continue; bool hasRotationCenter = false; if (animation.subtype == QTransform::TxRotate) { for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) { const QPointF center = it->value().value(0).value(); if (!center.isNull()) { hasRotationCenter = true; break; } } } stream() << "SequentialAnimation {"; m_indentLevel++; int previousTime = 0; QVariantList previousParameters; for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) { const int time = it.key(); const int frameTime = time - previousTime; const QVariantList ¶meters = it.value().value(); if (parameters.isEmpty()) continue; if (parameters == previousParameters) { if (frameTime > 0) stream() << "PauseAnimation { duration: " << frameTime << " }"; } else { stream() << "ParallelAnimation {"; m_indentLevel++; const QString propertyTargetName = targetName + QStringLiteral("_transform_") + QString::number(groupIndex) + QStringLiteral("_") + QString::number(i); switch (animation.subtype) { case QTransform::TxTranslate: { const QPointF translation = parameters.first().value(); generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("x"), translation.x(), animation, frameTime, time); generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("y"), translation.y(), animation, frameTime, time); break; } case QTransform::TxScale: { const QPointF scale = parameters.first().value(); generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("xScale"), scale.x(), animation, frameTime, time); generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("yScale"), scale.y(), animation, frameTime, time); break; } case QTransform::TxRotate: { Q_ASSERT(parameters.size() == 2); const qreal angle = parameters.value(1).toReal(); if (hasRotationCenter) { const QPointF center = parameters.value(0).value(); generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("origin"), QVector3D(center.x(), center.y(), 0.0), animation, frameTime, time); } generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("angle"), angle, animation, frameTime, time); break; } case QTransform::TxShear: { const QPointF skew = parameters.first().value(); generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("xAngle"), skew.x(), animation, frameTime, time); generateAnimatedPropertySetter(propertyTargetName, QStringLiteral("yAngle"), skew.y(), animation, frameTime, time); break; } default: Q_UNREACHABLE(); } m_indentLevel--; stream() << "}"; // Parallel key frame animation } previousTime = time; previousParameters = parameters; } m_indentLevel--; stream() << "}"; // Parallel key frame animation } m_indentLevel--; stream() << "}"; // Parallel key frame animation // If the animation ever finishes, then we add an action on the end that handles itsr // freeze state if (firstAnimation.repeatCount >= 0) { stream() << "ScriptAction {"; m_indentLevel++; stream() << "script: {"; m_indentLevel++; if (!freeze) { stream() << targetName << "_transform_base_group.deactivate(" << targetName << "_transform_group_" << groupIndex << ")"; } else if (!replace) { stream() << targetName << "_transform_base_group.deactivateOverride(" << targetName << "_transform_group_" << groupIndex << ")"; } m_indentLevel--; stream() << "}"; m_indentLevel--; stream() << "}"; } m_indentLevel--; stream() << "}"; } m_indentLevel--; stream() << "}"; } bool QQuickQmlGenerator::generateStructureNode(const StructureNodeInfo &info) { if (!isNodeVisible(info)) return false; const bool isPathContainer = !info.forceSeparatePaths && info.isPathContainer; if (info.stage == StructureNodeStage::Start) { if (!info.clipBox.isEmpty()) { stream() << "Item { // Clip"; m_indentLevel++; stream() << "width: " << info.clipBox.width(); stream() << "height: " << info.clipBox.height(); stream() << "clip: true"; } if (isPathContainer) { generatePathContainer(info); } else if (!info.customItemType.isEmpty()) { stream() << info.customItemType << " {"; } else { stream() << "Item { // Structure node"; } m_indentLevel++; if (!info.viewBox.isEmpty()) { stream() << "transform: ["; m_indentLevel++; bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y()); if (translate) stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },"; stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }"; m_indentLevel--; stream() << "]"; } generateNodeBase(info); } else { generateNodeEnd(info); if (isPathContainer) m_inShapeItemLevel--; if (!info.clipBox.isEmpty()) { m_indentLevel--; stream() << "}"; } } return true; } bool QQuickQmlGenerator::generateMaskNode(const MaskNodeInfo &info) { // Generate an invisible item subtree which can be used in ShaderEffectSource if (info.stage == StructureNodeStage::Start) { stream() << "Item {"; m_indentLevel++; generateNodeBase(info); stream() << "visible: false"; stream() << "width: originalBounds.width"; stream() << "height: originalBounds.height"; stream() << "property real maskX: " << info.maskRect.left(); stream() << "property real maskY: " << info.maskRect.top(); stream() << "property real maskWidth: " << info.maskRect.width(); stream() << "property real maskHeight: " << info.maskRect.height(); stream() << "function maskRect(other) {"; m_indentLevel++; stream() << "return "; if (info.isMaskRectRelativeCoordinates) { stream(SameLine) << "Qt.rect(" << info.id << ".maskX * other.originalBounds.width + other.originalBounds.x," << info.id << ".maskY * other.originalBounds.height + other.originalBounds.y," << info.id << ".maskWidth * other.originalBounds.width," << info.id << ".maskHeight * other.originalBounds.height)"; } else { stream(SameLine) << "Qt.rect(" << info.id << ".maskX, " << info.id << ".maskY, " << info.id << ".maskWidth, " << info.id << ".maskHeight)"; } m_indentLevel--; stream() << "}"; } else { generateNodeEnd(info); } return true; } bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info) { const QStringList comments = m_commentString.split(u'\n'); if (!isNodeVisible(info)) { m_indentLevel = 0; if (comments.isEmpty()) { stream() << "// Generated from SVG"; } else { for (const auto &comment : comments) stream() << "// " << comment; } stream() << "import QtQuick"; stream() << "import QtQuick.Shapes" << Qt::endl; stream() << "Item {"; m_indentLevel++; double w = info.size.width(); double h = info.size.height(); if (w > 0) stream() << "implicitWidth: " << w; if (h > 0) stream() << "implicitHeight: " << h; m_indentLevel--; stream() << "}"; return false; } if (info.stage == StructureNodeStage::Start) { m_indentLevel = 0; if (comments.isEmpty()) stream() << "// Generated from SVG"; else for (const auto &comment : comments) stream() << "// " << comment; stream() << "import QtQuick"; stream() << "import QtQuick.VectorImage"; stream() << "import QtQuick.VectorImage.Helpers"; stream() << "import QtQuick.Shapes"; for (const auto &import : std::as_const(m_extraImports)) stream() << "import " << import; stream() << Qt::endl << "Item {"; m_indentLevel++; double w = info.size.width(); double h = info.size.height(); if (w > 0) stream() << "implicitWidth: " << w; if (h > 0) stream() << "implicitHeight: " << h; if (Q_UNLIKELY(!isRuntimeGenerator())) { stream() << "component AnimationsInfo : QtObject"; stream() << "{"; m_indentLevel++; } stream() << "property bool paused: false"; stream() << "property int loops: 1"; stream() << "signal restart()"; if (Q_UNLIKELY(!isRuntimeGenerator())) { m_indentLevel--; stream() << "}"; stream() << "property AnimationsInfo animations : AnimationsInfo {}"; } if (!info.viewBox.isEmpty()) { stream() << "transform: ["; m_indentLevel++; bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y()); if (translate) stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },"; stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }"; m_indentLevel--; stream() << "]"; } if (!info.forceSeparatePaths && info.isPathContainer) { m_topLevelIdString = QStringLiteral("__qt_toplevel"); stream() << "id: " << m_topLevelIdString; generatePathContainer(info); m_indentLevel++; generateNodeBase(info); } else { m_topLevelIdString = generateNodeBase(info); if (m_topLevelIdString.isEmpty()) qCWarning(lcQuickVectorImage) << "No ID specified for top level item"; } } else { if (m_inShapeItemLevel > 0) { m_inShapeItemLevel--; m_indentLevel--; stream() << "}"; } for (const auto [coords, id] : m_easings.asKeyValueRange()) { stream() << "property easingCurve " << id << ": { type: Easing.BezierSpline; bezierCurve: [ "; for (auto coord : coords) stream(SameLine) << coord << ", "; stream(SameLine) << "1, 1 ] }"; } generateNodeEnd(info); stream().flush(); } return true; } QStringView QQuickQmlGenerator::indent() { static QString indentString; int indentWidth = m_indentLevel * 4; if (indentWidth > indentString.size()) indentString.fill(QLatin1Char(' '), indentWidth * 2); return QStringView(indentString).first(indentWidth); } QTextStream &QQuickQmlGenerator::stream(int flags) { if (m_stream.device() == nullptr) m_stream.setDevice(&m_result); else if (!(flags & StreamFlags::SameLine)) m_stream << Qt::endl << indent(); return m_stream; } const char *QQuickQmlGenerator::shapeName() const { return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData(); } QT_END_NAMESPACE