Thursday, January 27, 2011

Mastering Storyboards One Mistake at a Time

I've always been interested in animation. I wrote over 100 programs in QBASIC and Turbo C++ as a teenager where I was creating various animations and exploring new techniques. Then I graduated to newer technologies like OpenGL, Flash, and much later -- Silverlight.

I almost became a game programmer, and probably would have if I didn't have to relocate. But anyway, as time goes on you begin to understand what tools to use and when. And obviously animation is necessary in today's video games, but it's not something that should be overused in other applications. Still, I contend that when used in moderation, it can be a nice finishing touch.

Storyboards

Storyboards in Silverlight are a simple, declarative way to define animation sequences. They can be used to animate just about any property on a UIElement.

I'm going to walk through a few Storyboard examples that build upon eachother while pointing out several different techniques (some better than others). The goal is not to cover every technical possibility with Silverlight. Rather, it's to intentionally run into common technical, design, or usability mistakes and finally arrive at something that's pretty good.

In my examples below I chose to animate the Opacity property. The Opacity property is simply a double value that accepts any number between 0 (transparent) and 1 (opaque).

I'm going to create a set of images and captions, where both the image and caption will fade their opacity to 1 (opaque) as you mouseover them. I also want them to be clickable to open a bigger image.

Example 1


This first attempt is simple and straightforward.

First, I create my images and captions in XAML:

<Grid x:Name="LayoutRoot" Background="Black">
 <StackPanel HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10">
  <StackPanel Orientation="Horizontal">
   <Image x:Name="Image1" Opacity="0.5" Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/011/788697398_xPrbE-Tiny.jpg"/>
   <TextBlock x:Name="Caption1" Opacity="0.5" VerticalAlignment="Center" Width="250" Text="I arrived on Santa Cruz Island and after just a few minutes I stumbled across this view." TextWrapping="Wrap" />
  </StackPanel>
  <StackPanel Orientation="Horizontal">
   <Image x:Name="Image2" Opacity="0.5" Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/136/789417908_3bjhd-Tiny.jpg"/>
   <TextBlock x:Name="Caption2" Opacity="0.5" VerticalAlignment="Center" Width="250" Text="But there was much more coast and wildlife to see, so I hiked for hours." TextWrapping="Wrap" />
  </StackPanel>
  <StackPanel Orientation="Horizontal">
   <Image x:Name="Image3" Opacity="0.5" Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/088/789378275_maquH-Tiny.jpg"/>
   <TextBlock x:Name="Caption3" Opacity="0.5" VerticalAlignment="Center" Width="250" Text="Then I relaxed a bit." TextWrapping="Wrap" />
  </StackPanel>
 </StackPanel>
</Grid>

Then, I create storyboards for each:

<UserControl.Resources>
  <Storyboard x:Name="ImageRollover1">
   <DoubleAnimation Storyboard.TargetName="Image1" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption1" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollout1">
   <DoubleAnimation Storyboard.TargetName="Image1" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption1" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollover2">
   <DoubleAnimation Storyboard.TargetName="Image2" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption2" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollout2">
   <DoubleAnimation Storyboard.TargetName="Image2" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption2" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollover3">
   <DoubleAnimation Storyboard.TargetName="Image3" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption3" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollout3">
   <DoubleAnimation Storyboard.TargetName="Image3" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption3" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
  </Storyboard>
 </UserControl.Resources>

