aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndy Nichols <andy.nichols@qt.io>2023-06-28 17:32:30 +0200
committerAndy Nichols <nezticle@gmail.com>2023-12-06 12:37:10 +0100
commit0272b5215ef3bfdc591ee444c9d2614b58f14bfd (patch)
tree5250b17c670fb3bdc1fd89492f5d0731aee18d37
parent320965fe0cd07e0d06116545dfce41fd198328b1 (diff)
Add QML module QtQuick.Timeline.BlendTrees
This new QML module adds the ability to blend multiple TimelineAnimations together to create new more dynamic animations. This commit adds the following components: BlendTreeNode BlendAnimationNode TimelineAnimationNode BlendTreeNodes are a base class that produces frame data(property changes for a frame). TimelineAnimationNodes produces frameData using a TimelineAnimation. BlendAnimationNodes allows you to combine two sources of frame data, by blending based on a weight value. These can be chained together to create complex animations. By default, no property changes are actually written to the scene. If you want to commit the changes to the scene you should set the outputEnabled of any BlendTreeNode to true, and it will commit its properties to the scene. Normally, a Timeline component can only have a single animation playing at a time, and enforces this behavior. It is not uncommon to define multiple animations in a single Timeline though. For this module to work you cannot use the Timeline.animations property, since this is the mechanism that enforces that only one Animation can be played at a time. Task-number: QTBUG-113136 Change-Id: I7d6cd7beaa1edf504cbd7386979e764bd42b84ec Reviewed-by: Christian Strømme <christian.stromme@qt.io>
-rw-r--r--src/timeline/CMakeLists.txt3
-rw-r--r--src/timeline/blendtrees/CMakeLists.txt33
-rw-r--r--src/timeline/blendtrees/doc/qtquicktimelineblendtrees.qdocconf43
-rw-r--r--src/timeline/blendtrees/qblendanimationnode.cpp251
-rw-r--r--src/timeline/blendtrees/qblendanimationnode_p.h65
-rw-r--r--src/timeline/blendtrees/qblendtreenode.cpp73
-rw-r--r--src/timeline/blendtrees/qblendtreenode_p.h53
-rw-r--r--src/timeline/blendtrees/qtimelineanimationnode.cpp149
-rw-r--r--src/timeline/blendtrees/qtimelineanimationnode_p.h63
-rw-r--r--src/timeline/blendtrees/qtquicktimelineblendtreesglobal.h10
-rw-r--r--src/timeline/blendtrees/qtquicktimelineblendtreesglobal_p.h21
-rw-r--r--tests/auto/CMakeLists.txt1
-rw-r--r--tests/auto/qtquicktimeline_blendtrees/CMakeLists.txt26
-rw-r--r--tests/auto/qtquicktimeline_blendtrees/data/BlendTreeTest.qml175
-rw-r--r--tests/auto/qtquicktimeline_blendtrees/tst_blendtrees.cpp284
15 files changed, 1250 insertions, 0 deletions
diff --git a/src/timeline/CMakeLists.txt b/src/timeline/CMakeLists.txt
index a086b31..3cb8c17 100644
--- a/src/timeline/CMakeLists.txt
+++ b/src/timeline/CMakeLists.txt
@@ -30,3 +30,6 @@ qt_internal_add_qml_module(QuickTimeline
qt_internal_add_docs(QuickTimeline
doc/qtquicktimeline.qdocconf
)
+
+add_subdirectory(blendtrees)
+
diff --git a/src/timeline/blendtrees/CMakeLists.txt b/src/timeline/blendtrees/CMakeLists.txt
new file mode 100644
index 0000000..c034b5e
--- /dev/null
+++ b/src/timeline/blendtrees/CMakeLists.txt
@@ -0,0 +1,33 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_qml_module(QuickTimelineBlendTrees
+ URI "QtQuick.Timeline.BlendTrees"
+ VERSION "${PROJECT_VERSION}"
+ DESIGNER_SUPPORTED
+ CLASS_NAME QtQuickTimelineBlendTreesPlugin
+ PLUGIN_TARGET qtquicktimelineblendtreesplugin
+ DEPENDENCIES
+ QtQuickTimeline
+ SOURCES
+ qblendtreenode.cpp qblendtreenode_p.h
+ qtimelineanimationnode.cpp qtimelineanimationnode_p.h
+ qblendanimationnode.cpp qblendanimationnode_p.h
+ qtquicktimelineblendtreesglobal.h qtquicktimelineblendtreesglobal_p.h
+ DEFINES
+ QT_BUILD_QUICKTIMELINEBLENDTREES_LIB
+ PUBLIC_LIBRARIES
+ Qt::Core
+ Qt::Qml
+ Qt::Quick
+ Qt::QuickTimeline
+ LIBRARIES
+ Qt::QuickPrivate
+ Qt::QuickTimelinePrivate
+ GENERATE_CPP_EXPORTS
+ GENERATE_PRIVATE_CPP_EXPORTS
+)
+
+qt_internal_add_docs(QuickTimelineBlendTrees
+ doc/qtquicktimelineblendtrees.qdocconf
+)
diff --git a/src/timeline/blendtrees/doc/qtquicktimelineblendtrees.qdocconf b/src/timeline/blendtrees/doc/qtquicktimelineblendtrees.qdocconf
new file mode 100644
index 0000000..4848ed4
--- /dev/null
+++ b/src/timeline/blendtrees/doc/qtquicktimelineblendtrees.qdocconf
@@ -0,0 +1,43 @@
+include($QT_INSTALL_DOCS/global/qt-module-defaults.qdocconf)
+
+project = QtQuickTimelineBlendTrees
+description = Qt Quick Timeline Blend Trees Reference Documentation
+version = $QT_VERSION
+buildversion = Qt Quick Timeline Blend Trees | Commercial or GPLv3
+
+examplesinstallpath = qtquicktimelineblendtrees
+
+qhp.projects = QtQuickTimelineBlendTrees
+
+qhp.QtQuickTimelineBlendTrees.file = qtquicktimelineblendtrees.qhp
+qhp.QtQuickTimelineBlendTrees.namespace = org.qt-project.qtquicktimelineblendtrees.$QT_VERSION_TAG
+qhp.QtQuickTimelineBlendTrees.virtualFolder = qtquicktimelineblendtrees
+qhp.QtQuickTimelineBlendTrees.indexTitle = Qt Quick Timeline Blend Trees
+qhp.QtQuickTimelineBlendTrees.indexRoot =
+
+qhp.QtQuickTimelineBlendTrees.subprojects = qmltypes
+
+qhp.QtQuickTimelineBlendTrees.subprojects.qmltypes.title = QML Types
+qhp.QtQuickTimelineBlendTrees.subprojects.qmltypes.indexTitle = Qt Quick Timeline Blend Trees QML Types
+qhp.QtQuickTimelineBlendTrees.subprojects.qmltypes.selectors = qmlclass
+qhp.QtQuickTimelineBlendTrees.subprojects.qmltypes.sortPages = true
+
+#qhp.QtQuickTimelineBlendTrees.subprojects.examples.title = Examples
+#qhp.QtQuickTimelineBlendTrees.subprojects.examples.indexTitle = Qt Qt Quick Timeline Blend Trees Examples
+#qhp.QtQuickTimelineBlendTrees.subprojects.examples.selectors = fake:example
+#qhp.QtQuickTimelineBlendTrees.subprojects.examples.sortPages = true
+
+headerdirs += ..
+sourcedirs += ..
+#exampledirs =
+imagedirs += images \
+
+depends += qtcore qtdoc qtqml qtquick qtquicktimeline
+
+tagfile = qtquicktimelineblendtrees.tags
+
+#add generic thumbnail images for example documentation that does not have an image.
+#manifestmeta.thumbnail.names +=
+
+navigation.landingpage = "Qt Quick Timeline Blend Trees"
+navigation.qmltypespage = "Qt Quick Timeline Blend Trees QML Types"
diff --git a/src/timeline/blendtrees/qblendanimationnode.cpp b/src/timeline/blendtrees/qblendanimationnode.cpp
new file mode 100644
index 0000000..d763d70
--- /dev/null
+++ b/src/timeline/blendtrees/qblendanimationnode.cpp
@@ -0,0 +1,251 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "qblendanimationnode_p.h"
+
+#include <QVariant>
+#include <QVector2D>
+#include <QVector3D>
+#include <QVector4D>
+#include <QQuaternion>
+#include <QColor>
+#include <QRect>
+#include <QRectF>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype BlendAnimationNode
+ \inherits QBlendTreeNode
+ \instantiates QBlendAnimationNode
+ \inqmlmodule QtQuick.Timeline.BlendTrees
+ \ingroup qtqmltypes
+
+ \brief A blend tree node that blends between two animation sources.
+
+ BlendAnimationNode is a blend tree node that blends between two animation
+ sources based on a weight value. The weight value can be animated to
+ dynamically blend between the two animation sources.
+*/
+
+/*!
+ \qmlproperty BlendTreeNode BlendAnimationNode::source1
+
+ This property holds the first animation source.
+*/
+
+/*!
+ \qmlproperty BlendTreeNode BlendAnimationNode::source2
+
+ This property holds the second animation source.
+*/
+
+/*!
+ \qmlproperty real BlendAnimationNode::weight
+
+ This property holds the weight value used to blend between the two animation
+ sources.
+ The weight value determines how much of the first animation source is blended
+ with the second animation source. A weight value of \c 0.0 means the first
+ animation source is used exclusively, a weight value of \c 1.0 means the
+ second animation source is used exclusively, and a weight value of \c 0.5 means
+ both animation sources are blended equally. The default value is \c 0.5.
+*/
+
+QBlendAnimationNode::QBlendAnimationNode(QObject *parent)
+ : QBlendTreeNode(parent)
+{
+ connect(this,
+ &QBlendAnimationNode::weightChanged,
+ this,
+ &QBlendAnimationNode::handleInputFrameDataChanged);
+}
+
+QBlendTreeNode *QBlendAnimationNode::source1() const
+{
+ return m_source1;
+}
+
+void QBlendAnimationNode::setSource1(QBlendTreeNode *newSource1)
+{
+ if (m_source1 == newSource1)
+ return;
+
+ if (m_source1) {
+ disconnect(m_source1OutputConnection);
+ disconnect(m_source1DestroyedConnection);
+ }
+
+ m_source1 = newSource1;
+
+ if (m_source1) {
+ m_source1OutputConnection = connect(m_source1,
+ &QBlendTreeNode::frameDataChanged,
+ this,
+ &QBlendAnimationNode::handleInputFrameDataChanged);
+ m_source1DestroyedConnection = connect(m_source1,
+ &QObject::destroyed,
+ this,
+ [this] { setSource1(nullptr);});
+ }
+ Q_EMIT source1Changed();
+}
+
+QBlendTreeNode *QBlendAnimationNode::source2() const
+{
+ return m_source2;
+}
+
+void QBlendAnimationNode::setSource2(QBlendTreeNode *newSource2)
+{
+ if (m_source2 == newSource2)
+ return;
+
+ if (m_source2) {
+ disconnect(m_source2OutputConnection);
+ disconnect(m_source2DestroyedConnection);
+ }
+
+ m_source2 = newSource2;
+
+ if (m_source2) {
+ m_source2OutputConnection = connect(m_source2,
+ &QBlendTreeNode::frameDataChanged,
+ this,
+ &QBlendAnimationNode::handleInputFrameDataChanged);
+ m_source2DestroyedConnection = connect(m_source2,
+ &QObject::destroyed,
+ this,
+ [this] { setSource2(nullptr);});
+ }
+
+ Q_EMIT source2Changed();
+}
+
+qreal QBlendAnimationNode::weight() const
+{
+ return m_weight;
+}
+
+void QBlendAnimationNode::setWeight(qreal newWeight)
+{
+ if (qFuzzyCompare(m_weight, newWeight))
+ return;
+ m_weight = newWeight;
+ Q_EMIT weightChanged();
+}
+
+static QVariant lerp(const QVariant &first, const QVariant &second, float weight)
+{
+ // Don't bother with weights if there is no data (for now)
+ if (first.isNull())
+ return second;
+ else if (second.isNull())
+ return first;
+
+ const QMetaType type = first.metaType();
+ switch (type.id()) {
+ case QMetaType::Bool:
+ return QVariant((1.0f - weight) * first.toBool() + weight * second.toBool() >= 0.5f);
+ case QMetaType::Int:
+ return QVariant((1.0f - weight) * first.toInt() + weight * second.toInt());
+ case QMetaType::Float:
+ return QVariant((1.0f - weight) * first.toFloat() + weight * second.toFloat());
+ case QMetaType::Double:
+ return QVariant((1.0 - weight) * first.toDouble() + weight * second.toDouble());
+ case QMetaType::QVector2D: {
+ QVector2D firstVec = first.value<QVector2D>();
+ QVector2D secondVec = second.value<QVector2D>();
+ return QVariant::fromValue<QVector2D>(firstVec * (1.0f - weight) + secondVec * weight);
+ }
+ case QMetaType::QVector3D: {
+ QVector3D firstVec = first.value<QVector3D>();
+ QVector3D secondVec = second.value<QVector3D>();
+ return QVariant::fromValue<QVector3D>(firstVec * (1.0f - weight) + secondVec * weight);
+ }
+ case QMetaType::QVector4D: {
+ QVector4D firstVec = first.value<QVector4D>();
+ QVector4D secondVec = second.value<QVector4D>();
+ return QVariant::fromValue<QVector4D>(firstVec * (1.0f - weight) + secondVec * weight);
+ }
+ case QMetaType::QQuaternion: {
+ QQuaternion firstQuat = first.value<QQuaternion>();
+ QQuaternion secondQuat = second.value<QQuaternion>();
+ return QVariant::fromValue<QQuaternion>(QQuaternion::nlerp(firstQuat, secondQuat, weight));
+ }
+ case QMetaType::QColor: {
+ QColor firstColor = first.value<QColor>();
+ QColor secondColor = second.value<QColor>();
+ int r = (1.0f - weight) * firstColor.red() + weight * secondColor.red();
+ int g = (1.0f - weight) * firstColor.green() + weight * secondColor.green();
+ int b = (1.0f - weight) * firstColor.blue() + weight * secondColor.blue();
+ int a = (1.0f - weight) * firstColor.alpha() + weight * secondColor.alpha();
+ return QVariant::fromValue<QColor>(QColor(r, g, b, a));
+ }
+ case QMetaType::QRect: {
+ QRect firstRect = first.value<QRect>();
+ QRect secondRect = second.value<QRect>();
+ int x = (1.0f - weight) * firstRect.x() + weight * secondRect.x();
+ int y = (1.0f - weight) * firstRect.y() + weight * secondRect.y();
+ int width = (1.0f - weight) * firstRect.width() + weight * secondRect.width();
+ int height = (1.0f - weight) * firstRect.height() + weight * secondRect.height();
+ return QVariant::fromValue<QRect>(QRect(x, y, width, height));
+ }
+ case QMetaType::QRectF: {
+ QRectF firstRectF = first.value<QRectF>();
+ QRectF secondRectF = second.value<QRectF>();
+ qreal x = (1.0 - weight) * firstRectF.x() + weight * secondRectF.x();
+ qreal y = (1.0 - weight) * firstRectF.y() + weight * secondRectF.y();
+ qreal width = (1.0 - weight) * firstRectF.width() + weight * secondRectF.width();
+ qreal height = (1.0 - weight) * firstRectF.height() + weight * secondRectF.height();
+ return QVariant::fromValue<QRectF>(QRectF(x, y, width, height));
+ }
+ default:
+ // Unsupported type, return an invalid QVariant
+ return QVariant();
+ }
+
+}
+
+void QBlendAnimationNode::handleInputFrameDataChanged()
+{
+ const QHash<QQmlProperty, QVariant> &frameData1 = m_source1 ? m_source1->frameData() : QHash<QQmlProperty, QVariant>();
+ const QHash<QQmlProperty, QVariant> &frameData2 = m_source2 ? m_source2->frameData() : QHash<QQmlProperty, QVariant>();
+
+ // Do the LERP blending here
+ if (m_weight <= 0.0) {
+ // all source1
+ m_frameData = frameData1;
+ } else if (m_weight >= 1.0) {
+ // all source2
+ m_frameData = frameData2;
+ } else {
+ // is a mix
+ QHash<QQmlProperty, QPair<QVariant, QVariant>> allData;
+ const auto &keys1 = frameData1.keys();
+ for (const auto &property : keys1)
+ allData.insert(property, QPair<QVariant, QVariant>(frameData1[property], QVariant()));
+ const auto &keys2 = frameData2.keys();
+ for (const auto &property : keys2) {
+ // first check if property is already in all data, and if so modify the pair to include this value
+ if (allData.contains(property)) {
+ allData[property].second = frameData2[property];
+ } else {
+ allData.insert(property, QPair<QVariant, QVariant>(QVariant(), frameData2[property]));
+ }
+ }
+
+ QHash<QQmlProperty, QVariant> newFrameData;
+
+ const auto &keys = allData.keys();
+ for (const auto &property : keys) {
+ const auto &dataPair = allData[property];
+ newFrameData.insert(property, lerp(dataPair.first, dataPair.second, m_weight));
+ }
+ m_frameData = newFrameData;
+ }
+
+ Q_EMIT frameDataChanged();
+}
+
+QT_END_NAMESPACE
diff --git a/src/timeline/blendtrees/qblendanimationnode_p.h b/src/timeline/blendtrees/qblendanimationnode_p.h
new file mode 100644
index 0000000..ad5814d
--- /dev/null
+++ b/src/timeline/blendtrees/qblendanimationnode_p.h
@@ -0,0 +1,65 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QBLENDANIMATIONNODE_P_H
+#define QBLENDANIMATIONNODE_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 <QObject>
+#include <QtQml>
+#include <QtQuick/private/qquickanimation_p.h>
+#include <QtQuickTimelineBlendTrees/private/qblendtreenode_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QBlendAnimationNode : public QBlendTreeNode
+{
+ Q_OBJECT
+ Q_PROPERTY(QBlendTreeNode *source1 READ source1 WRITE setSource1 NOTIFY source1Changed FINAL)
+ Q_PROPERTY(QBlendTreeNode *source2 READ source2 WRITE setSource2 NOTIFY source2Changed FINAL)
+ Q_PROPERTY(qreal weight READ weight WRITE setWeight NOTIFY weightChanged FINAL)
+ QML_NAMED_ELEMENT(BlendAnimationNode)
+public:
+ explicit QBlendAnimationNode(QObject *parent = nullptr);
+
+ QBlendTreeNode *source1() const;
+ void setSource1(QBlendTreeNode *newSource1);
+
+ QBlendTreeNode *source2() const;
+ void setSource2(QBlendTreeNode *newSource2);
+
+ qreal weight() const;
+ void setWeight(qreal newWeight);
+
+private Q_SLOTS:
+ void handleInputFrameDataChanged();
+
+Q_SIGNALS:
+ void source1Changed();
+ void source2Changed();
+ void weightChanged();
+
+private:
+ QBlendTreeNode *m_source1 = nullptr;
+ QBlendTreeNode *m_source2 = nullptr;
+ qreal m_weight = 0.5;
+
+ QMetaObject::Connection m_source1OutputConnection;
+ QMetaObject::Connection m_source2OutputConnection;
+ QMetaObject::Connection m_source1DestroyedConnection;
+ QMetaObject::Connection m_source2DestroyedConnection;
+};
+
+QT_END_NAMESPACE
+
+#endif // QBLENDANIMATIONNODE_P_H
diff --git a/src/timeline/blendtrees/qblendtreenode.cpp b/src/timeline/blendtrees/qblendtreenode.cpp
new file mode 100644
index 0000000..1ff7e2c
--- /dev/null
+++ b/src/timeline/blendtrees/qblendtreenode.cpp
@@ -0,0 +1,73 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "qblendtreenode_p.h"
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype BlendTreeNode
+ \inherits QObject
+ \instantiates QBlendTreeNode
+ \inqmlmodule QtQuick.Timeline.BlendTrees
+ \ingroup qtqmltypes
+
+ \brief Base class for all blend tree nodes.
+
+ BlendTreeNode is the base class for all blend tree nodes. It is not
+ intended to be used directly, but rather to be subclassed to create
+ custom blend tree nodes.
+*/
+
+/*!
+ \qmlproperty bool BlendTreeNode::outputEnabled
+
+ This property determines whether the blend tree node should commit
+ the property changes. The default value is /c false.
+
+ Any node can be an output node which commits the property changes,
+ but it should usually be the last node in the blend tree. If multiple
+ nodes are outputting data and there is a conflict, then the last
+ property change will be used. So ideally if there are multiple output
+ nodes in a tree, the targets and properties effected should be disjoint.
+*/
+
+QBlendTreeNode::QBlendTreeNode(QObject *parent)
+ : QObject{parent}
+{
+ connect(this, &QBlendTreeNode::frameDataChanged, this, &QBlendTreeNode::handleFrameDataChanged);
+ connect(this, &QBlendTreeNode::outputEnabledChanged, this, &QBlendTreeNode::handleFrameDataChanged);
+}
+
+const QHash<QQmlProperty, QVariant> &QBlendTreeNode::frameData()
+{
+ return m_frameData;
+}
+
+bool QBlendTreeNode::outputEnabled() const
+{
+ return m_outputEnabled;
+}
+
+void QBlendTreeNode::setOutputEnabled(bool isOutputEnabled)
+{
+ if (m_outputEnabled == isOutputEnabled)
+ return;
+ m_outputEnabled = isOutputEnabled;
+ Q_EMIT outputEnabledChanged();
+}
+
+void QBlendTreeNode::handleFrameDataChanged()
+{
+ // If we are not outputting data, then there is nothing to do
+ if (!m_outputEnabled)
+ return;
+
+ // Commit the property
+ for (auto it = m_frameData.cbegin(), end = m_frameData.cend(); it != end; ++it) {
+ const auto &property = it.key();
+ property.write(it.value());
+ }
+}
+
+QT_END_NAMESPACE
diff --git a/src/timeline/blendtrees/qblendtreenode_p.h b/src/timeline/blendtrees/qblendtreenode_p.h
new file mode 100644
index 0000000..ce45e9d
--- /dev/null
+++ b/src/timeline/blendtrees/qblendtreenode_p.h
@@ -0,0 +1,53 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QBLENDTREENODE_P_H
+#define QBLENDTREENODE_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 <QObject>
+#include <QtQml>
+
+QT_BEGIN_NAMESPACE
+
+class QBlendTreeNode : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(bool outputEnabled READ outputEnabled WRITE setOutputEnabled NOTIFY outputEnabledChanged FINAL)
+ QML_NAMED_ELEMENT(BlendTreeNode)
+ QML_UNCREATABLE("Interface Class")
+public:
+ explicit QBlendTreeNode(QObject *parent = nullptr);
+
+ const QHash<QQmlProperty, QVariant> &frameData();
+
+ bool outputEnabled() const;
+ void setOutputEnabled(bool isOutputEnabled);
+
+Q_SIGNALS:
+ void frameDataChanged();
+ void outputEnabledChanged();
+
+protected:
+ QHash<QQmlProperty, QVariant> m_frameData;
+
+private Q_SLOTS:
+ void handleFrameDataChanged();
+
+private:
+ bool m_outputEnabled = false;
+};
+
+QT_END_NAMESPACE
+
+#endif // QBLENDTREENODE_P_H
diff --git a/src/timeline/blendtrees/qtimelineanimationnode.cpp b/src/timeline/blendtrees/qtimelineanimationnode.cpp
new file mode 100644
index 0000000..4f1166d
--- /dev/null
+++ b/src/timeline/blendtrees/qtimelineanimationnode.cpp
@@ -0,0 +1,149 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "qtimelineanimationnode_p.h"
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype TimelineAnimationNode
+ \inherits QBlendTreeNode
+ \instantiates QTimelineAnimationNode
+ \inqmlmodule QtQuick.Timeline.BlendTrees
+ \ingroup qtqmltypes
+
+ \brief A blend tree source node that plays a timeline animation.
+
+ TimelineAnimationNode is a blend tree source node that plays a timeline
+ animation and outputs the animation's frame data. This node wraps a
+ TimelineAnimation and its associated Timeline and provides a way to
+ intercept the animation's frame data and output it to the blend tree.
+*/
+
+/*!
+ \qmlproperty TimelineAnimation TimelineAnimationNode::animation
+
+ This property holds the timeline animation to play.
+*/
+
+/*!
+ \qmlproperty Timeline TimelineAnimationNode::timeline
+
+ This property holds the timeline that the animation is played on.
+*/
+
+/*!
+ \qmlproperty real TimelineAnimationNode::currentFrame
+
+ This property holds the current frame of the animation.
+*/
+
+QTimelineAnimationNode::QTimelineAnimationNode(QObject *parent)
+ : QBlendTreeNode(parent)
+{
+
+}
+
+QQuickTimelineAnimation *QTimelineAnimationNode::animation() const
+{
+ return m_animation;
+}
+
+void QTimelineAnimationNode::setAnimation(QQuickTimelineAnimation *newAnimation)
+{
+ if (m_animation == newAnimation)
+ return;
+
+ if (m_animation)
+ disconnect(m_animationDestroyedConnection);
+
+ m_animation = newAnimation;
+
+ if (m_animation)
+ m_animationDestroyedConnection = connect(m_animation,
+ &QObject::destroyed,
+ this,
+ [this] {setAnimation(nullptr);});
+
+ updateAnimationTarget();
+ updateFrameData();
+ Q_EMIT animationChanged();
+}
+
+QQuickTimeline *QTimelineAnimationNode::timeline() const
+{
+ return m_timeline;
+}
+
+void QTimelineAnimationNode::setTimeline(QQuickTimeline *newTimeline)
+{
+ if (m_timeline == newTimeline)
+ return;
+
+ if (m_timeline)
+ disconnect(m_timelineDestroyedConnection);
+
+ m_timeline = newTimeline;
+
+ if (m_timeline)
+ m_timelineDestroyedConnection = connect(m_timeline,
+ &QObject::destroyed,
+ this,
+ [this] {setTimeline(nullptr);});
+
+ updateFrameData();
+ Q_EMIT timelineChanged();
+}
+
+qreal QTimelineAnimationNode::currentFrame() const
+{
+ return m_currentFrame;
+}
+
+void QTimelineAnimationNode::setCurrentFrame(qreal newCurrentFrame)
+{
+ if (qFuzzyCompare(m_currentFrame, newCurrentFrame))
+ return;
+ m_currentFrame = newCurrentFrame;
+ updateFrameData();
+ Q_EMIT currentFrameChanged();
+}
+
+static QHash<QQmlProperty, QVariant> getFrameData(QQuickTimeline *timeline, qreal frame)
+{
+ QHash<QQmlProperty, QVariant> frameData;
+ if (timeline) {
+ QQmlListReference keyframeGroups(timeline, "keyframeGroups");
+ if (keyframeGroups.isValid() && keyframeGroups.isReadable()) {
+ for (int i = 0; i < keyframeGroups.count(); ++i) {
+ QQuickKeyframeGroup *keyframeGroup = qobject_cast<QQuickKeyframeGroup *>(keyframeGroups.at(i));
+ if (keyframeGroup && keyframeGroup->target()) {
+ QQmlProperty qmlProperty(keyframeGroup->target(), keyframeGroup->property());
+ QVariant value = keyframeGroup->evaluate(frame);
+ frameData.insert(qmlProperty, value);
+ }
+ }
+ }
+ }
+
+ return frameData;
+}
+
+void QTimelineAnimationNode::updateFrameData()
+{
+ if (!m_animation || !m_timeline)
+ return;
+
+ m_frameData = getFrameData(m_timeline, m_currentFrame);
+ Q_EMIT frameDataChanged();
+}
+
+void QTimelineAnimationNode::updateAnimationTarget()
+{
+ if (!m_animation)
+ return;
+ // Property should already be set to "currentFrame"
+ m_animation->setTargetObject(this);
+}
+
+QT_END_NAMESPACE
diff --git a/src/timeline/blendtrees/qtimelineanimationnode_p.h b/src/timeline/blendtrees/qtimelineanimationnode_p.h
new file mode 100644
index 0000000..32a6860
--- /dev/null
+++ b/src/timeline/blendtrees/qtimelineanimationnode_p.h
@@ -0,0 +1,63 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QTIMELINEANIMATIONNODE_P_H
+#define QTIMELINEANIMATIONNODE_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 <QObject>
+#include <QtQml>
+#include <QtQuickTimeline/private/qquicktimeline_p.h>
+#include <QtQuickTimeline/private/qquicktimelineanimation_p.h>
+#include <QtQuickTimelineBlendTrees/private/qblendtreenode_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QTimelineAnimationNode : public QBlendTreeNode
+{
+ Q_OBJECT
+ Q_PROPERTY(QQuickTimelineAnimation *animation READ animation WRITE setAnimation NOTIFY animationChanged FINAL)
+ Q_PROPERTY(QQuickTimeline *timeline READ timeline WRITE setTimeline NOTIFY timelineChanged FINAL)
+ Q_PROPERTY(qreal currentFrame READ currentFrame WRITE setCurrentFrame NOTIFY currentFrameChanged FINAL)
+ QML_NAMED_ELEMENT(TimelineAnimationNode)
+public:
+ explicit QTimelineAnimationNode(QObject *parent = nullptr);
+
+ QQuickTimelineAnimation *animation() const;
+ void setAnimation(QQuickTimelineAnimation *newAnimation);
+
+ QQuickTimeline *timeline() const;
+ void setTimeline(QQuickTimeline *newTimeline);
+
+ qreal currentFrame() const;
+ void setCurrentFrame(qreal newCurrentFrame);
+
+Q_SIGNALS:
+ void animationChanged();
+ void timelineChanged();
+ void currentFrameChanged();
+
+private:
+ void updateFrameData();
+ void updateAnimationTarget();
+ QQuickTimelineAnimation *m_animation = nullptr;
+ QQuickTimeline *m_timeline = nullptr;
+ qreal m_currentFrame = -1.0;
+
+ QMetaObject::Connection m_animationDestroyedConnection;
+ QMetaObject::Connection m_timelineDestroyedConnection;
+};
+
+QT_END_NAMESPACE
+
+#endif // QTIMELINEANIMATIONNODE_P_H
diff --git a/src/timeline/blendtrees/qtquicktimelineblendtreesglobal.h b/src/timeline/blendtrees/qtquicktimelineblendtreesglobal.h
new file mode 100644
index 0000000..8e11fd6
--- /dev/null
+++ b/src/timeline/blendtrees/qtquicktimelineblendtreesglobal.h
@@ -0,0 +1,10 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QTQUICKTIMELINEBLENDTREESGLOBAL_H
+#define QTQUICKTIMELINEBLENDTREESGLOBAL_H
+
+#include <QtCore/qglobal.h>
+#include <QtQuickTimelineBlendTrees/qtquicktimelineblendtreesexports.h>
+
+#endif // QTQUICKTIMELINEBLENDTREESGLOBAL_H
diff --git a/src/timeline/blendtrees/qtquicktimelineblendtreesglobal_p.h b/src/timeline/blendtrees/qtquicktimelineblendtreesglobal_p.h
new file mode 100644
index 0000000..70530d7
--- /dev/null
+++ b/src/timeline/blendtrees/qtquicktimelineblendtreesglobal_p.h
@@ -0,0 +1,21 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QTQUICKTIMELINEBLENDTREESGLOBAL_P_H
+#define QTQUICKTIMELINEBLENDTREESGLOBAL_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 "qtquicktimelineblendtreesglobal.h"
+#include <QtQuickTimelineBlendTrees/private/qtquicktimelineblendtreesexports_p.h>
+
+#endif // QTQUICKTIMELINEBLENDTREESGLOBAL_P_H
diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt
index 1acd924..0a6ebc1 100644
--- a/tests/auto/CMakeLists.txt
+++ b/tests/auto/CMakeLists.txt
@@ -4,3 +4,4 @@
# Generated from auto.pro.
add_subdirectory(qtquicktimeline)
+add_subdirectory(qtquicktimeline_blendtrees)
diff --git a/tests/auto/qtquicktimeline_blendtrees/CMakeLists.txt b/tests/auto/qtquicktimeline_blendtrees/CMakeLists.txt
new file mode 100644
index 0000000..bad2339
--- /dev/null
+++ b/tests/auto/qtquicktimeline_blendtrees/CMakeLists.txt
@@ -0,0 +1,26 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+# Collect test data
+file(GLOB_RECURSE test_data
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data/*
+)
+
+qt_internal_add_test(tst_blendtrees
+ SOURCES
+ tst_blendtrees.cpp
+ DEFINES
+ SRCDIR="${CMAKE_CURRENT_SOURCE_DIR}/"
+ LIBRARIES
+ Qt::Gui
+ Qt::Qml
+ Qt::QmlPrivate
+ Qt::Quick
+ Qt::QuickPrivate
+ TESTDATA ${test_data}
+)
+
+if(QT_BUILD_STANDALONE_TESTS)
+ qt_import_qml_plugins(tst_blendtrees)
+endif()
diff --git a/tests/auto/qtquicktimeline_blendtrees/data/BlendTreeTest.qml b/tests/auto/qtquicktimeline_blendtrees/data/BlendTreeTest.qml
new file mode 100644
index 0000000..d531dc9
--- /dev/null
+++ b/tests/auto/qtquicktimeline_blendtrees/data/BlendTreeTest.qml
@@ -0,0 +1,175 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Timeline
+import QtQuick.Timeline.BlendTrees
+
+Item {
+ Item {
+ width: 480
+ height: 480
+
+ TimelineAnimation {
+ objectName: "animation1"
+ id: animation1
+ duration: 20000
+ loops: -1
+ from: 0
+ to: 100
+ }
+ TimelineAnimation {
+ objectName: "animation2"
+ id: animation2
+ duration: 20000
+ loops: -1
+ from: 100
+ to: 200
+ }
+
+ Timeline {
+ id: timeline
+ objectName: "timeline"
+
+ enabled: true
+
+ KeyframeGroup {
+ objectName: "group01"
+ target: rectangle
+ property: "x"
+
+ Keyframe {
+ frame: 0
+ value: 0
+ }
+
+ Keyframe {
+ objectName: "keyframe"
+ frame: 50
+ value: 100
+ }
+
+ Keyframe {
+ frame: 100
+ value: 200
+ }
+
+ Keyframe {
+ frame: 150
+ value: 100
+ }
+
+ Keyframe {
+ frame: 200
+ value: 0
+ }
+ }
+
+ KeyframeGroup {
+ target: rectangle
+ property: "y"
+
+ Keyframe {
+ frame: 0
+ value: 0
+ }
+
+ Keyframe {
+ frame: 50
+ value: 100
+ }
+
+ Keyframe {
+ objectName: "easingBounce"
+ frame: 100
+ value: 200
+ easing.type: Easing.InBounce
+ }
+
+ Keyframe {
+ frame: 150
+ value: 300
+ }
+
+ Keyframe {
+ frame: 200
+ value: 400
+ }
+ }
+
+ KeyframeGroup {
+ target: rectangle
+ property: "color"
+
+ Keyframe {
+ frame: 0
+ value: "red"
+ }
+
+ Keyframe {
+ frame: 50
+ value: "blue"
+ }
+
+ Keyframe {
+ frame: 100
+ value: "yellow"
+ }
+
+ Keyframe {
+ frame: 150
+ value: "cyan"
+ }
+
+ Keyframe {
+ frame: 200
+ value: "magenta"
+ }
+ }
+ }
+
+ TimelineAnimationNode {
+ id: animation1Node
+ objectName: "animation1Node"
+ timeline: timeline
+ animation: animation1
+ }
+
+ TimelineAnimationNode {
+ id: animation2Node
+ objectName: "animation2Node"
+ timeline: timeline
+ animation: animation2
+ }
+
+ BlendAnimationNode {
+ id: animationBlendNode
+ objectName: "blendAnimation"
+ source1: animation1Node
+ source2: animation2Node
+ weight: 0.5
+ outputEnabled: true
+ }
+
+ Rectangle {
+ id: rectangle
+
+ objectName: "rectangle"
+
+ width: 20
+ height: 20
+ color: "red"
+ }
+
+ AnimationController {
+ id: animation1Controller
+ objectName: "animation1Controller"
+ animation: animation1
+ }
+ AnimationController {
+ id: animation2Controller
+ objectName: "animation2Controller"
+ animation: animation2
+ }
+ }
+}
diff --git a/tests/auto/qtquicktimeline_blendtrees/tst_blendtrees.cpp b/tests/auto/qtquicktimeline_blendtrees/tst_blendtrees.cpp
new file mode 100644
index 0000000..19c5939
--- /dev/null
+++ b/tests/auto/qtquicktimeline_blendtrees/tst_blendtrees.cpp
@@ -0,0 +1,284 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtTest/QtTest>
+#include <QtQml/qqmlengine.h>
+#include <QtQml/qqmlcomponent.h>
+
+#include <QEasingCurve>
+#include <QVector3D>
+
+inline QUrl testFileUrl(const QString &fileName)
+{
+ static const QString dir = QTest::qFindTestData("data");
+
+ QString result = dir;
+ result += QLatin1Char('/');
+ result += fileName;
+
+ return QUrl::fromLocalFile(result);
+}
+
+class Tst_BlendTrees : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void checkImport();
+ void testBlendAnimationNode();
+
+};
+
+
+
+void Tst_BlendTrees::checkImport()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine);
+ component.setData("import QtQuick; import QtQuick.Timeline; import QtQuick.Timeline.BlendTrees; Item { }", QUrl());
+
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY2(!object.isNull(), qPrintable(component.errorString()));
+}
+
+void Tst_BlendTrees::testBlendAnimationNode()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine);
+ component.loadUrl(testFileUrl("BlendTreeTest.qml"));
+
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY2(!object.isNull(), qPrintable(component.errorString()));
+
+ // Get all of the necessary objects
+ auto *timeline = object->findChild<QObject * >("timeline");
+ auto *timelineAnimation1 = object->findChild<QObject *>("animation1");
+ QVERIFY2(timelineAnimation1, "Could not find animation1");
+ auto *timelineAnimation2 = object->findChild<QObject *>("animation2");
+ QVERIFY2(timelineAnimation2, "Could not find animation2");
+ auto *animation1Node = object->findChild<QObject *>("animation1Node");
+ QVERIFY2(animation1Node, "Could not find animation1Node");
+ auto *animation2Node = object->findChild<QObject *>("animation2Node");
+ QVERIFY2(animation2Node, "Could not find animation2Node");
+ auto *blendAnimation = object->findChild<QObject *>("blendAnimation");
+ QVERIFY2(blendAnimation, "Could not find blendAnimation");
+ auto *rectangle = object->findChild<QObject *>("rectangle");
+ QVERIFY2(rectangle, "Could not find rectangle");
+ auto *animation1Controller = object->findChild<QObject *>("animation1Controller");
+ QVERIFY2(animation1Controller, "Could not find animation1Controller");
+ auto *animation2Controller = object->findChild<QObject *>("animation2Controller");
+ QVERIFY2(animation2Controller, "Could not find animation2Controller");
+
+
+ // At this point nothing should be happening because the animations have controllers
+ // attached to this which forces them to always be paused. Starting states is:
+ // animation1Node.currentFrame == 0
+ // animation2Node.currentFrame == 100
+ // rectangle.x = 100
+ // rectangle.y = 100
+ // rectangle.color = #ff7f00
+
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 0);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 100);
+ QCOMPARE(rectangle->property("y").toInt(), 100);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff7f00"));
+
+ // Push animation1 to end
+ // Should be a blend of 50% blend of animation1 and and 50% blend of animation2
+ // animation 1 should be at frame 100 (100%)
+ // animation 2 should be at frame 100 (0%)
+ animation1Controller->setProperty("progress", 1.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 200);
+ QCOMPARE(rectangle->property("y").toInt(), 200);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ffff00"));
+
+ // Push animation2 to end
+ // Should be a blend of 50% blend of animation1 and and 50% blend of animation2
+ // animation 1 should be at frame 100 (100%)
+ // animation 2 should be at frame 200 (100%)
+ animation2Controller->setProperty("progress", 1.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 100);
+ QCOMPARE(rectangle->property("y").toInt(), 300);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff7f7f"));
+
+ // Change weight to 0.0
+ // Should be a blend of 100% blend of animation1 and and 0% blend of animation2
+ // animation 1 should be at frame 100 (100%)
+ // animation 2 should be at frame 200 (100%)
+ blendAnimation->setProperty("weight", 0.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 200);
+ QCOMPARE(rectangle->property("y").toInt(), 200);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ffff00"));
+
+ // Change weight to 1.0
+ // Should be a blend of 0% blend of animation1 and and 100% blend of animation2
+ // animation 1 should be at frame 100 (100%)
+ // animation 2 should be at frame 200 (100%)
+ blendAnimation->setProperty("weight", 1.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 400);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff00ff"));
+
+ // Change animation1 to start (should be the same as previous)
+ // Should be a blend of 0% blend of animation1 and and 100% blend of animation2
+ // animation 1 should be at frame 0 (0%)
+ // animation 2 should be at frame 200 (100%)
+ animation1Controller->setProperty("progress", 0.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 0);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 400);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff00ff"));
+
+ // Change weight to 0.0
+ // Should be a blend of 100% blend of animation1 and and 0% blend of animation2
+ // animation 1 should be at frame 0 (0%)
+ // animation 2 should be at frame 200 (100%)
+ blendAnimation->setProperty("weight", 0.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 0);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 0);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff0000"));
+
+ // Change animation2 to start (should be the same as previous)
+ // Should be a blend of 100% blend of animation1 and and 0% blend of animation2
+ // animation 1 should be at frame 0 (0%)
+ // animation 2 should be at frame 100 (0%)
+ animation2Controller->setProperty("progress", 0.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 0);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 0);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff0000"));
+
+ // Disable committing of changes by animationBlendNode
+ blendAnimation->setProperty("outputEnabled", false);
+ // Now changing either animation progress or weight should have no effect on the scene
+ animation1Controller->setProperty("progress", 1.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 0);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff0000"));
+
+ animation2Controller->setProperty("progress", 1.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 0);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff0000"));
+
+ blendAnimation->setProperty("weight", 0.5f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 0);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff0000"));
+
+ // re-enable committing of changes by animationBlendNode
+ // This should cause the animation to blend to the new state
+ blendAnimation->setProperty("outputEnabled", true);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 100);
+ QCOMPARE(rectangle->property("y").toInt(), 300);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff7f7f"));
+
+ // Test disconnecting animation1
+ blendAnimation->setProperty("source1", QVariant());
+
+ animation1Controller->setProperty("progress", 0.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 0);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 100);
+ QCOMPARE(rectangle->property("y").toInt(), 300);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff7f7f"));
+
+ // Test disconnecting animation2
+ blendAnimation->setProperty("source2", QVariant());
+ animation2Controller->setProperty("progress", 0.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 0);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 100);
+ QCOMPARE(rectangle->property("y").toInt(), 300);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff7f7f"));
+
+ // Disable committing of changes by animationBlendNode
+ blendAnimation->setProperty("outputEnabled", false);
+
+ // Try outputting dirrectly from animation1
+ animation1Node->setProperty("outputEnabled", true);
+ // Should have changed to be the value of animation1 at frame 0
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 0);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff0000"));
+
+ animation1Controller->setProperty("progress", 1.0f);
+ // Should have changed to be the value of animation1 at frame 100
+ QCOMPARE(rectangle->property("x").toInt(), 200);
+ QCOMPARE(rectangle->property("y").toInt(), 200);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ffff00"));
+
+ // Disable outputting dirrectly from animation1
+ animation1Node->setProperty("outputEnabled", false);
+ // Try outputting dirrectly from animation2
+ animation2Node->setProperty("outputEnabled", true);
+ // Nothing should have changed since both would be at frame 100 anyway
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 200);
+ QCOMPARE(rectangle->property("y").toInt(), 200);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ffff00"));
+
+ animation2Controller->setProperty("progress", 1.0f);
+ QCOMPARE(animation1Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 200);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 400);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff00ff"));
+
+ // Try breaking animation2Node's connection to timeline (the source of frame data)
+ // currentFrame should change but the output should not
+ animation2Node->setProperty("timeline", QVariant());
+ animation2Controller->setProperty("progress", 0.0f);
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 400);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff00ff"));
+
+ // reattach the timeline
+ animation2Node->setProperty("timeline", QVariant::fromValue(timeline));
+ // Should update the output now that we can fetch frameData for frame 100
+ QCOMPARE(animation2Node->property("currentFrame").toInt(), 100);
+ QCOMPARE(rectangle->property("x").toInt(), 200);
+ QCOMPARE(rectangle->property("y").toInt(), 200);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ffff00"));
+
+ // Try breaking animation2Node's connection to animation
+ animation2Node->setProperty("animation", QVariant());
+ animation2Controller->setProperty("progress", 1.0f);
+ QCOMPARE(rectangle->property("x").toInt(), 200);
+ QCOMPARE(rectangle->property("y").toInt(), 200);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ffff00"));
+
+ // reattach the animation
+ animation2Node->setProperty("animation", QVariant::fromValue(timelineAnimation2));
+ // Should update the output now that we can fetch frameData for frame 200
+ QCOMPARE(rectangle->property("x").toInt(), 0);
+ QCOMPARE(rectangle->property("y").toInt(), 400);
+ QCOMPARE(rectangle->property("color").value<QColor>(), QColor("#ff00ff"));
+}
+
+QTEST_MAIN(Tst_BlendTrees)
+
+#include "tst_blendtrees.moc"