Playing with the Windows Animation Manager: Fixing a sample


Windows 7 provides a component known as the Windows Animation Manager, known to some people by its acronym WAM, pronounced "wham". There are some nice sample programs for WAM on MSDN, but for some reason, the authors of the samples decided to animate the three color components of a resultant color.

Because apparently the authors of those sample programs can look at a color and say, "Oh, clearly the red component of this color increases gradually at first, then speeds up its rate of increase, and then slows back down until it reaches its final value; while simultaneously the blue component is doing the opposite, but over a shorter time span, and the green component is remaining fixed."

Today's exercise is to fix the sample program so you can actually see and understand what WAM is doing, rather than just watching psychedelic colors change and saying, "Gee, that's pretty."

But first, some background:

Windows Animation is a component which manipulates variables. A variable is a number which varies over time. You tell Windows Animation things like "I would like you to animate this variable from 1 to 10 over the next 7 seconds." You can then interrogate the variable for its current value, and it might say "Right now, the value is 6."

The idea is that each of these variables is connected to some visual property, like the position of an object. When you paint the object, you consult the current value of the variable to find out where you should draw it.

One of the annoying bits about Windows Animation is that you have to set up a bunch of stuff just to get things started. You need an animation manager, which is the object that runs the show. You also need an animation timer whose job is to tell the animation manager what time it is. (Under normal circumstances, you would use the default timer, which records real-world time, but you might want to replace it with a special timer for debugging that runs at half-speed, or maybe one which varies its speed based on how fast you clap.)

Okay, back to fixing the sample.

Start with the Timer-Driven Animation and make these changes:

                // disable the initial animation
                // Fade in with Red
                // hr = ChangeColor(COLOR_MAX, COLOR_MIN, COLOR_MIN);

HRESULT CMainWindow::DrawBackground(
    Graphics &graphics,
    const RectF &rectPaint
    )
{
    // Get the RGB animation variable values

    INT32 red;
    HRESULT hr = m_pAnimationVariableRed->GetIntegerValue(
        &red
        );
    if (SUCCEEDED(hr))
    {
        INT32 green;
        hr = m_pAnimationVariableGreen->GetIntegerValue(
            &green
            );
        if (SUCCEEDED(hr))
        {
            INT32 blue;
            hr = m_pAnimationVariableBlue->GetIntegerValue(
                &blue
                );
            if (SUCCEEDED(hr))
            {
                // Replace the drawing code as follows
                SolidBrush brushBackground(Color(255, 255, 255));
                hr = HrFromStatus(graphics.FillRectangle(
                    &brushBackground,
                    rectPaint
                    ));

                SolidBrush brushCircle(Color(0, 0, 0));
                hr = HrFromStatus(graphics.FillEllipse(
                    &brushCircle,
                    red, green, 10, 10
                    ));
            }
        }
    }

    return hr;
}

Instead of drawing a psychedelic background color, I draw a small circle using the old red value as the x-coordinate, and the old green value as the y-coordinate. I didn't rename the variables or get rid of the unused blue variable because I wanted to make as few changes as possible.

Run this program, and click to make the circle move. Observe that when the circle moves, it starts slowly, then accelerates, and then decelerates as it gets closer to its final location. What's more, if you click while the circle is still moving, the circle demonstrates inertia as it turns to head toward its new target location.

I bet you never noticed the acceleration, deceleration, or inertia in the original background-color version.

With a little bit of work, you can make the sample even more interesting by making the circle go to where you clicked. It looks like a lot of work when I spell it out below, but most of it consists of deleting code.

First, do a search/replace and rename m_pAnimationVariableRed to m_pAnimationVariableX, and rename m_pAnimationVariableGreen to m_pAnimationVariableY. Delete m_pAnimationVariableBlue entirely, as well as any references to it. I decided to just bite the bullet and deal with the consequences of renaming/deleting variables.

Now we can simplify the CMain­Window::Create­Animation­Variables method so all it does is create the two coordinate variables.

