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

Marcin J.
1 point
32 months ago

That looks great, and seems very useful for games that could very well be implemented in Qt (Quick).

Niels Mayer
1 point
32 months ago

Do you have plans to apply FrameAnimation to #QNanoPainter? Or examples showing integration with FrameAnimation?

Is it as simple as replacing the timer-based frame updates with FrameAnimation updates?

Can it be used to improve existing FPS-counters vs https://code.qt.io/cgit/qt/qtmultimedia.git/tree/examples/multimedia/video/qmlvideo/frequencymonitor.cpp?h=6.4 or https://github.com/QUItCoding/qnanopainter/blob/master/examples/qnanopainter_vs_qpainter_demo/qml/FpsItem.qml ?

Kaj Grönholm
1 point
32 months ago

@Niels Ah, replied already in a tweet with video https://twitter.com/QUItCoding/status/1560876823969517569

The FPS item in QNanoPainter examples (and other places) could be improved with FrameAnimation so that there isn't a need for "dummy animation" to get the animation tickers.

Felix
1 point
32 months ago

I have been wanting this for a long time, Thanks!. Prevalence of many different refresh rate screens really necessitates a good approach, this is great. Could you please investigate whether the 'frametime' property outputs correct values? When I construct animations which advance a property with realtime like so: FrameAnimation { running: true onTriggered: { panTool.pan(frameTime, 0) } }I am still seeing some jitter in the position of the object I am trying to move. This animation should move the item in a consistent pace according to cumulative time elapsed. Is frameTime being rounded? Most likely it's a performance issue somewhere else on my end but I'm interested in the precision of frameTime

Kaj Grönholm
1 point
32 months ago

@Felix: Thanks for the comment! I feel you, there has been times when this would have been useful so now took the time to get it into Qt Quick.

Frame time is calculated with https://doc-snapshots.qt.io/qt6-dev/qelapsedtimer.html#nsecsElapsed so theoretical accuracy is nanoseconds, but the actual resolution depends of course on the platform. It is time in seconds since previous animation ticker, so contains some fluctuation from 0.016666.. (on 60Hz screen). When there is a need for more stable values you can use smoothFrameTime property which on my Windows laptop now gave values like: qml: smoothFrameTime: 0.01668457919031026 qml: smoothFrameTime: 0.016695221271279235 qml: smoothFrameTime: 0.016690869144151313 qml: smoothFrameTime: 0.01667907222973618 qml: smoothFrameTime: 0.016702725006762564 qml: smoothFrameTime: 0.016691022506086308 qml: smoothFrameTime: 0.01669270025547768 qml: smoothFrameTime: 0.016695180229929912 qml: smoothFrameTime: 0.01668765220693692 qml: smoothFrameTime: 0.016689826986243227 qml: smoothFrameTime: 0.016676244287618904 qml: smoothFrameTime: 0.016666089858857015 qml: smoothFrameTime: 0.016666790872971314 qml: smoothFrameTime: 0.016666261785674184 ...

Kaj Grönholm
0 points
32 months ago

@Marcin Thanks! Yes, useful for games and game-like UX where you need more freedom for animations.