And as a last step I wire the MouseEnter and MouseLeave events in the codebehind:
public MainPage()
  {
   InitializeComponent();

   Image1.MouseEnter += Image1_MouseEnter;
   Image1.MouseLeave += Image1_MouseLeave;

   Image2.MouseEnter += Image2_MouseEnter;
   Image2.MouseLeave += Image2_MouseLeave;

   Image3.MouseEnter += Image3_MouseEnter;
   Image3.MouseLeave += Image3_MouseLeave;
  }

  private void Image1_MouseEnter(object sender, MouseEventArgs e)
  {
   ImageRollover1.Begin();
  }
  private void Image1_MouseLeave(object sender, MouseEventArgs e)
  {
   ImageRollout1.Begin();
  }

  private void Image2_MouseEnter(object sender, MouseEventArgs e)
  {
   ImageRollover2.Begin();
  }
  private void Image2_MouseLeave(object sender, MouseEventArgs e)
  {
   ImageRollout2.Begin();
  }

  private void Image3_MouseEnter(object sender, MouseEventArgs e)
  {
   ImageRollover3.Begin();
  }
  private void Image3_MouseLeave(object sender, MouseEventArgs e)
  {
   ImageRollout3.Begin();
  }

And here's the result:

Get Microsoft Silverlight

These pictures came from a trip to California last year to attend an SEO course by Bruce Clay (but I also made a vacation out of it).

Anyway, as you can see it does work. However, there are a few problems:
  1. The mouseover effect only works over the image.
  2. There's a lot of redundant code here. All of those storyboards are identical except for their TargetNames.
  3. With all that code, the images are still not clickable.


Example 2

So let's get a little more sophisticated with our approach. There's a built-in control that can help us. It's the HyperlinkButton.

Go ahead and add a HyperlinkButton to the page. And then right-click on it and click "Edit Template..." > "Edit a copy..."

(To do this you'll need Expression Blend. If you don't have it, don't worry, I'll still post the resulting XAML below.)

After doing that and removing and changing a few things, I end up with this:
<UserControl.Resources>             
        <Style x:Key="HyperlinkButtonRollover" TargetType="HyperlinkButton">
         <Setter Property="Foreground" Value="#FFF"/>
         <Setter Property="Padding" Value="0"/>
         <Setter Property="Cursor" Value="Hand"/>
         <Setter Property="Background" Value="Transparent"/>
         <Setter Property="Template">
          <Setter.Value>
           <ControlTemplate TargetType="HyperlinkButton">
            <Grid Background="{TemplateBinding Background}" Cursor="{TemplateBinding Cursor}">
             <VisualStateManager.VisualStateGroups>
              <VisualStateGroup x:Name="CommonStates">
               <VisualState x:Name="Normal">
                <Storyboard>
                 <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
                </Storyboard>
               </VisualState>
               <VisualState x:Name="MouseOver">
                <Storyboard>
                 <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
                </Storyboard>
               </VisualState>
              </VisualStateGroup>
             </VisualStateManager.VisualStateGroups>
             <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
            </Grid>
           </ControlTemplate>
          </Setter.Value>
         </Setter>
        </Style>
    </UserControl.Resources>

One of the cool things about this approach is the VisualStates built into the HyperlinkButton. By using the "Normal" and "MouseOver" states, I can actually get rid of all that code we had added earlier in the codebehind.

And then the XAML for the images and captions looks like this:
<Grid x:Name="LayoutRoot" Background="Black">
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10">
         <HyperlinkButton Style="{StaticResource HyperlinkButtonRollover}" NavigateUri="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/011/788697398_xPrbE-L.jpg" TargetName="_blank">
          <HyperlinkButton.Content>
           <StackPanel Orientation="Horizontal">
                  <Image Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/011/788697398_xPrbE-Tiny.jpg"/>
                  <TextBlock VerticalAlignment="Center" Width="250" Text="I arrived on Santa Cruz Island and after just a few minutes I stumbled across this view." TextWrapping="Wrap" />
              </StackPanel>
    </HyperlinkButton.Content>
         </HyperlinkButton>
   <HyperlinkButton Style="{StaticResource HyperlinkButtonRollover}" NavigateUri="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/136/789417908_3bjhd-L.jpg" TargetName="_blank">
          <HyperlinkButton.Content>
           <StackPanel Orientation="Horizontal">
                  <Image Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/136/789417908_3bjhd-Tiny.jpg"/>
                  <TextBlock VerticalAlignment="Center" Width="250" Text="But there was much more coast and wildlife to see, so I hiked for hours." TextWrapping="Wrap" />
              </StackPanel>
    </HyperlinkButton.Content>
         </HyperlinkButton>  
   <HyperlinkButton Style="{StaticResource HyperlinkButtonRollover}" NavigateUri="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/088/789378275_maquH-L.jpg" TargetName="_blank">
          <HyperlinkButton.Content>
           <StackPanel Orientation="Horizontal">
                  <Image Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/088/789378275_maquH-Tiny.jpg"/>
                  <TextBlock VerticalAlignment="Center" Width="250" Text="Then I relaxed a bit." TextWrapping="Wrap" />
              </StackPanel>
    </HyperlinkButton.Content>
         </HyperlinkButton>  
        </StackPanel>
    </Grid>

