I thought I'd start my blog on media and animations in "WPF/E" with a frequently asked question about how to accomplish state-based animations on the Dec CTP bits.  This can be a bit tricky given a couple of the limitations and issues that were not fixed in time for the CTP, so I thought I'd share a way of accomplishing this.

This post assumes you are familiar with WPF/E.  If you are not, you can find samples and documentation in the WPF/E Dec CTP SDK, which can be found at http://microsoft.com/wpfe.  There are other resources at that location as well.

By state-based animations, I mean any set of animations that affect the same set of properties on the same object.  Each set of animations can be grouped together to indicate a transition to a new state, and all animations in a set run in parallel.  The classic example is a button that expands when the user mouses over (expanded state), and shrinks when the user mouses out (shrunk state).  Another example is a photo library app where each image can be minimized to a filmstrip area (thumbnail state), or expanded to fit a viewing area (full-view state), where each image goes into full-view state when the user selects it.

There are basically three guidelines that you should follow to successfully create a state-based animation on the Dec CTP bits.  More details about how these were derived are below, but they are:

  1. Create a different Storyboard for each state of your object, each of which defines a different target value for the animations.
  2. Set BeginTime="1 on each Storyboard, so that they can be started interactively by calling the begin() method.
  3. When switching states, first begin the Storyboard associated with the new state, then stop any other storyboards associated with this state change.

In this post, I go over the creation of a mini-panel that has three states.  Each state represents a different size and position on the screen, and the transition between states is done using Storyboards.  This could be used to provide a richer user experience when navigating between different elements on a page (e.g., user selects a an image out a number of thumbnails).  A snapshot of the sample can be found below, and the code for it can be found at the end of the post (I haven't figured out a good way to post a sample).

blogPost01Main

In the above image, the rust-colored rectangle is the mini-panel that moves around the WPF/E control.  The mini-panel has three rectangles inside of it, indicating the different sizes and positions it can assume: top left, bottom left and right.  The mini-panel is at the top left position in the image above.  There are three other rectangles indicating, in the WPF/E control, the location of the mini-panel when it's in each of the three possible states.

The idea is that the panel should move between states based on some user input.  This could be a MouseEnter event in the case of a growing/shrinking button, or a mouse click selecting a different image to display.  In this sample, a state transition occurs whenever the user clicks on one of the rectangles inside of the panel.  Each state is achieved in this example by modifying two transforms on the panel:

<TransformGroup>
  <
ScaleTransform x:Name="myScale" ScaleX="3.2" ScaleY="1.7"/>
  <
TranslateTransform x:Name="myTranslate" X="20" Y="20"/>
</
TransformGroup>

The properties on these two transforms fully define each state.  In this case, state 1 shown in the picture requires a scaling factor of 3.2 horizontally, a scaling factor of 1.7 in the vertical direction, and a translation of 20 in both X and Y directions.

In order to create animations between transitions, a Storyboard is needed to animate the properties above to their appropriate values.  For example, state 1 could be represented by the Storyboard below.  Instead of directly setting these properties, this Storyboard tells the panel to animate to the appropriate values in a certain amount of time.

<Storyboard x:Name="state1SB">
  <
DoubleAnimation To="20" Duration="0:0:1" Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.X)/>"/>
  <
DoubleAnimation To="20" Duration="0:0:1" Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.Y)/>"/>
  <
DoubleAnimation To="3.2" Duration="0:0:1" Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleX)/>"/>
  <
DoubleAnimation To="1.7" Duration="0:0:1" Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleY)/>"/>
</
Storyboard>

Note that in the animations above, a "From" value is not defined.  This means that the animation will begin from the property value at the instant this Storyboard is begun.  Each state also has a different Storyboard associated with it ("state2SB" and "state3SB", which are identical to "state1SB" except for the "To" values in their animations).

