Support fill-opacity and stroke-opacity in VectorImage

The alpha value of the fill and stroke colors can be animated
separately in SVG. In order to support this, we introduce a
specialized ColorOpacityAnimation type in a Helpers library
which only overwrites the alpha channel of the target property.

This requires an extra hook in the animation frame work which
allows us to get the current value of the property. It should
have minimal impact on any existing code, but may have
additional use cases later, when we implement support for
additive color animations for instance. Since the interpolator
API in QVariantAnimation is public API, we add a secondary,
private API for this. If we see use for it in the future,
this could mature to a public API as well.

Fixes: QTBUG-135322
Change-Id: I803f4e64c41e9d6dc355f2468233661885aa7f15
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
This commit is contained in:
Eskil Abrahamsen Blomfeldt 2025-04-01 14:24:53 +02:00
parent f52428e600
commit 5f3b613b2e
11 changed files with 277 additions and 18 deletions

View File

@ -2709,7 +2709,7 @@ void QQuickAnimationPropertyUpdater::setValue(qreal v)
for (int ii = 0; ii < actions.size(); ++ii) {
QQuickStateAction &action = actions[ii];
if (v == 1.) {
if (v == qreal(1.0) && extendedInterpolator == nullptr) {
QQmlPropertyPrivate::write(action.property, action.toValue, QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding);
} else {
if (!fromIsSourced && !fromIsDefined) {
@ -2725,8 +2725,21 @@ void QQuickAnimationPropertyUpdater::setValue(qreal v)
interpolator = QVariantAnimationPrivate::getInterpolator(prevInterpolatorType);
}
}
if (interpolator)
QQmlPropertyPrivate::write(action.property, interpolator(action.fromValue.constData(), action.toValue.constData(), v), QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding);
QVariant interpolated;
if (extendedInterpolator) {
QVariant current = action.property.read();
interpolated = extendedInterpolator(action.fromValue.constData(),
action.toValue.constData(),
current,
v);
} else if (interpolator) {
interpolated = interpolator(action.fromValue.constData(), action.toValue.constData(), v);
}
QQmlPropertyPrivate::write(action.property,
interpolated,
QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding);
}
if (deleted)
return;
@ -2880,6 +2893,7 @@ QAbstractAnimationJob* QQuickPropertyAnimation::transition(QQuickStateActions &a
QQuickAnimationPropertyUpdater *data = new QQuickAnimationPropertyUpdater;
data->interpolatorType = d->interpolatorType;
data->interpolator = d->interpolator;
data->extendedInterpolator = d->extendedInterpolator;
data->reverse = direction == Backward ? true : false;
data->fromIsSourced = false;
data->fromIsDefined = d->fromIsDefined;

View File

@ -240,7 +240,7 @@ class Q_QUICK_EXPORT QQuickPropertyAnimationPrivate : public QQuickAbstractAnima
public:
QQuickPropertyAnimationPrivate()
: QQuickAbstractAnimationPrivate(), target(nullptr), fromIsDefined(false), toIsDefined(false), ourPropertiesDirty(false),
defaultToInterpolatorType(0), interpolatorType(0), interpolator(nullptr), duration(250), actions(nullptr) {}
defaultToInterpolatorType(0), interpolatorType(0), interpolator(nullptr), extendedInterpolator(nullptr), duration(250), actions(nullptr) {}
void animationCurrentLoopChanged(QAbstractAnimationJob *job) override;
@ -260,13 +260,14 @@ public:
bool defaultToInterpolatorType:1;
int interpolatorType;
QVariantAnimation::Interpolator interpolator;
typedef QVariant (*ExtendedInterpolator)(const void *from, const void *to, const QVariant &currentValue, qreal progress);
ExtendedInterpolator extendedInterpolator;
int duration;
QEasingCurve easing;
// for animations that don't use the QQuickBulkValueAnimator
QQuickStateActions *actions;
static QVariant interpolateVariant(const QVariant &from, const QVariant &to, qreal progress);
static void convertVariant(QVariant &variant, QMetaType type);
};
@ -282,7 +283,7 @@ public:
class Q_AUTOTEST_EXPORT QQuickAnimationPropertyUpdater : public QQuickBulkValueUpdater
{
public:
QQuickAnimationPropertyUpdater() : interpolatorType(0), interpolator(nullptr), prevInterpolatorType(0), reverse(false), fromIsSourced(false), fromIsDefined(false), wasDeleted(nullptr) {}
QQuickAnimationPropertyUpdater() : interpolatorType(0), interpolator(nullptr), extendedInterpolator(nullptr), prevInterpolatorType(0), reverse(false), fromIsSourced(false), fromIsDefined(false), wasDeleted(nullptr) {}
~QQuickAnimationPropertyUpdater() override;
void setValue(qreal v) override;
@ -292,6 +293,7 @@ public:
QQuickStateActions actions;
int interpolatorType; //for Number/ColorAnimation
QVariantAnimation::Interpolator interpolator;
QQuickPropertyAnimationPrivate::ExtendedInterpolator extendedInterpolator;
int prevInterpolatorType; //for generic
bool reverse;
bool fromIsSourced;

View File

@ -28,6 +28,7 @@ qt_internal_add_qml_module(QuickVectorImage
VERSION "${PROJECT_VERSION}"
PLUGIN_TARGET qquickvectorimageplugin
CLASS_NAME QtQuickVectorImagePlugin
IMPORTS QtQuick.VectorImage.Helpers
SOURCES
qquickvectorimage_p.h qquickvectorimage.cpp
qquickvectorimage_p_p.h
@ -36,3 +37,16 @@ qt_internal_add_qml_module(QuickVectorImage
Qt::QuickVectorImageGeneratorPrivate
Qt::SvgPrivate
)
qt_internal_add_qml_module(QuickVectorImageHelpers
URI "QtQuick.VectorImage.Helpers"
VERSION "${PROJECT_VERSION}"
PLUGIN_TARGET qquickvectorimagehelpersplugin
NO_PLUGIN_OPTIONAL
CLASS_NAME QtQuickVectorImageHelpersPlugin
SOURCES
helpers/qquickcoloropacityanimation_p.h helpers/qquickcoloropacityanimation.cpp
LIBRARIES
Qt::QuickPrivate
Qt::QuickVectorImageGeneratorPrivate
)

View File

@ -53,6 +53,7 @@ struct StrokeStyle
qreal dashOffset = 0;
QList<qreal> dashArray;
QQuickAnimatedProperty color = QQuickAnimatedProperty(QVariant::fromValue(QColorConstants::Transparent));
QQuickAnimatedProperty opacity = QQuickAnimatedProperty(QVariant::fromValue(qreal(1.0)));
qreal width = 1.0;
static StrokeStyle fromPen(const QPen &p)
@ -74,6 +75,7 @@ struct PathNodeInfo : NodeInfo
QPainterPath painterPath;
Qt::FillRule fillRule = Qt::FillRule::WindingFill;
QQuickAnimatedProperty fillColor = QQuickAnimatedProperty(QVariant::fromValue(QColor{}));
QQuickAnimatedProperty fillOpacity = QQuickAnimatedProperty(QVariant::fromValue(qreal(1.0)));
StrokeStyle strokeStyle;
QGradient grad;
QTransform fillTransform;
@ -89,7 +91,9 @@ struct TextNodeInfo : NodeInfo
QFont font;
Qt::Alignment alignment;
QQuickAnimatedProperty fillColor = QQuickAnimatedProperty(QVariant::fromValue(QColor{}));
QQuickAnimatedProperty fillOpacity = QQuickAnimatedProperty(QVariant::fromValue(qreal(1.0)));
QQuickAnimatedProperty strokeColor = QQuickAnimatedProperty(QVariant::fromValue(QColor{}));
QQuickAnimatedProperty strokeOpacity = QQuickAnimatedProperty(QVariant::fromValue(qreal(1.0)));
};
struct AnimateColorNodeInfo : NodeInfo

View File

@ -260,7 +260,8 @@ void QQuickQmlGenerator::generateGradient(const QGradient *grad)
void QQuickQmlGenerator::generatePropertyAnimation(const QQuickAnimatedProperty &property,
const QString &targetName,
const QString &propertyName)
const QString &propertyName,
AnimationType animationType)
{
if (property.animationCount() > 1) {
stream() << "ParallelAnimation {";
@ -295,10 +296,17 @@ void QQuickQmlGenerator::generatePropertyAnimation(const QQuickAnimatedProperty
const int time = it.key();
const QVariant &value = it.value();
if (value.typeId() == QMetaType::QColor)
stream() << "ColorAnimation {";
else
stream() << "PropertyAnimation {";
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() << "target: " << targetName;
@ -318,13 +326,23 @@ void QQuickQmlGenerator::generatePropertyAnimation(const QQuickAnimatedProperty
if (!(animation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd)) {
stream() << "ScriptAction {";
m_indentLevel++;
stream() << "script: " << targetName << "." << propertyName << " = ";
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() << "}";
}
@ -372,14 +390,17 @@ void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainte
static int counter = 0;
const QColor strokeColor = info.strokeStyle.color.defaultValue().value<QColor>();
const bool noPen = strokeColor == QColorConstants::Transparent && !info.strokeStyle.color.isAnimated();
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<QColor>();
const bool noFill = info.grad.type() == QGradient::NoGradient
&& fillColor == QColorConstants::Transparent
&& !info.fillColor.isAnimated();
&& !info.fillColor.isAnimated()
&& !info.fillOpacity.isAnimated();
if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill)
return;
@ -464,7 +485,9 @@ void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainte
stream() << "}";
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);
counter++;
}
@ -504,7 +527,9 @@ void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info)
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();
@ -886,6 +911,7 @@ bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info)
stream() << "// " << comment;
stream() << "import QtQuick";
stream() << "import QtQuick.VectorImage.Helpers";
stream() << "import QtQuick.Shapes" << Qt::endl;
stream() << "Item {";
m_indentLevel++;

