September 2014

Volume 29 Number 9

Windows with C++ : DirectComposition: Transforms and Animation

Kenny Kerr | September 2014

Kenny KerrDirectComposition visuals provide a lot more than just the offset and content properties I’ve illustrated in my last few columns. The visuals really come to life when you begin to affect them with transforms and animations. In both cases, the Windows composition engine is a sort of processor and it’s up to you to calculate or construct the transform matrices, as well as the animation curves with cubic functions and sine waves. Thankfully, the Windows API provides the necessary support with a pair of very complementary APIs. Direct2D provides great support for defining transform matrices, making light work of describing rotation, scale, perspective, translation and much more. Likewise, the Windows Animation Manager frees you from having to be a wizard at mathematics, allowing you to describe animations using a rich library of animation transitions, a storyboard with key frames and loads more. It’s when you combine the Windows Animation Manager, Direct2D and DirectComposition into a single application that you can really experience the sheer power at your fingertips.

In my previous column (msdn.microsoft.com/magazine/dn759437) I showed how the DirectComposition API can be used along with Direct2D to get the best of both retained-mode and immediate-mode graphics. The sample project included with that column illustrated this concept with a simple application that lets you create circles, move them around, and control their Z-order quite simply. In this column I want to show you how easy it is to add some powerful effects with transforms and animation. The first thing to realize is that DirectComposition provides overloads for many of its scalar properties. You might have noticed this if you’ve been following along over the last few months as I’ve explored the API. For example, a visual’s offset can be set with the SetOffsetX and SetOffsetY methods, as follows:

ComPtr<IDCompositionVisual2> visual = ...
HR(visual->SetOffsetX(10.0f));
HR(visual->SetOffsetY(20.0f));

But the IDCompositionVisual interface IDCompositionVisual2 derives from also provides overloads of these methods that accept an animation object rather than a floating point value. This animation object materializes as the IDCompositionAnimation interface. For example, I could set a visual’s offset with one or two animation objects, depending on whether I need to animate one or both axes, like so:

ComPtr<IDCompositionAnimation> animateX = ...
ComPtr<IDCompositionAnimation> animateY = ...
HR(visual->SetOffsetX(animateX.Get()));
HR(visual->SetOffsetY(animateY.Get()));

But the composition engine is capable of animating far more than just a visual’s offset. Visuals also support 2D and 3D transformations. A visual’s SetTransform method may be used to apply a 2D transform, given a scalar value:

D2D1_MATRIX_3X2_F matrix = ...
HR(visual->SetTransform(matrix));

Here, DirectComposition actually relies on the 3x2 matrix defined by the Direct2D API. You can do things like rotate, translate, scale and skew a visual. This affects the coordinate space the visual’s content is projected in, but it’s confined within two dimensional graphics with an X and Y axis.

Naturally, the IDCompositionVisual interface provides an overload of the SetTransform method, but it doesn’t accept an animation object directly. You see, an animation object is responsible only for animating a single value over time. A matrix, by definition, consists of a number of values. You might want to animate any number of its members, depending on the effect you’d like to achieve. So, instead, the SetTransform overload accepts a transform object:

ComPtr<IDCompositionTransform> transform = ...
HR(visual->SetTransform(transform.Get()));

It’s the transform object, specifically the various interfaces derived from IDCompositionTransform, that provides overloaded methods that accept either scalar values or animation objects. In this way, you might define a rotation matrix with an animated angle of rotation, but a fixed center point and axes. What you animate is, of course, up to you. Here’s a simple example:

ComPtr<IDCompositionRotateTransform> transform = ...
HR(transform->SetCenterX(width / 2.0f));
HR(transform->SetCenterY(height / 2.0f));
HR(transform->SetAngle(animation.Get()));
HR(visual->SetTransform(transform.Get()));

The IDCompositionRotateTransform interface derives from IDCompositionTransform and represents a 2D transform that affects the rotation of a visual around the Z-axis. Here, I’m setting the center point to a fixed value, based on some width and height, and using an animation object to control the angle.

So that’s the basic pattern. I’ve only described 2D transforms, but 3D transforms work in much the same way. Now let me illustrate more practically by showing you how you can take the sample project included with my previous column and transform and animate it in various ways with the help of Direct2D and the Windows Animation Manager.

Illustrating transforms and animation in a print magazine does begin to push the limits of what can easily be grasped by staring at a static page. If you’d like to see it in action, check out my online course, where you can watch as it all comes to life (bit.ly/WhKQZT). To make the concepts a little more print-friendly, I’ll take the liberty of tweaking the sample project from my previous column to use squares rather than circles. This should make the various transformations a little more obvious on paper. First, I’ll replace the SampleWindow m_geometry member variable with a rectangle geometry:

