A while back I created a pair of Storyboards that make a pair of panels swivel to show a front side and a back side. The key ingredients in the storyboards are:

  • Plane Projection using Perspective 3D
  • Easing
  • Visibility (Opacity can be used instead if you prefer)

Easing and 3D features are new to Silverlight 3 but really give it a nice effect. The easing makes it start slowly, then appear faster at the 90 degree turn, and then ease out.

Storyboard

The storyboards are not that involved, in fact here is the code for the storyboard that swivels the panels to show the backside:

<Storyboard x:Name="SwivelToBackStoryboard">
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelBack"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
<DiscreteObjectKeyFrame KeyTime="00:00:00.5000000">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelFront"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00.0000000">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
<DiscreteObjectKeyFrame KeyTime="00:00:00.5000000">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelFront"
Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)">
<</s pan>EasingDoubleKeyFrame KeyTime="00:00:00.0000000" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:00.5000000" Value="-90">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:01" Value="-180">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="SwivelPanelBack"
Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)">
<EasingDoubleKeyFrame KeyTime="00:00:00.0000000" Value="180">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:00.5000000" Value="90">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="00:00:01" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>

The front-to-back storyboard is pretty straightforward. I animate the visibility from collapsed to visible for the panels at the 90 degree turn. I use keyframes over a period of 1 second to animate the plane projection from 0 to -180 degrees for the front panel and from 180 to 0 for the back panel. The key here is that whatever panel is at 0 degrees will have clear text when it is at rest at 0 degrees. I found that if I animate them and have the front panel be at 360 degrees or even 180 degrees (if it starts at an offset), the text looks fuzzy. So this swivel storyboard works nicely for text.

You can see try this storyboard by downloading the code here.

Behavior

So the immediate problem I have with this solution is that I want to reuse this storyboard in an application. I certainly do not want to copy and paste it everywhere. This is where behaviors and triggers make a big difference. Behaviors are one of the most exciting features in Silverlight 3 and Blend 3 to me.

The goal is to make it as simple as possible to implement the swivel effect. So I rewrote the storyboards for the front to back to front swivels of the panels in .NET code inside of a behavior. This admittedly took a bit as I had to do something in .NET code that is considerably easier and faster to create in Blend. Download the code and you will see the difference between the first example, which uses storyboards in XAML, and the second example which uses .NET code in a behavior. It is not difficult by any means … but in Blend it took me 5 minutes to create the storyboard I wanted. It took me about an hour to convert and test the .NET code.

Below you can see the code for the Swivel behavior class itself. The complete code can be downloaded here.

public class Swivel : TriggerAction<FrameworkElement>
{
    #region Front element
 
    public static readonly DependencyProperty FrontElementNameProperty =
        DependencyProperty.Register("FrontElementName", typeof(string),
                                    typeof(Swivel), new PropertyMetadata(null));
 
    [Category("Swivel Properties")]
    public string FrontElementName { get; set; }
 
    #endregion
 
    #region Back element
 
    public static readonly DependencyProperty BackElementNameProperty =
        DependencyProperty.Register("BackElementName", typeof(string),
                                    typeof(Swivel), new PropertyMetadata(null));
 
    [Category("Swivel Properties")]
    public string BackElementName { get; set; }
 
    #endregion
 
    #region Duration
 
    public static readonly DependencyProperty DurationProperty =
        DependencyProperty.Register("Duration", typeof(Duration),
                                    typeof(Swivel), new PropertyMetadata(null));
 
    [Category("Animation Properties")]
    public Duration Duration { get; set; }
 
    #endregion
 
    #region Rotation Direction
 
    public static readonly DependencyProperty RotationProperty =
        DependencyProperty.Register("Rotation", typeof(RotationDirection),
                                    typeof(Swivel), new PropertyMetadata(RotationDirection.LeftToRight));
 
    [Category("Animation Properties")]
    public RotationDirection Rotation { get; set; }
 
    #endregion
 
    private readonly Storyboard frontToBackStoryboard = new Storyboard();
    private readonly Storyboard backToFrontStoryboard = new Storyboard();
    private bool forward = true;
 
