New in Qt 6.4: FrameAnimation

In this blog post we try to solve the classical "Mouse chasing Mouse" -problem. Don't know it? No problem, nobody does. But if you are interested in Qt Quick, smooth animations and what's new in Qt 6.4 (Beta3 was just released!), please continue reading and you'll find out!

When animating a Qt Quick property from A to B with speed X, you usually use some Animation element like PropertyAnimation, NumberAnimation or ColorAnimation. And for combining multiple animations, you can use ParallelAnimation or SequentialAnimation. These are declarative and work great in most cases. Sometimes in the middle of an animation the target changes to C instead (or back to A) and standard animations can handle also this case smoothly. But if C is a moving target, changing frequently or speed X should be adjustable during the animation (and not just some pre-defined easing curve), then these standard Qt Quick animations are not ideal anymore.

Now let's demonstrate this issue with a mouse chasing mouse tester application and present different (more or less optional) ways to solve it.

Solution 1: Behavior

When I have property changes and need to animate them, first option that comes to my mind usually is adding a Behavior for those properties. So let's try that first. Attach the mouse position to mouseX and mouseY properties and whenever they change, instead of changing directly to the new values, use Behavior to animate the changes like this:

property real mouseX: 0
property real mouseY: 0

Behavior on mouseX {
NumberAnimation {
duration: 1000
}
}
Behavior on mouseY {
NumberAnimation {
duration: 1000
}
}

Here is what this solution looks like:

 

There are a few issues with this approach: As the animations are duration-based, mouse moves faster when the distance is longer and slower when it is shorter. Another issue is that animations are recalculated & restarted whenever the mouse pointer moves, which causes extra CPU usage. This is especially notable when we artificially generate high CPU load → the animations become jumpy. Instead of NumberAnimation it would be possible to use SmoothedAnimation or SpringAnimation but the easings of those animation types don't really suit for this use case and they don't really fix the issues.

Solution 2: Timer

As the property animations seem a bit too restrictive for this use case, someone (not me) could consider using Timer with 16ms interval for 60fps animation action. Code for that would look something like this:

Timer {
running: true
repeat: true
interval: 16
onTriggered: {
var xDelta = mouseArea.mouseX - mouseX;
var yDelta = mouseArea.mouseY - mouseY;
var length = Math.sqrt(xDelta * xDelta + yDelta * yDelta);
var speed = 3.0;
if (length > speed) {
var xNormalized = xDelta / length;
var yNormalized = yDelta / length;
mouseX += xNormalized * speed;
mouseY += yNormalized * speed;
}
}
}

And the outcome:

 

Initially this Timer approach seems to work pretty well. But it also comes with its own flaws: As we set the interval to 16ms, the timer doesn't match well to non-60Hz animation refresh rates. Also, QML Timers are not really meant for animations and internally they have an extra Qt event loop roundtrip, meaning that they are not as tightly integrated with the animation loop. This is clearly visible when loading the event system e.g. by moving the window → the animation becomes jumpy. Lastly, if the target doesn't reach the intended fps for any reason, the mouse starts to move slower as the speed doesn't have any multiplier taking the animation frame rate into account. We could manually calculate some multiplier e.g. with JavaScript Date & getMilliseconds() but that is an extra work we rather avoid if possible.

Solution 3: FrameAnimation

Next we will switch to the main topic of this blog post, the new FrameAnimation element. FrameAnimation can be considered as a "custom animation" where you control what happens each time it is triggered. Compared to Timer, FrameAnimation doesn't have repeat or interval properties, since the intervals are always synchronized with the animations and triggered once per Qt Quick animation frame. Source code of the FrameAnimation version is very similar to previous Timer code, with an addition to also rotate the mouse image:

FrameAnimation {
running: true
onTriggered: {
var xDelta = mouseArea.mouseX - mouseX;
var yDelta = mouseArea.mouseY - mouseY;
var length = Math.sqrt(xDelta * xDelta + yDelta * yDelta);
var speed = 3.0 * 60 * frameTime;
if (length > speed) {
var xNormalized = xDelta / length;
var yNormalized = yDelta / length;
mouseX += xNormalized * speed;
mouseY += yNormalized * speed;
var rot = Math.atan2(yDelta, xDelta) - (Math.PI / 2);
mouseImage.rotation = rot * (180 / Math.PI);
}
}
}

And the outcome:


This version is the one to use because FrameAnimation is better synchronized with the animation loop, getting triggered for every animation frame. This way it will adjust to different target screen refresh rates when using the threaded render loop with vsync-based throttling. You can also use frameTime (or smoothFrameTime) property as a multiplier and your animation speed will then adjust to different target fps, taking into account possible missed frames. Actually, FrameAnimation with smoothFrameTime property is used in this example also for the simple frame time & rate information, which is good enough for our needs to show when the animation speed drops:

Text {
text: fpsHelper.ft + " ms, " + fpsHelper.fps + " fps"
FrameAnimation {
id: fpsHelper
readonly property int fps: smoothFrameTime > 0 ? Math.round(1.0 / smoothFrameTime) : 0
readonly property string ft: (1000 * smoothFrameTime).toFixed(1)
running: true
}
}

Performance considerations

Now you might think "OK, this FrameAnimation can do stuff but shouldn't we avoid QML JavaScript code for optimal performance?". This was true in the past but not as much anymore. With Qt 6, a lot of work has been done to optimize the QML engine and the new Qt Quick Compiler even compiles QML JavaScript to native C++ code. For more details, see the Qt Quick Compiler performance blog post and the related optimization series posts. So while you should still keep the frontend UI layer (QML) and the backend logic (C++) separate and have most of the imperative code in C++ side, don't be afraid of using QML scripts a bit more for UI related things.

The example used in this blog post was just a quick tester app, but FrameAnimation can of course also be used to do much nicer things (which I will blog about soonish!). But in conclusion for this post: If you are using Qt 6.4 (or newer) and have a need for fully custom Qt Quick animations, consider using the new FrameAnimation element.


Blog Topics:

Comments