ComPtr<ID2D1RectangleGeometry> m_geometry;

Then in the SampleWindow CreateFactoryAndGeometry method I’ll get the Direct2D factory to create a rectangle geometry instead of the ellipse geometry:

D2D1_RECT_F const rectangle =
  RectF(0.0f, 0.0f, 100.0f, 100.0f);
HR(m_factory->CreateRectangleGeometry(
  rectangle,
  m_geometry.GetAddressOf()));

And that’s all it takes. The rest of the app will simply use the geometry abstraction for rendering and hit testing as before. You can see the result in Figure 1.

Illustrating Transforms and Animation with Squares Instead of Circles
Figure 1 Illustrating Transforms and Animation with Squares Instead of Circles

Next, I’m going to add a simple message handler to respond to the WM_KEYDOWN message. In the SampleWindow MessageHandler method I’ll add an if statement for this:

else if (WM_KEYDOWN == message)
{
  KeyDownHandler(wparam);
}

As usual, the handler will need the necessary error handling to recover from device loss. Figure 2 provides the usual pattern of releasing the device resources and invalidating the window so the WM_PAINT message handler can rebuild the device stack. I’m also limiting the handler to the Enter key to avoid confusion with the Control key, which is used when adding shapes.

Figure 2 Scaffolding for Device Loss Recovery

void KeyDownHandler(WPARAM const wparam)
{
  try
  {
    if (wparam != VK_RETURN)
    {
      return;
    }
    // Do stuff!
  }
  catch (ComException const & e)
  {
    TRACE(L"KeyDownHandler failed 0x%X\n",
      e.result);
    ReleaseDeviceResources();
    VERIFY(InvalidateRect(m_window,
                          nullptr,
                          false));
  }
}

At this point, I’m ready to start experimenting with transforms and animations. Let’s begin simply with a basic 2D rotation transform. First, I need to determine the center point that will represent the Z axis, or the point around which to rotate. Because DirectComposition expects physical pixel coordinates, I can simply use the GetClientRect function here:

RECT bounds {};
VERIFY(GetClientRect(m_window, &bounds));

I can then derive the center point of the window’s client area, as follows:

D2D1_POINT_2F center
{
  bounds.right / 2.0f,
  bounds.bottom / 2.0f
};

I can also rely on the Direct2D matrix helper functions to construct a matrix describing a 2D rotation transform of 30 degrees:

D2D1_MATRIX_3X2_F const matrix =
  Matrix3x2F::Rotation(30.0f, center);

And then I can simply set a visual’s transform property and commit the change to the visual tree. I’ll just apply this change to the root visual for simplicity:

HR(m_rootVisual->SetTransform(matrix));
HR(m_device->Commit());

You can, of course, apply any number of changes to any number of visuals and the composition engine will take care of coordinating it all effortlessly. You can see the results of this simple 2D transform in Figure 3. You might notice some aliasing. Even though Direct2D defaults to anti-aliasing, it assumes the drawing will appear in the coordinate space in which it was rendered. The composition engine doesn’t have any knowledge of the geometry the composition surface was rendered with, so it has no way of correcting this. In any case, once animation is added to the mix, the aliasing will be short-lived and hard to spot.

A Simple 2D Transform
Figure 3 A Simple 2D Transform

To add animation to this transformation, I need to switch the matrix structure out for a composition transform. I’ll replace both the D2D1_POINT_2F and D2D1_MATRIX_3X2_F structures with a single rotate transform. First, I need to create the rotate transform using the composition device:

ComPtr<IDCompositionRotateTransform> transform;
HR(m_device->CreateRotateTransform(transform.GetAddressOf()));

Keep in mind that even such seemingly simple objects must be discarded if and when the device is lost and recreated. I can then set the center point and angle using interface methods rather than a Direct2D matrix structure:

HR(transform->SetCenterX(bounds.right / 2.0f));
HR(transform->SetCenterY(bounds.bottom / 2.0f));
HR(transform->SetAngle(30.0f));

And the compiler picks the appropriate overload to handle the composition transform:

HR(m_rootVisual->SetTransform(transform.Get()));

Running this produces the same effect as in Figure 3, because I haven’t yet added any animation. Creating an animation object is simple enough:

ComPtr<IDCompositionAnimation> animation;
HR(m_device->CreateAnimation(animation.GetAddressOf()));

I can then use this animation object instead of the constant value when setting the angle:

HR(transform->SetAngle(animation.Get()));

It gets more interesting when you try to configure the animation. Simple animations are relatively straightforward. As I mentioned earlier, animations are described with cubic functions and sine waves. I can animate this rotation angle with a linear transition, where the value progresses from 0 to 360 over 1 second by adding a cubic function:

float duration = 1.0f;
HR(animation->AddCubic(0.0,
                       0.0f,
                       360.0f / duration,
                       0.0f,
                       0.0f));