View File

@ -96,9 +96,16 @@ private:
void generateTransform(const QTransform &xf);
void generatePathContainer(const StructureNodeInfo &info);
void generateAnimateTransform(const QString &targetName, const NodeInfo &info);
enum class AnimationType {
Auto = 0,
ColorOpacity = 1
};
void generatePropertyAnimation(const QQuickAnimatedProperty &property,
const QString &targetName,
const QString &propertyName);
const QString &propertyName,
AnimationType animationType = AnimationType::Auto);
QStringView indent();
enum StreamFlags { NoFlags = 0x0, SameLine = 0x1 };

View File

@ -939,12 +939,24 @@ void QSvgVisitorImpl::visitTextNode(const QSvgText *node)
applyAnimationsToProperty(animations, &info.fillColor, calculateInterpolatedValue);
}
{
QList<AnimationPair> animations = collectAnimations(node, QStringLiteral("fill-opacity"));
if (!animations.isEmpty())
applyAnimationsToProperty(animations, &info.fillOpacity, calculateInterpolatedValue);
}
{
QList<AnimationPair> animations = collectAnimations(node, QStringLiteral("stroke"));
if (!animations.isEmpty())
applyAnimationsToProperty(animations, &info.strokeColor, calculateInterpolatedValue);
}
{
QList<AnimationPair> animations = collectAnimations(node, QStringLiteral("stroke-opacity"));
if (!animations.isEmpty())
applyAnimationsToProperty(animations, &info.strokeOpacity, calculateInterpolatedValue);
}
info.position = node->position();
info.size = node->size();
info.font = font;
@ -1214,11 +1226,23 @@ void QSvgVisitorImpl::fillColorAnimationInfo(const QSvgNode *node, PathNodeInfo
applyAnimationsToProperty(animations, &info.fillColor, calculateInterpolatedValue);
}
{
QList<AnimationPair> animations = collectAnimations(node, QStringLiteral("fill-opacity"));
if (!animations.isEmpty())
applyAnimationsToProperty(animations, &info.fillOpacity, calculateInterpolatedValue);
}
{
QList<AnimationPair> animations = collectAnimations(node, QStringLiteral("stroke"));
if (!animations.isEmpty())
applyAnimationsToProperty(animations, &info.strokeStyle.color, calculateInterpolatedValue);
}
{
QList<AnimationPair> animations = collectAnimations(node, QStringLiteral("stroke-opacity"));
if (!animations.isEmpty())
applyAnimationsToProperty(animations, &info.strokeStyle.opacity, calculateInterpolatedValue);
}
}
void QSvgVisitorImpl::fillTransformAnimationInfo(const QSvgNode *node, NodeInfo &info)

