To learn about the perspective tools available in Silverlight 3, a good place to start is my friend Corey Schuman's post.
I had a conversation with Jesse Liberty at MIX09 this past week, and we talked about different techniques for animating various objects. Out of that conversation, I wanted to explore how to go about emulating a 3D object with the new Perspective Tools in Blend/Silverlight 3.
I spent some time putting together an application that simulates a 3D playing card rotation. If you have Silverlight 3 installed, you can see that application here. The project files are available at the end of this post.
The basic functionality for the project involves two images - a "card front" image, which in this case was the Jack of Clubs, and a "card back" image, which is a standard blue backed deck. The two images are aligned right on top of one another. The 3D perspective manipulations are enabled and bound to sliders as Corey describes in the article linked above. I added a bit of code in the code-behind file to "watch" for certain ranges and collapse the visibility of the "card back" image when the "card front" should be showing. There are two tricks involved with this.
First, the card face image is loaded and by default is scaled to -1X. This reverses the image. This is because the perspective tools actually flip around the image. If it was not scaled to -1X, the card front would actually show up backwards when it's rotated and made visible.
The second is the drop shadow bitmap effect I added. This one was a bit confusing - in Joe Stegman's session on the new features added to Silverlight 3, he made the point several times that bitmap effects were the last step before an object is written to the screen. If this were the case, the effects would not be affected by the perspective transforms, but they appear to be as the drop shadow flips along with the transform. At any rate, this was easy enough to solve. I grouped my card object into another canvas, and moved the effect off of the canvas containing the card images to the new container.
Let's take a look at some code. Following is the "CardEffectContainer" Canvas object, which is where the actual drop shadow effect is applied to the object. Inside of that is the "Jack_Clubs" Canvas object, which contains the Canvas.Projection used to make the card appear as though it is rotating. You can also see the "CardFront" and "CardBack" images. As I mentioned above, notice that the CardFront image is scaled -1X.
<Canvas x:Name="CardEffectContainer" Height="252" Width="185" Canvas.Left="300" Canvas.Top="110"> <Canvas.Effect> <DropShadowEffect Opacity="0.5" BlurRadius="8"/> </Canvas.Effect> <Canvas x:Name="Jack_Clubs" Height="252" Width="185" IsHitTestVisible="False"> <Canvas.Projection> <PlaneProjection x:Name="CardRotation" RotationX="0" RotationY="0" RotationZ="0"/> </Canvas.Projection> <Image x:Name="CardFront" Height="252" Width="185" Source="cardFront.png" RenderTransformOrigin="0.5,0.5"> <Image.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="-1"/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </Image.RenderTransform> </Image> <Image x:Name="CardBack" Height="252" Width="185" Canvas.Left="0" Source="cardBack.png"/> </Canvas> </Canvas>
After that, I have 3 Canvas object, each containing a Slider and associated TextBlock for display. Following is the listing for the SliderY control. Notice that the Value is using Binding to tie the Slider Control's Value to RotationY property in the "CardRotation" Canvas' PlaneProjection shown in the above listing. This makes it so I don't need to write any code in order to allow the Slider to affect the rotation value.
<Canvas x:Name="YRotationStuff" Height="37" Width="200" Canvas.Left="20" Canvas.Top="491"> <Slider x:Name="SliderY" Width="200" Value="{Binding ElementName=CardRotation, Mode=TwoWay, Path=RotationY}" Minimum="0" Maximum="360" Canvas.Top="19"/> <TextBlock x:Name="SliderYValue" Text="Y Rotation: 0.00" TextWrapping="Wrap" Foreground="#FFFFFFFF"/> </Canvas>
I'm not going to show the X and Z rotation sliders here in this post, but they look just like the Y Slider code shown above. In my code behind, I needed to watch the rotations in order to hide the card back at the appropriate times. This was done by setting up two event listeners as shown below.
SliderY.ValueChanged += new RoutedPropertyChangedEventHandler(Slider_ValueChanged); SliderX.ValueChanged += new RoutedPropertyChangedEventHandler(Slider_ValueChanged);
Notice that both listeners call the same event handler - Slider_ValueChanged - which is shown following. This event handler starts out by updating the text visible to the user. I orignally had this property bound but wanted to do some formatting on the text, so pushed it to the code behind.
After that, you'll see a series of "if" statements that test ranges of values to determine the rotational state of the card. For example, the first statement checks to see if the card has been rotated along the X axis (tilting the card forward or backwards) more than 90 degrees, or less than 270 degrees.
That condition would result in the card front being visible (to visualize this, hold a playing card straight out in front of you and rotate it towards you - when you pass 90 degrees, you can see the card front, when you pass 270 degrees, you're viewing the card back again).
Since we're dealing with multiple axes here, we also need to check the Y rotation value. If the card has been rotated along the Y axis less than 90 degrees, or more than 270 degrees, then we know that the X and Y values are in a range where the card front should be visible, and we collapse the CardBack image from view. Easy, right?
You can see the other set of tests used to determine the card front visibility in the listing.
private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { SliderXValue.Text = "X Rotation: " + String.Format("{0:0.00}", SliderX.Value); SliderYValue.Text = "Y Rotation: " + String.Format("{0:0.00}", SliderY.Value); if (SliderX.Value > 90 && SliderX.Value < 270 && (SliderY.Value < 90 || SliderY.Value > 270)) { CardBack.Visibility = Visibility.Collapsed; } else if (SliderX.Value > 90 && SliderX.Value < 270 && (SliderY.Value > 90 && SliderY.Value < 270)) { CardBack.Visibility = Visibility.Visible; } else if ((SliderX.Value < 90 || SliderX.Value > 270) && (SliderY.Value < 90 || SliderY.Value > 270)) { CardBack.Visibility = Visibility.Visible; } else if ((SliderX.Value < 90 || SliderX.Value > 270) && (SliderY.Value > 90 || SliderY.Value < 270)) { CardBack.Visibility = Visibility.Collapsed; } }
That handles X and Y rotations, but what about Z? The Z-axis runs directly into your screen and would spin the card like a propeller (assuming the transform origin were in the center as it is here). Since the visibility of the card front is handled when the X and Y sliders are manipulated, all we need to do is update the feedback we're giving the user when the Z-slider is manipulated (remember that the value is bound through the XAML, so no code is necessary to update the actual Z rotation).
This is done with a simple event listener:
SliderZ.ValueChanged += new RoutedPropertyChangedEventHandler(SliderZ_ValueChanged);
and its associated handler, shown here:
private void SliderZ_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { SliderZValue.Text = "Z Rotation: " + String.Format("{0:0.00}", SliderZ.Value); }
From that, you get a fairly convincing 3D object rotation that was created from just a couple of images. Of course, a playing card is flat, so it's easy to manipulate in this manner. If you have all of the Silverlight 3 bits installed and would like the grab the project file, it's here.






Entries (RSS)