The third parameter of the AddCubic method indicates the linear coefficient, so that makes sense. If I left it there, the visual would rotate 360 degrees every second for eternity. I can decide to bring the animation to an end once it reaches 360 degrees, as follows:

HR(animation->End(duration, 360.0f));

The first parameter of the End method indicates the offset from the beginning of the animation, regardless of the value of the animation function. The second parameter is the final value of the animation. Keep this in mind because the animation will “snap” to that value if it doesn’t line up with the animation curve, and that would produce a jarring visual effect.

Such linear animations are easy enough to reason about, but more complex animations can become exceedingly complicated. That’s where the Windows Animation Manager comes in. Rather than having to call the various methods of IDCompositionAnimation to add sinusoidal and cubic polynomial segments, repeated segments, and more, I can construct an animation storyboard with the Windows Animation Manager and its rich library of transitions. When that’s done, I can use the resulting animation variable to populate the composition animation. This involves a bit more code, but the benefit is a great deal more power and control over your app’s animation. First, I need to create the animation manager itself:

ComPtr<IUIAnimationManager2> manager;
HR(CoCreateInstance(__uuidof(UIAnimationManager2),
  nullptr, CLSCTX_INPROC, __uuidof(manager),
  reinterpret_cast<void **>(manager.GetAddressOf())));

Yes, the Windows Animation Manager relies on COM activation, so be sure to call CoInitializeEx or RoInitialize to initialize the runtime. I also need to create the transition library:

ComPtr<IUIAnimationTransitionLibrary2> library;
HR(CoCreateInstance(__uuidof(UIAnimationTransitionLibrary2),
  nullptr, CLSCTX_INPROC, __uuidof(library),
  reinterpret_cast<void **>(library.GetAddressOf())));

Typically, applications will hold on to these two objects for their lifetime as they’re needed for continuous animation and, specifically, for velocity matching. Next, I need to create an animation storyboard:

ComPtr<IUIAnimationStoryboard2> storyboard;
HR(manager->CreateStoryboard(storyboard.GetAddressOf()));

The storyboard is what associates transitions with animation variables and defines their relative schedule over time. The storyboard is able to aggregate various transitions applied to different animation variables; it ensures they remain synchronized; and it’s the storyboard that’s scheduled as a whole. Of course, you can create multiple storyboards to schedule independent animations. Now, I need to ask the animation manager to create an animation variable:

ComPtr<IUIAnimationVariable2> variable;
  HR(manager->CreateAnimationVariable(
    0.0, // initial value
    variable.GetAddressOf()));

Once a storyboard is scheduled, the animation manager is responsible for keeping the variable up-to-date so the app can request the effective value at any time. In this case, I’m simply going to use the animation variable to populate the composition animation with segments and then discard it. Now I can use the mighty transition library to create an interesting transition effect for the animation variable:

ComPtr<IUIAnimationTransition2> transition;
HR(library->CreateAccelerateDecelerateTransition(
  1.0,   // duration
  360.0, // final value
  0.7,   // acceleration ratio
  0.3,   // deceleration ratio
  transition.GetAddressOf()));

The transition will cause the animation variable to speed up and then slow down over the given duration until it comes to a rest at its final value. The ratios are used to affect how relatively quickly the variable will accelerate and then decelerate. Keep in mind that the ratios combined can’t exceed a value of one. With the animation variable and transition ready to go, I can add them to the storyboard:

HR(storyboard->AddTransition(variable.Get(),
                             transition.Get()));

The storyboard is now ready to be scheduled:

HR(storyboard->Schedule(0.0));

The single parameter of the Schedule method tells the scheduler the present animation time. This is more useful for coordinating animation and coordinating with the composition engine’s refresh rate, but it will do for now. At this point, the animation variable is primed and I can ask it to populate the composition animation with curves:

HR(variable->GetCurve(animation.Get()));

In this case, it’s as if I called the composition animation, as follows:

HR(animation->AddCubic(0.0, 0.0f, 0.0f, 514.2f, 0.0f));
HR(animation->AddCubic(0.7, 252.0f, 720.0f, -1200.0f, 0.0f));
HR(animation->End(1.0, 360.0f));

That’s certainly a lot less code than it took with the Windows Animation Manager, but making sense of that math isn’t that simple. The Windows Animation Manager also offers the ability to coordinate and smoothly transition running animations, something that would be exceedingly difficult to do manually.


Kenny Kerr is a computer programmer based in Canada, as well as an author for Pluralsight and a Microsoft MVP. He blogs at kennykerr.ca and you can follow him on Twitter at twitter.com/kennykerr.

Thanks to the following Microsoft technical experts for reviewing this article: Leonardo Blanco and James Clarke