Note also that the animations above are interactive (i.e., they start only in response to user input).  By default on the Dec CTP, however, all animations begin when the element containing the Storyboard is Loaded, i.e., shown on the screen.  We need a way of specifying that the Storyboard should not begin at that point but rather when the user does something.  On Dec CTP bits, this can be accomplished by setting a large BeginTime on the storyboard, such as 1 day.  This way, the Storyboard object only kicks off when its begin() method is called.  Please note this is a limitation in the Dec CTP bits, and will be more easily done in future CTPs.

<Storyboard x:Name="state1SB" BeginTime="1"> ... </Storyboard>
<Storyboard x:Name="state2SB" BeginTime="1"> ... </Storyboard>
<Storyboard x:Name="state3SB" BeginTime="1"> ... </Storyboard>

Now that the Storyboards will not start immediately, we need a way of specifying when they should begin. In this sample, the panel changes state whenever the different rectangles inside of it are clicked, so we listen for the MouseLeftButtonUp events on them:

<Rectangle Canvas.Left="4" Canvas.Top="4" Height="34" Width="64" Fill="#FFBBBBBB" MouseLeftButtonUp="javascript:onState1Selected"/>
<Rectangle Canvas.Left="72" Canvas.Top="4" Height="92" Width="24" Fill="#FFBBBBBB" MouseLeftButtonUp="javascript:onState2Selected"/>
<
Rectangle Canvas.Left="4" Canvas.Top="42" Height="54" Width="64" Fill="#FFBBBBBB" MouseLeftButtonUp="javascript:onState3Selected"/>

Inside of each event handler, we need to do two things (the order is crucial):

  1. Begin the storyboard associated with the newly selected state.
  2. Stop any other storyboards that are associated with the properties being animated.  In this case, we need to stop the Storyboards associated with the other states.

  function onState1Selected(sender, mouseEventArgs)
  {
    state1SB.begin();
    state2SB.stop();
    state3SB.stop();

  }

This is another workaround that should only be needed for the Dec CTP of WPF/E.  The reason why you need to call stop on the other Storyboards is that we can only have a single animation on any given property at a time.  When another animation is started on a property that already has an animation applied to it, the latest one should remove the previous animation and replace it.  This is not working in every case on Dec CTP bits, so you need to call stop() on the previous ones.  Also, you need to begin the new Storyboard _before_ stopping the other ones to ensure that the current property value is correctly snapshoted (keep in mind the animations specified above have a default From value, so they snapshot the current property values when they begin).

By following these three simple (though not very discoverable) guidelines, you too can accomplish state-based animations in the Dec CTP of WPF/E:

  • Define a different Storyboard for each state by using implicit "From" values.  This allows the runtime to use the current property value as the initial value for your animation.  It also allows the user to start a new state transitions while another one is taking place.
  • Delay the starting time of your Storyboards by setting BeginTime="1" (this means 1 day).  This allows you to start your animations later in response to some user input by calling the begin() method on your Storyboard.
  • To enter a state, first begin the Storyboard corresponding to that state, then stop all of the remaining Storyboards.  This will ensure that you only have a single Storyboard running on the same property at any given time.

This enables a multitude of rich user experiences without requiring a lot of code on your part!

XAML for the full Demo:

<Canvas xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Loaded="javascript:onLoaded">
  <Rectangle Height="500" Width="500">
    <
Rectangle.Fill>
      <
LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <
GradientStop Offset="0" Color="DarkBlue"/>
        <
GradientStop Offset="1" Color="Black"/>
      </LinearGradientBrush>
    </
Rectangle.Fill>
  </
Rectangle>
  <
Rectangle Canvas.Left="15" Canvas.Top="15" Height="180" Width="330" Fill="#90EEEEEE"/>
  <
Rectangle Canvas.Left="15" Canvas.Top="205" Height="280" Width="330" Fill="#90EEEEEE"/>
  <
