aboutsummaryrefslogtreecommitdiffstats
path: root/tests
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 /tests
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>
Diffstat (limited to 'tests')
-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
4 files changed, 486 insertions, 0 deletions
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"