HRESULT CMainWindow::CreateAnimationVariables()
{
    HRESULT hr = m_pAnimationManager->CreateAnimationVariable(
        0,
        &m_pAnimationVariableX
        );
    if (SUCCEEDED(hr))
    {
        hr = m_pAnimationManager->CreateAnimationVariable(
            0,
            &m_pAnimationVariableY
            );
    }

    return hr;
}

We want the circle to move when you click the mouse, so let's do that. Delete CMain­Window::On­LButton­Down and change the window procedure so that clicks move the circle.

LRESULT CALLBACK CMainWindow::WndProc(
    HWND hwnd,
    UINT uMsg,
    WPARAM wParam,
    LPARAM lParam
    )
{
    ...
        case WM_LBUTTONDOWN:
            {
                pMainWindow->ChangePos(
                    (SHORT)LOWORD(lParam),
                    (SHORT)HIWORD(lParam)
                 );
            }
            return MESSAGE_PROCESSED;
    ...
}

And rename the member function Change­Color to Change­Pos, and instead of taking red and green, have it take x and y.

HRESULT CMainWindow::ChangePos(
    INT x,
    INT y
    )
{
    const UI_ANIMATION_SECONDS DURATION = 0.5;
    const DOUBLE ACCELERATION_RATIO = 0.5;
    const DOUBLE DECELERATION_RATIO = 0.5;

    // Create a storyboard

    IUIAnimationStoryboard *pStoryboard = NULL;
    HRESULT hr = m_pAnimationManager->CreateStoryboard(
        &pStoryboard
        );
    if (SUCCEEDED(hr))
    {
        // Create transitions for the position animation variables

        IUIAnimationTransition *pTransitionX;
        hr = m_pTransitionLibrary->CreateAccelerateDecelerateTransition(
            DURATION,
            x,
            ACCELERATION_RATIO,
            DECELERATION_RATIO,
            &pTransitionX
            );
        if (SUCCEEDED(hr))
        {
            IUIAnimationTransition *pTransitionY;
            hr = m_pTransitionLibrary->CreateAccelerateDecelerateTransition(
                DURATION,
                y,
                ACCELERATION_RATIO,
                DECELERATION_RATIO,
                &pTransitionY
                );
            // delete former "blue" transition
            if (SUCCEEDED(hr))
            {
                // Add transitions to the storyboard

                hr = pStoryboard->AddTransition(
                    m_pAnimationVariableX,
                    pTransitionX
                    );
                if (SUCCEEDED(hr))
                {
                    hr = pStoryboard->AddTransition(
                        m_pAnimationVariableY,
                        pTransitionY
                        );
                    // delete former "blue" transition
                    if (SUCCEEDED(hr))
                    {
                        // Get the current time and schedule the storyboard for play
                        UI_ANIMATION_SECONDS secondsNow;
                        hr = m_pAnimationTimer->GetTime(
                            &secondsNow
                            );
                        if (SUCCEEDED(hr))
                        {
                            hr = pStoryboard->Schedule(
                                secondsNow
                                );
                        }
                    }
                }

                // delete former "blue" transition

                pTransitionY->Release();
            }

            pTransitionX->Release();
        }

        pStoryboard->Release();
    }

    return hr;
}

Now you can click the mouse on the client area, and the dot will chase it like a puppy.

The basic idea behind the Windows Animation Library is that for each property you want to animate, you associate an animation variable, and when you want to perform the animation, you create a transition for each variable describing how you want the animation to proceed, put all the transitions into a storyboard, and then schedule the storyboard.

Of course, you can build optimizations on top of the basic idea. For example, you might not create the animation variable until the first time you need to animate the property. Another optimization is invalidating only the parts of the window that need repainting, rather than invalidating the entire client area. You can do this by registering a change handler on your variables: When the change handler notifies you that a value changed, invalidate the old position and the new position. This will erase the old location and draw at the new location.

Next time, I'll build a program that animates a hundred objects, just for fun.