View File

@ -0,0 +1,103 @@
// Copyright (C) 2025 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 "qquickcoloropacityanimation_p.h"
#include <QtQuick/private/qquickanimation_p_p.h>
QT_BEGIN_NAMESPACE
/*!
\qmlmodule QtQuick.VectorImage.Helpers
\title Qt Quick Vector Image Helpers QML Types
\ingroup qmlmodules
\brief Provides QML types used by VectorImage and related tools.
\since 6.10
This module contains types used in scenes generated by \l{VectorImage}, \l{svgtoqml} and related
tools. The types are made to replicate specialized behavior defined by the vector graphics file
formats it loads and are not intended to be generally useful.
To use the types in this module, import the module with the following line:
\qml
import QtQuick.VectorImage.Helpers
\endqml
\section1 QML Types
*/
static QVariant opacityInterpolator(const QColor &from,
const QColor &to,
const QVariant &current,
qreal progress)
{
QColor color = current.value<QColor>();
qreal fromAlpha = from.alphaF();
qreal toAlpha = to.alphaF();
color.setAlphaF(fromAlpha + (toAlpha - fromAlpha) * progress);
return QVariant::fromValue(color);
}
/*!
\qmltype ColorOpacityAnimation
\inqmlmodule QtQuick.VectorImage.Helpers
\inherits Item
\brief Animates the alpha value of a color without modifying the rest of the color.
This type will animate the alpha value of a color without modifying the other components. It
is used to support the \c fill-opacity and \c stroke-opacity properties of SVG, which can be
animated separately from the opacity of the shape itself.
*/
QQuickColorOpacityAnimation::QQuickColorOpacityAnimation(QObject *parent)
: QQuickPropertyAnimation(parent)
{
Q_D(QQuickPropertyAnimation);
d->defaultToInterpolatorType = true;
d->interpolatorType = QMetaType::QColor;
d->extendedInterpolator = reinterpret_cast<QQuickPropertyAnimationPrivate::ExtendedInterpolator>(reinterpret_cast<void(*)()>(opacityInterpolator));
}
/*!
\qmlproperty real QtQuick.VectorImage.Helpers::ColorOpacityAnimation::from
The value of the color's alpha channel at the start of the animation.
*/
qreal QQuickColorOpacityAnimation::from() const
{
Q_D(const QQuickPropertyAnimation);
return d->from.value<QColor>().alphaF();
}
void QQuickColorOpacityAnimation::setFrom(qreal from)
{
QColor color = Qt::red;
color.setAlphaF(from);
QQuickPropertyAnimation::setFrom(color);
}
/*!
\qmlproperty real QtQuick.VectorImage.Helpers::ColorOpacityAnimation::to
The value of the color's alpha channel at the end of the animation.
*/
qreal QQuickColorOpacityAnimation::to() const
{
Q_D(const QQuickPropertyAnimation);
return d->to.value<QColor>().alphaF();
}
void QQuickColorOpacityAnimation::setTo(qreal to)
{
QColor color = Qt::yellow;
color.setAlphaF(to);
QQuickPropertyAnimation::setTo(color);
}
QT_END_NAMESPACE
#include <moc_qquickcoloropacityanimation_p.cpp>

