diff options
author | Andy Nichols <andy.nichols@qt.io> | 2023-06-28 17:32:30 +0200 |
---|---|---|
committer | Andy Nichols <nezticle@gmail.com> | 2023-12-06 12:37:10 +0100 |
commit | 0272b5215ef3bfdc591ee444c9d2614b58f14bfd (patch) | |
tree | 5250b17c670fb3bdc1fd89492f5d0731aee18d37 /tests | |
parent | 320965fe0cd07e0d06116545dfce41fd198328b1 (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.txt | 1 | ||||
-rw-r--r-- | tests/auto/qtquicktimeline_blendtrees/CMakeLists.txt | 26 | ||||
-rw-r--r-- | tests/auto/qtquicktimeline_blendtrees/data/BlendTreeTest.qml | 175 | ||||
-rw-r--r-- | tests/auto/qtquicktimeline_blendtrees/tst_blendtrees.cpp | 284 |
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" |