Rectangle Canvas.Left="355" Canvas.Top="15" Height="470" Width="130" Fill="#90EEEEEE"/>

  <Canvas>
    <
Canvas.RenderTransform>
      <
TransformGroup>
        <
ScaleTransform x:Name="myScale" ScaleX="3.2" ScaleY="1.7"/>
        <
TranslateTransform x:Name="myTranslate" X="20" Y="20"/>
      </
TransformGroup>
    </
Canvas.RenderTransform>
    <
Canvas.Triggers>
      <
EventTrigger RoutedEvent="Canvas.Loaded">
        <
EventTrigger.Actions>
          <
BeginStoryboard>
            <
Storyboard BeginTime="1" x:Name="state1SB">
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.X)">
                <
SplineDoubleKeyFrame Value="20" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.Y)">
                <
SplineDoubleKeyFrame Value="20" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleX)">
                <
SplineDoubleKeyFrame Value="3.2" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleY)">
                <
SplineDoubleKeyFrame Value="1.7" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
            </
Storyboard>
          </
BeginStoryboard>
          <
BeginStoryboard>
            <
Storyboard BeginTime="1" x:Name="state2SB">
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.X)">
                <
SplineDoubleKeyFrame Value="360" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.Y)">
                <
SplineDoubleKeyFrame Value="20" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleX)">
                <
SplineDoubleKeyFrame Value="1.2" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleY)">
                <
SplineDoubleKeyFrame Value="4.6" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
            </
Storyboard>
          </
BeginStoryboard>
          <
BeginStoryboard>
            <
Storyboard BeginTime="1" x:Name="state3SB">
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.X)">
                <
SplineDoubleKeyFrame Value="20" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myTranslate" Storyboard.TargetProperty="(TranslateTransform.Y)">
                <
SplineDoubleKeyFrame Value="210" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleX)">
                <
SplineDoubleKeyFrame Value="3.2" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
              <
DoubleAnimationUsingKeyFrames Storyboard.TargetName="myScale" Storyboard.TargetProperty="(ScaleTransform.ScaleY)">
                <
SplineDoubleKeyFrame Value="2.7" KeyTime="0:0:1" KeySpline="0,1 0,1"/>
              </
DoubleAnimationUsingKeyFrames>
            </Storyboard>
          </
BeginStoryboard>
        </
EventTrigger.Actions>
      </
EventTrigger>
    </
Canvas.Triggers>
    <
Rectangle Height="100" Width="100" Fill="#FF886666"/>
    <
Rectangle Canvas.Left="4" Canvas.Top="4" Height="34" Width="64" Fill="#FFBBBBBB" MouseLeftButtonUp="javascript:onState1Selected"/>
    <Rectangle Canvas.Left="72" Canvas.Top="4" Height="92" Width="24" Fill="#FFBBBBBB" MouseLeftButtonUp="javascript:onState2Selected"/>
    <
Rectangle Canvas.Left="4" Canvas.Top="42" Height="54" Width="64" Fill="#FFBBBBBB" MouseLeftButtonUp="javascript:onState3Selected"/>
  </
Canvas>
</
Canvas>

 JavaScript:

var wpfe;
var state1SB = null;

var state2SB = null;
var state3SB = null;

function onLoaded (s,e)
{
  wpfe = document.getElementById(
"WpfeControl1"); // assumes control is 500x500
  state1SB = wpfe.findName("state1SB");
  state2SB = wpfe.findName("state2SB");
  state3SB = wpfe.findName("state3SB");

}

function onState1Selected(sender, mouseEventArgs)
{
  state1SB.begin();
  state2SB.
stop();
  state3SB.
stop();
}

function onState2Selected(sender, mouseEventArgs)
{
  state2SB.
begin();
  state1SB.
stop();
  state3SB.
stop();
}

function onState3Selected(sender, mouseEventArgs)
{
  state3SB.
begin();
  state1SB.
stop();
  state2SB.
stop();
}