The Pick a Card Magic Trick app enables you to amaze your friends with a slick magic trick that’s likely to keep them guessing how it’s done even after multiple performances. In this trick, you ask someone in your audience to name any card while the deck of cards is shuffling. After he or she names a card, you press “tap here when ready” to stop the shuffling, shown in Figure 17.1. You then tap the screen to flip over the card, and the card they just named is shown! You can tap the card again to start over.
The magician’s secret is not revealed in this book; you’ll have to run the app (or look at the full source code) that comes with this book in order to see how it is done! (My wife still can’t figure it out; she thinks the app is using speech recognition in order to know what card to show.) What is shown is the main lesson of this chapter—using 3D transforms to flip the playing cards.
3D Transforms
Unlike XNA, Silverlight does not provide a full 3D graphics engine. However, Silverlight enables you to perform the most common 3D effects with perspective transforms. These transforms escape the limitations of the 2D transforms by enabling you to rotate and translate an element in any or all of the three dimensions.
Perspective transforms are normally done with a class called PlaneProjection, which defines RotationX, RotationY, and RotationZ properties. The X and Y dimensions are defined as usual, and the Z dimension extends into and out of the screen, as illustrated in Figure 17.2. X increases from left-to-right, Y increases from topto- bottom, and Z increases from backto- front.
Although plane projections act like render transforms, they are not assigned to an element via the RenderTransform property, but rather a separate property called Projection. The following plane projections are marked on playing card images, producing the result in Figure 17.3:
[code]
<phone:PhoneApplicationPage …>
<StackPanel Orientation=”Horizontal”>
<Image Source=”Images/CardHA.png” Width=”150” Margin=”12”>
<Image.Projection>
<PlaneProjection RotationX=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH2.png” Width=”150”>
<Image.Projection>
<PlaneProjection RotationY=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH3.png” Width=”150” Margin=”36”>
<Image.Projection>
<PlaneProjection RotationZ=”55”/>
</Image.Projection>
</Image>
<Image Source=”Images/CardH4.png” Width=”150” Margin=”48”>
<Image.Projection>
<PlaneProjection RotationX=”30” RotationY=”30” RotationZ=”30”/>
</Image.Projection>
</Image>
</StackPanel>
</phone:PhoneApplicationPage>
[/code]
Notice that rotating around only the Z axis is like using a 2D RotateTransform, although the direction is reversed.
Although having permanently rotated elements might be interesting for some apps, normally plane projections are used as the target of an animation. Pick a Card leverages a plane projection for its card-flip animation, as well as its card-shuffling animation. Figure 17.4 demonstrates the 3D card flip. After the card back is rotated 90° (to be perpendicular to the screen and therefore temporarily invisible), the image is hidden to reveal the 9 of diamonds card front for the remaining 90° of the animation.
Much like the 2D transform classes, PlaneProjection defines additional properties for changing the center of rotation: CenterOfRotationX, CenterOfRotationY, and CenterOfRotationZ. The first two properties are relative to the size of the element, on a scale from 0 to 1. The CenterOfRotationZ property is always in terms of absolute pixels, as elements never have any size in the Z dimension to enable a relative specification. Pick a Card leverages CenterOfRotationX in its shuffle animation to make cards flip in from either the left edge of the screen or the right edge of the screen, as demonstrated in Figure 17.5 for the following XAML:
[code]
<phone:PhoneApplicationPage …>
<Grid>
<!– The card on the left –>
<Image Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection RotationY=”62” CenterOfRotationX=”0”/>
</Image.Projection>
</Image>
<!– The card on the right –>
<Image Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection RotationY=”-62” CenterOfRotationX=”1”/>
</Image.Projection>
</Image>
</Grid>
</phone:PhoneApplicationPage>
[/code]
PlaneProjection defines six properties for translating an element in any or all dimensions. GlobalOffsetX, GlobalOffsetY, and GlobalOffsetZ apply the translation after the rotation, so the offsets are relative to the global screen coordinates. LocalOffsetX, LocalOffsetY, and LocalOffsetZ apply the translation before the rotation, causing the rotation to be relative to the rotated coordinate space.
The Main Page
Pick a Card’s main page doesn’t do much; it’s a main menu that has two modes—an initial one for teaching you how to use the app, and one that hides the secrets once you have learned how to perform the trick. Both modes are shown in Figure 17.6.
The initial main menu has three buttons: one for instructions, one for performing the trick in a special practice mode, and one for the standard about page. Once the instructions have self-destructed (which can be done by tapping a button on the instructions page), the main menu has a button for performing the trick in its normal mode, and the same about button.
The User Interface
Listing 17.1 contains the XAML for the main page.
LISTING 17.1 MainPage.xaml—The User Interface for Pick a Card’s Main Page
[code]
<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”5*”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<Rectangle Fill=”{StaticResource PhoneForegroundBrush}” Margin=”0,0,0,40”
Width=”158” Height=”200” VerticalAlignment=”Bottom”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/logo.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Text=”pick a card” Margin=”21,16,0,0”
Style=”{StaticResource PhoneTextTitle1Style}”/>
<Button x:Name=”InstructionsButton” Grid.Row=”1”
Content=”instructions for the new magician” Height=”100”
local:Tilt.IsEnabled=”True” Click=”InstructionsButton_Click”/>
<Button x:Name=”BeginButton” Grid.Row=”2”
Content=”practice (FOR YOUR EYES ONLY!)” Height=”100”
local:Tilt.IsEnabled=”True” Click=”BeginButton_Click”/>
<Button Content=”about” Grid.Row=”3” Height=”100” Click=”AboutButton_Click”
local:Tilt.IsEnabled=”True”/>
</Grid>
</phone:PhoneApplicationPage>
[/code]
Notes:
- The page is set up for its initial mode. Code-behind transforms it to the other mode.
- Rather than using an Image element to display the logo, Listing 17.1 uses the logo.png file as an opacity mask for a rectangle filled with the phone theme foreground color. This is done to enable the otherwise-white image to appear black under the light theme, as shown in Figure 17.7.
Opacity masks are often harmful for performance!
Using an opacity mask with an image brush is a neat trick for enabling nonvector content to respect the phone’s current theme.However, be aware that their use can severely hamper the performance of your app, especially when animations are involved.Opacity masks cause animations to be rasterized on the UI thread, even if they otherwise would have been able to completely run on the compositor thread.Therefore, use extreme caution when applying an opacity mask.You can check whether it is impacting your app by examining the frame rate counter with and without the opacity mask applied.
The Code-Behind
Listing 17.2 contains the code-behind for the main page, which consists of straightforward Click event handlers for each button and code to morph the main menu after the instructions have been hidden. This is based off of a single setting defined in Settings.cs:
[code]
public static class Settings
{
public static readonly Setting<bool> PracticeMode =
new Setting<bool>(“PracticeMode”, true);
}
[/code]
LISTING 17.2 MainPage.xaml.cs—The Code-Behind for Pick a Card’s Main Page
[ocde]
using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (!Settings.PracticeMode.Value)
{
BeginButton.Content = “begin”;
InstructionsButton.Visibility = Visibility.Collapsed;
}
}
void BeginButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/TrickPage.xaml”,
UriKind.Relative));
}
void InstructionsButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutButton_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Pick a Card”, UriKind.Relative));
}
}
}
[/code]
The Trick Page
The “trick page” is used for both phases of the trick—the shuffling and the final card reveal. This same page is used whether the trick is running in “practice mode” or for real.
The User Interface
Listing 17.3 contains the XAML for the trick page.
LISTING 17.3 TrickPage.xaml—The User Interface for Pick a Card’s Trick Page
[code]
<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.TrickPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– Prevent off-screen visuals from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<!– Add two storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The flip –>
<Storyboard x:Name=”FlipStoryboard”
Storyboard.TargetName=”ChosenCardProjection”
Storyboard.TargetProperty=”RotationY”
Completed=”FlipStoryboard_Completed”>
<DoubleAnimation By=”90” Duration=”0:0:.25”/>
</Storyboard>
<!– The shuffle, with separate left and right animations –>
<Storyboard x:Name=”ShuffleStoryboard”
Storyboard.TargetProperty=”RotationY”>
<DoubleAnimation Storyboard.TargetName=”NextCardLeftProjection” From=”120”
To=”0” Duration=”0:0:.2” RepeatBehavior=”Forever”
BeginTime=”0:0:.1”/>
<DoubleAnimation Storyboard.TargetName=”NextCardRightProjection”
From=”-120” To=”0” Duration=”0:0:.2”
RepeatBehavior=”Forever”/>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<Grid Background=”Black”>
<!– The card that flips over –>
<Grid>
<Grid.Projection>
<PlaneProjection x:Name=”ChosenCardProjection”/>
</Grid.Projection>
<Image x:Name=”CardFrontImage” RenderTransformOrigin=”.5,.5”>
<!– Reverse, so it looks correct when flipped over –>
<Image.RenderTransform>
<ScaleTransform ScaleX=”-1”/>
</Image.RenderTransform>
</Image>
<Image x:Name=”CardBackImage” Source=”Images/CardBack.png”/>
</Grid>
<!– More cards, for shuffling –>
<Image x:Name=”NextCardRightImage” Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection x:Name=”NextCardRightProjection” CenterOfRotationX=”1”/>
</Image.Projection>
</Image>
<Image x:Name=”NextCardLeftImage” Source=”Images/CardBack.png”>
<Image.Projection>
<PlaneProjection x:Name=”NextCardLeftProjection” CenterOfRotationX=”-1”/>
</Image.Projection>
</Image>
<!– The “tap here when ready” button and a translucent background–>
<Grid x:Name=”ReadyPanel” Background=”#7000”>
<Button Background=”{StaticResource PhoneBackgroundBrush}”
Content=”tap here when ready”
VerticalAlignment=”Center”/>
</Grid>
<!– Images for practice mode –>
<Image x:Name=”PracticeImage1” Visibility=”Collapsed”
Source=”Images/practice1.png”/>
<Image x:Name=”PracticeImage2” Visibility=”Collapsed”
Source=”Images/practice2.png”/>
</Grid>
</phone:PhoneApplicationPage>
[/code]
Notes:
- The grid containing the chosen card has a plane projection (ChosenCardProjection) that is animated by FlipStoryboard to perform the 3D flip. This grid contains the image for the card front (chosen by code-behind) and the image for the card back. The card front image is reversed (with a ScaleTransform) so it appears correctly once the grid is flipped around. The animation only rotates the card 90°, because at that point the card back needs to be hidden so the card front can be seen for the remaining 90°. This is handled by the FlipStoryboard_Completed method in codebehind.
- ShuffleStoryboard performs the shuffling by animating the plane projections on NextCardRightImage and NextCardLeftImage. These are given centers of rotation that make them flip from the outer edges of the screen, as seen back in Figure 17.1. The left image is given a center of –1 rather than 0 to give a more realistic, asymmetric effect.
The Code-Behind
Listing 17.4 contains the code-behind for the trick page, with 61 lines of code omitted that “magically” set the chosenSuit string to C, D, H, or S and the chosenRank string to A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, or K.
LISTING 17.4 TrickPage.xaml.cs—The Code-Behind for Pick a Card’s Trick Page
[code]
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class TrickPage : PhoneApplicationPage
{
string chosenSuit;
string chosenRank;
bool flipPart2;
bool finalPhase;
public TrickPage()
{
InitializeComponent();
this.AddHandler(Page.MouseLeftButtonUpEvent,
new MouseButtonEventHandler(MainPage_MouseLeftButtonUp),
true /* handledEventsToo, so we get the button click */);
InitializeTrick();
}
void InitializeTrick()
{
if (Settings.PracticeMode.Value)
this.PracticeImage1.Visibility = Visibility.Visible;
// Reset everything
this.ReadyPanel.Visibility = Visibility.Visible;
this.CardBackImage.Visibility = Visibility.Visible;
this.NextCardLeftImage.Visibility = Visibility.Visible;
this.NextCardRightImage.Visibility = Visibility.Visible;
this.CardFrontImage.Source = null;
this.flipPart2 = false;
this.ChosenCardProjection.RotationY = 0;
// Start shuffling
this.ShuffleStoryboard.Begin();
}
void MainPage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (this.ReadyPanel.Visibility == Visibility.Visible)
{
// This is a tap on the “tap here when ready” button
if (Settings.PracticeMode.Value)
{
this.PracticeImage1.Visibility = Visibility.Collapsed;
this.PracticeImage2.Visibility = Visibility.Visible;
}
// Hide ReadyPanel and the shuffling deck,
// leaving the single card back exposed
this.ReadyPanel.Visibility = Visibility.Collapsed;
this.NextCardLeftImage.Visibility = Visibility.Collapsed;
this.NextCardRightImage.Visibility = Visibility.Collapsed;
this.ShuffleStoryboard.Stop();
this.finalPhase = true;
}
else if (this.finalPhase)
{
// This is a tap on the card back to flip it over
if (Settings.PracticeMode.Value)
this.PracticeImage2.Visibility = Visibility.Collapsed;
// Show the chosen card image
this.CardFrontImage.Source = new BitmapImage(new Uri(“Images/Card” +
this.chosenSuit + this.chosenRank + “.png”, UriKind.Relative));
// Perform the first 90° of the flip
this.FlipStoryboard.Begin();
this.finalPhase = false;
}
else if (this.FlipStoryboard.GetCurrentState() != ClockState.Active)
{
// Do it again. (Don’t allow this until the flip animation is finished.)
InitializeTrick();
}
}
void FlipStoryboard_Completed(object sender, EventArgs e)
{
if (!this.flipPart2)
{
// The card is now perpendicular to the screen. It’s time to hide the
// back and run the animation again so the remaining 90° shows the front
this.CardBackImage.Visibility = Visibility.Collapsed;
this.flipPart2 = true;
this.FlipStoryboard.Begin();
}
}
#region Magician’s Secret
…
#endregion
}
}
[/code]
Notes:
- A single handler—MainPage_MouseLeftButtonUp—handles the first tap on the “tap here when ready” button, which can actually be anywhere on the screen, the tap on the card back to flip it over, and the tap on the card front to start the trick again. The handler is attached with true passed for handledEventsToo, so the event is received when the button is tapped.
- When the card back is tapped (indicated by finalPhase being true inside MainPage_MouseLeftButtonUp), the card front image is set to one of 52 images included in the project. These 52 images are shown in Figure 17.8.
- Inside FlipStoryboard_Completed, the card back is hidden and FlipStoryboad is run again to complete the 180° flip. This works because the animation is marked with By=”90”, so the first run takes it from 0° to 90°, and the second run takes it from 90° to 180°. The card back must be manually hidden because flipping elements over in 3D does not change their Z-order. In other words, unlike in the physical world, the card back remains on top of the card front regardless of the angle of rotation.
The Instructions Page
The instructions page, shown in Figure 17.9, contains a button that makes them selfdestruct (turning off practice mode and changing the main menu). Once this is done, the instructions never come back unless the app is uninstalled and reinstalled. This is done to prevent nosy audience members from figuring out the secret to the trick. The XAML for this page isn’t very interesting (other than the fact that its text reveals the secret to the trick), but Listing 17.5 shows the code-behind, which implements the self-destructing behavior.
LISTING 17.5 InstructionsPage.xaml.cs—The Code-Behind for Pick a Card’s Instructions Page
[code]
using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class InstructionsPage : PhoneApplicationPage
{
public InstructionsPage()
{
InitializeComponent();
}
void SelfDestructButton_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(“To protect the secret of this trick, these “ +
“instructions will disappear forever once you turn off practice mode.” +
“ The only way to get them back is to uninstall then reinstall this “ +
“app. Are you ready to destroy the instructions?”,
“These instructions will self-destruct!”, MessageBoxButton.OKCancel)
== MessageBoxResult.OK)
{
Settings.PracticeMode.Value = false;
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}
}
[/code]
To make the instructions self-destruct, Listing 17.5 changes the PracticeMode persisted setting to false and then navigates back to the main page which hides the instructions button. This setting never changes unless the app is uninstalled because it doesn’t provide the user any way to change it back. Because uninstalling an app removes anything it puts in isolated storage, however, reinstalling it restores PracticeMode’s default value of true.
To implement a behavior that only happens the first time an app is run (or until the user makes some action to turn it off ), simply base it on a value persisted in isolated storage.The Setting object used throughout this book internally uses isolated storage to preserve each value until it is either changed by code or deleted by the app being uninstalled.
The Finished Product