View File

@ -0,0 +1,45 @@
// Copyright (C) 2025 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
#ifndef QQUICKOPACITYANIMATION_P_H
#define QQUICKOPACITYANIMATION_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtQuick/private/qquickanimation_p.h>
#include <QtQuickVectorImageHelpers/qtquickvectorimagehelpersexports.h>
QT_BEGIN_NAMESPACE
class Q_QUICKVECTORIMAGEHELPERS_EXPORT QQuickColorOpacityAnimation : public QQuickPropertyAnimation
{
Q_OBJECT
Q_DECLARE_PRIVATE(QQuickPropertyAnimation)
Q_PROPERTY(qreal from READ from WRITE setFrom)
Q_PROPERTY(qreal to READ to WRITE setTo)
QML_NAMED_ELEMENT(ColorOpacityAnimation)
public:
QQuickColorOpacityAnimation(QObject *parent = nullptr);
qreal from() const;
void setFrom(qreal from);
qreal to() const;
void setTo(qreal to);
};
QT_END_NAMESPACE
#endif // QQUICKOPACITYANIMATION_P_H

View File

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<style>
@keyframes opacity {
0% {fill-opacity: 0; stroke-opacity: 0; }
1% {fill-opacity: 0.75; stroke-opacity: 0.5; }
100% {fill-opacity: 0.75; stroke-opacity: 0.5; }
}
rect {
animation-duration: 50s;
animation-name: opacity;
animation-iteration-count: 1;
}
</style>
<rect width="100" height="100" fill="green" stroke="red" stroke-width="6"/>
</svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@ -4,18 +4,20 @@ import QtQuick.VectorImage
Rectangle {
id: topLevelItem
width: 200
height: 100
height: 200
ListModel {
id: renderers
ListElement { renderer: VectorImage.GeometryRenderer; src: "../shared/svg/animateOpacity.svg" }
ListElement { renderer: VectorImage.CurveRenderer; src: "../shared/svg/animateOpacity.svg" }
ListElement { renderer: VectorImage.GeometryRenderer; src: "../shared/svg/animateFillStrokeOpacity.svg" }
ListElement { renderer: VectorImage.CurveRenderer; src: "../shared/svg/animateFillStrokeOpacity.svg" }
}
Row {
Grid {
columns: 2
Repeater {
model: renderers
VectorImage {
layer.enabled: true
layer.samples: 4