Smooth animations using the QtQuick Canvas

Google's Material Design showcases a few nicely detailed animations that add life to the user interface. QML makes it straightforward to create the traditional moving, scaling and opacity change animations while taking advantage of the GPU, but how can we create an animation changing the shape of an element and not merely transforming it?

We'll make sure that we use the GPU to accelerate the rendering and use standard QtQuick animations to control the drawing evolution, as conveniently as with traditional transform animations.

Since you can animate any property in QML, not only the built-in ones, you can define the animation parameters declaratively as properties and then use those as input for your JavaScript Canvas drawing code, requesting a repaint each time an input changes.

Using QML properties to drive the animation

Then let's add the animation logic for the rotation. Use a State, triggered when the arrowFormState boolean property is true, make our whole drawing rotate by 180 degrees in that state and specify how we want it to be animated:

Each time one of our animated values change, tell the canvas to paint itself:

onAngleChanged: requestPaint()

The Canvas is using the software rasterizer by default, this permits using functions like getImageData() without problems (which is slow if the pixels are lying in graphics memory). In our case however, we prefer having our drawing rendered as fast as possible to allow a smooth animation. Use the FramebufferObject renderTarget to use the OpenGL paint engine and the Cooperative renderStrategy to make sure that OpenGL calls are made in the QtQuick render thread:

In practice we'll react to input events from a MouseArea, but for the sake of keeping the code simple in this demo we use a Timer to trigger a state change:

Timer { repeat: true; running: true; onTriggered: toggle() }

And this is what we get:

Taking advantage of existing animations

Pretty, although it would be nicer if the rotation would always be clockwise. This is possible to do with a NumberAnimation, but QtQuick already provides this functionality in RotationAnimation, we can just tell it to update our custom angle property instead. Since QtQuick uses degrees, except for the Canvas API which requires radians, we'll convert to radians in our paint code:

Change the shape based on animation parameters

Lastly we'll add the morphing logic. Create a new morphProgress property that we'll animate from 0.0 to 1.0 between the states, derive intermediate drawing local variables from that value and finally use them to animate the position of the line extremities between state changes. We could use a separate property for each animated parameter and let Qt animations do the interpolation, but this would spread the drawing logic around a bit more:

Wrapping it up

This is a simple example, but a more complex drawing will both be more difficult to maintain and risk hitting performance bottlenecks, which would defeat the purpose of the approach. For that reason it's important consider the limits of the technology while designing the UI.

Even though not as smooth or responsive, an AnimatedImage will sometimes be a more cost effective approach and require less coordination between the designer and the developer.

Performance and resources

Each pixel will need to be rendered twice for each frame, once onto the framebuffer object and then from the FBO to the window. This can be an issue if many Canvas items are animating at the same time of if the Canvas is taking a large portion of the screen on lower-end hardware.

The OpenGL paint engine isn't a silver bullet and state changes on the Canvas' context should be avoided when not necessary. Since draw calls aren't batched together, issuing a high number of drawing commands can also add overhead and reduce OpenGL's ability of parallelizing the rendering.

Declarative animations are great, but since we are writing our rendering code in JavaScript we are losing a part of their advantage and must accept a small overhead caused by our imperative painting code.

This leads us to our next blog post, next week we'll see how we can reduce the overhead to almost nothing by using a much more resource effective QML item: the ShaderEffect. You can subscribe via RSS or e-mail to be notified.