Get Microsoft Silverlight

With those modifications the XAML is looking a little neater and the app is more or less doing what I want.

The remaining problems are those of design and usability:
  1. If you mouseover every image very quickly you'll notice that they kind of flash abruptly. This is due to an oversight in our storyboards which we'll fix in the next example.
  2. The animations have a long duration. They are currently set at 0.75 seconds. That doesn't sound like a long time, but it feels like it when you're actually using the app. Slow transitions like this are fine in a loading screen, where the user can't do anything but wait. But when they're actually using your application, a slow animation can give the impression that your app is slow and unresponsive. The user may even feel like they must let the animation play in its entirety before clicking.
  3. The animation feels a bit robotic and unnatural.


Example 3

First we'll address the abrupt flashing we were experiencing. The problem occurs when you mouse over and then off of an element before its mouseover animation is done playing.

The problem is incredibly easy to solve, although the solution may not be obvious at first. All you have to do is remove the "From" attribute from the storyboards...

The "From" was causing the MouseOver and Normal states to always start at a specified Opacity. In reality, we simply don't care at what Opacity the animation starts. We only care that it ends.

Next I'll change the duration of the storyboards to 0.3 seconds for the "MouseOver" state and 0.5 seconds for the "Normal" state.

And last I'm going to specify some easing. Learning how to use easing makes the difference between robotic/abrupt feeling transitions, and a smooth user interface. I'm going to work with one of the simpler EasingFunctions, the CircleEase. And I'll use the EaseOut mode. By doing this, the majority of the animation occurs up front and then settles gradually. It's a subtle but noticeable difference.

The resulting changes were all in the Storyboards:
<UserControl.Resources>
        <Style x:Key="HyperlinkButtonRollover" TargetType="HyperlinkButton">
            <Setter Property="Foreground" Value="#FFF"/>
            <Setter Property="Padding" Value="1"/>
            <Setter Property="Cursor" Value="Hand"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="HyperlinkButton">
                        <Grid Background="{TemplateBinding Background}" Cursor="{TemplateBinding Cursor}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" To="0.5" Duration="00:00:00.500000">
                                             <DoubleAnimation.EasingFunction>
                                              <CircleEase EasingMode="EaseOut" />
            </DoubleAnimation.EasingFunction>
                                            </DoubleAnimation>          
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="MouseOver">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" To="1" Duration="00:00:00.300000">
                                             <DoubleAnimation.EasingFunction>
                                              <CircleEase EasingMode="EaseOut" />
            </DoubleAnimation.EasingFunction>
                                            </DoubleAnimation>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

And the final app is here...
Get Microsoft Silverlight


I've packaged all of the projects above into one zip file...
Download MasteringStoryboards.zip



Bonus: Building it all into a Control

I thought it might be cool to animate the image and the caption separately, as if the image is sliding over and bumping into the caption. To do this while maintaining everything the HyperlinkButton gives us I've created a new custom control called the Captioned Image Hyperlink.

1 comment:

  1. I really enjoyed it reading. And it also cleared lot of my doubts about Storyboard in silverlight. Check this link too its having interview question and answer on Silverlight.
    http://mindstick.com/Interviewer/QuestionPage.aspx?topicid=7&topic=Silverlight

    ReplyDelete