Comments (12)
  1. Jon says:

       hr = m_pAnimationVariableRed->GetIntegerValue(&red);

       if (SUCCEEDED(hr))

           hr = m_pAnimationVariableGreen->GetIntegerValue(&green);

           if (SUCCEEDED(hr))

               hr = m_pAnimationVariableBlue->GetIntegerValue(&blue);

                   if (SUCCEEDED(hr))

                       Draw()

    If you're going to animate 100 objects then I'm going to need a wider monitor.

  2. Danny says:

    "One of the annoying bits about Windows Animation is that you have to set up a bunch of stuff just to get things started. You need an animation manager, which is the object that runs the show. You also need an animation timer whose job is to tell the animation manager what time it is."

    Welcome to 3D world Microsoft. You need a "scene" where the show takes places. You need a "camera", the very thing that you see things through. You need light(s) to illuminate the objects / or you can embed the lights inside the objects. So NO Ray, those annoying bits are necessary. Use Ray's example on WMA folks, and you will make one step closer to 3D programming. I hardly can wait to shed off all the controls in WinForms and the only thing left in the world to be WPF (or the equivalent in other OS'es)

  3. Kawa says:

    I don't usually write in C++, but when I do I have experts like Chen hold my hand for every line.

  4. Sökö says:

    @Danny: "annoying" != "unnecessary". Also, Direct3D (en.wikipedia.org/…/Microsoft_Direct3D). You (apparently) heard it here first.

  5. dsn says:

    @jon

    If only there was some kind of looping mechanism in C that Raymond could use :)

  6. Paul Parks says:

    @Jon: All of my COM-related code looks like that. I tried some rather elaborate alternatives having to do with templates and exceptions, but the results tended to be slower, more fragile, more difficult for other developers to modify & understand, or some combination of all the preceding. It ends up being simpler the way Raymond coded it above, I think.

    One alternative I do use, if the nesting gets too deep or modifications are likely in the middle, is to use goto.

    (Now I've done it. I've started a holy war. Sorry, Raymond.)

  7. SimonRev says:

    COM is just ugly to program.  Personally I use a rather simple macro to convert HR failures to exceptions as well as logging the line of code where the error occurred.  

    Something like:

    HR(m_pAnimationVariableRed->GetIntegerValue(&red));

    HR(m_pAnimationVariableGreen->GetIntegerValue(&green));

    HR(m_pAnimationVariableGreen->GetIntegerValue(&blue));

    I find much easier to read, if performance isn't critical and I am already using exceptions.

    Note:  If you don't want exceptions, you can get the same effect without all the indenting by simply:

    hr = m_pAnimationVariableRed->GetIntegerValue(&red);

    if (SUCCEEDED(hr))

       hr = m_pAnimationVariableGreen->GetIntegerValue(&green);

    if (SUCCEEDED(hr))

       hr = m_pAnimationVariableBlue->GetIntegerValue(&blue);

    if (SUCCEEDED(hr))

       Draw()

  8. @Paul M. Parks:

    The use of goto won't turn into a big a thing as you may think, especially not in this situation. The issues with goto stemmed from people using it everywhere resulting in unreadable code. The use of goto here allows you to jump over all of the function calls that will never get executed when you are in a failed condition, so it simplifies the code and doesn't make it any harder to read or follow. I really don't think you will get attacked by a velociraptor here.

    I also think the situation has changed drastically since goto obtained the stigma that it still has. Modern languages are a totally different beast. This means that the need for goto has decreased dramatically too.

  9. Simon R says:

    if (!SUCCEEDED(hr))

       return hr;

    (Assuming the function signature allows it to return the status)

    ???

    [That requires RAII, which the sample code doesn't use. -Raymond]
  10. Rick C says:

    @Simon R:  That's usually a safe assumption with COM programming.  COM strongly encourages you to make your COM-related functions return HRESULTs.

  11. Paul Parks says:

    @Crescens2k: Yeah, that's pretty much what I was saying.

  12. Danny says:

    @Sökö

    Pff, you missed my post by a mile. Read again and tell me when you stumble upon "WPF" word then go to your snarky comment about Direct3D and cry. Oh, you might read this as well: en.wikipedia.org/…/Windows_Presentation_Foundation

    But lemme quote from the beginning of article: "Rather than relying on the older GDI subsystem, WPF utilizes DirectX". Now you can go and cry.

Comments are closed.