    protected override void Invoke(object parameter)
    {
        if (AssociatedObject == null) return;
 
        FrameworkElement parent = AssociatedObject; // as FrameworkElement;
        UIElement front = null;
        UIElement back = null;
 
        front = parent.FindName(FrontElementName) as UIElement;
        back = parent.FindName(BackElementName) as UIElement;
        if (front == null || back == null) return;
 
        if (front.Projection == null || back.Projection == null)
        {
            front.Projection = new PlaneProjection();
            front.RenderTransformOrigin = new Point(.5, .5);
            front.Visibility = Visibility.Visible;
 
            back.Projection = new PlaneProjection { CenterOfRotationY = .5, RotationY = 180.0 }; //, CenterOfRotationZ = this.CenterOfRotationZ };
            back.RenderTransformOrigin = new Point(.5, .5);
            back.Visibility = Visibility.Collapsed;
 
            RotationData showBackRotation = null;
            RotationData hideFrontRotation = null;
            RotationData showFrontRotation = null;
            RotationData hideBackRotation = null;
 
            var frontPP = new PlaneProjection(); // { CenterOfRotationZ = this.CenterOfRotationZ };
            var backPP = new PlaneProjection(); // { CenterOfRotationZ = this.CenterOfRotationZ };
 
            switch (Rotation)
            {
                case RotationDirection.LeftToRight:
                    backPP.CenterOfRotationY = frontPP.CenterOfRotationY = 0.5;
                    showBackRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    showFrontRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    break;
                case RotationDirection.RightToLeft:
                    backPP.CenterOfRotationY = frontPP.CenterOfRotationY = 0.5;
                    showBackRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    showFrontRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationY", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationY", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    break;
                case RotationDirection.BottomToTop:
                    backPP.CenterOfRotationX = frontPP.CenterOfRotationX = 0.5;
                    showBackRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    showFrontRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    break;
                case RotationDirection.TopToBottom:
                    backPP.CenterOfRotationX = frontPP.CenterOfRotationX = 0.5;
                    showBackRotation = new RotationData { FromDegrees = -180.0, MidDegrees = -90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    hideFrontRotation = new RotationData { FromDegrees = 0.0, MidDegrees = 90.0, ToDegrees = 180.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    showFrontRotation = new RotationData { FromDegrees = 180.0, MidDegrees = 90.0, ToDegrees = 0.0, RotationProperty = "RotationX", PlaneProjection = frontPP, AnimationDuration = this.Duration };
                    hideBackRotation = new RotationData { FromDegrees = 0.0, MidDegrees = -90.0, ToDegrees = -180.0, RotationProperty = "RotationX", PlaneProjection = backPP, AnimationDuration = this.Duration };
                    break;
            }
 
            front.RenderTransformOrigin = new Point(.5, .5);
            back.RenderTransformOrigin = new Point(.5, .5);
 
            front.Projection = frontPP;
            back.Projection = backPP;
 
            frontToBackStoryboard.Duration = this.Duration;
            backToFrontStoryboard.Duration = this.Duration;
 
            // Rotation
            frontToBackStoryboard.Children.Add(CreateRotationAnimation(showBackRotation));
            frontToBackStoryboard.Children.Add(CreateRotationAnimation(hideFrontRotation));
            backToFrontStoryboard.Children.Add(CreateRotationAnimation(hideBackRotation));
            backToFrontStoryboard.Children.Add(CreateRotationAnimation(showFrontRotation));
 
 
            // Visibility
            frontToBackStoryboard.Children.Add(CreateVisibilityAnimation(showBackRotation.AnimationDuration, front, false));
            frontToBackStoryboard.Children.Add(CreateVisibilityAnimation(hideFrontRotation.AnimationDuration, back, true));
            backToFrontStoryboard.Children.Add(CreateVisibilityAnimation(hideBackRotation.AnimationDuration, front, true));
            backToFrontStoryboard.Children.Add(CreateVisibilityAnimation(showFrontRotation.AnimationDuration, back, false));
        }
 
        if (forward)
        {
            frontToBackStoryboard.Begin();
            forward = false;
        }
        else
        {
            backToFrontStoryboard.Begin();
            forward = true;
        }
    }
 
    private static ObjectAnimationUsingKeyFrames CreateVisibilityAnimation(Duration duration, DependencyObject element, bool show)
    {
        var animation = new ObjectAnimationUsingKeyFrames();
        animation.BeginTime = new TimeSpan(0);
        animation.KeyFrames.Add(new DiscreteObjectKeyFrame { KeyTime = new TimeSpan(0), Value = (show ? Visibility.Collapsed : Visibility.Visible) });
        animation.KeyFrames.Add(new DiscreteObjectKeyFrame { KeyTime = new TimeSpan(duration.TimeSpan.Ticks / 2), Value = (show ? Visibility.Visible : Visibility.Collapsed) });
        Storyboard.SetTargetProperty(animation, new PropertyPath("Visibility"));
        Storyboard.SetTarget(animation, element);
        return animation;
    }
 
 
    private static DoubleAnimationUsingKeyFrames CreateRotationAnimation(RotationData rd)
    {
        var animation = new DoubleAnimationUsingKeyFrames();
        animation.BeginTime = new TimeSpan(0);
        animation.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = new TimeSpan(0), Value = rd.FromDegrees, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseIn } });
        animation.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = new TimeSpan(rd.AnimationDuration.TimeSpan.Ticks / 2), Value = rd.MidDegrees });
        animation.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = new TimeSpan(rd.AnimationDuration.TimeSpan.Ticks), Value = rd.ToDegrees, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut } });
        Storyboard.SetTargetProperty(animation, new PropertyPath(rd.RotationProperty)); 
        Storyboard.SetTarget(animation, rd.PlaneProjection);
        return animation;
    }
}

 

Once you create the behavior, it shows up in Blend in the Assets window, like this:

image

You can then drag the behavior onto whatever element you want to control the behavior. In my demo, I use the MouseLeftButtonUp event of the Grid, like this:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="MouseLeftButtonUp">
        <local:Swivel 
            Rotation="RightToLeft" 
            Duration="00:00:01" 
            BackElementName="SwivelPanelBack" 
            FrontElementName="SwivelPanelFront" />
    </i:EventTrigger>
</i:Interaction.Triggers>

 

 



UPDATE: I forgot to mention that I added a bit of code to this behavior based on a great behavior I saw from Joel Nuebeck. So I want to give him props for giving me the idea of making the 4 directional options so the developer can set the direction of the swivel. Originally I had forced the behavior to swivel in a specific direction. Thanks Joel!