The types of transforms you set with RenderTransform are all examples of two-dimensional affine transforms. Affine
transforms are very well behaved and just a little dull: Straight lines
are always transformed to straight lines, ellipses are always
transformed to ellipses, and squares are always transformed to
parallelograms. Two lines that are parallel before the transform are
still parallel after the transform.
Silverlight 3 introduced a new UIElement property named Projection that allows setting non-affine transforms on graphical objects, text, controls, and media. Non-affine transforms do not preserve parallelism.
The type of non-affine
transform allowed in Silverlight 3 is still represented by a matrix
multiplication, and it still has restrictions on what it can do.
Straight lines are always transformed to straight lines, and a square is
always transformed into a simple convex quadrilateral. By
“quadrilateral” I mean a four-sided figure (also called a tetragon or
quadrangle); by “simple” I mean that the sides don’t intersect except at
their vertices; by “convex” I mean that the internal angles at each
vertex are less than 180 degrees.
This type of non-affine transform is very useful for creating taper
transforms, where opposite sides of a square or rectangle taper
somewhat in one direction. Objects appear to be somewhat three
dimensional because part of the object seems further away from our
eyes—an effect called a perspective projection.
In a sense, the Projection
property gives Silverlight a little bit of “pseudo 3D.” It’s not a real
3D system because there’s no way to define objects in 3D space, no
concept of cameras, lights, or shading, and—perhaps most crucially—no
clipping of objects based on their arrangement in 3D space.
Still, working with the Projection
transform requires the programmer to begin thinking about three
dimensions and especially about 3D rotation. Fortunately, the developers
of Silverlight have made common and simple use of the Projection property fairly easy.
You can set this Projection property to one of two objects: You can be mathematical and flexible by using Matrix3DProjection, or you can do as I’ll do here and take the easy way out with PlaneProjection. Although PlaneProjection defines twelve settable properties, you can pretty much limit yourself to six of them.
The three crucial properties of PlaneProjection are RotationX, RotationY, and RotationX, which you can set to angle values to cause rotation around
the X axis (which extends in a positive direction from left to right),
the Y axis (which extends from top to bottom), and the Z axis (which
comes out of the screen towards the viewer).
You can anticipate the direction
of rotation using the right-hand rule: Point your thumb in the
direction of the positive axis. (For X, that’s to the right, for Y it’s
down, for Z, it’s toward you.) The curve that your other fingers make
indicates the direction of rotation for positive rotation angles.
Negative angles rotate in the opposite direction.
A composite rotation depends on the order in which the individual rotations are applied. When you use PlaneProjection, you are sacrificing some flexibility in these rotations. PlaneProjection always applies RotationX first, then RotationY, and finally RotationZ, but in many cases you only need set one of these properties. As with RenderTransform, Projection doesn’t affect layout. The layout system always sees an untransformed and unprojected element.
RotationX, RotationY, and RotationZ are all backed by dependency properties, so they can all be animation targets, as demonstrated by the PerspectiveRotation program. The content area contains a TextBlock with a PlaneProjection object set to its Projection property, and three buttons:
Example 1. Silverlight Project: PerspectiveRotation File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions>
<TextBlock Name="txtblk" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Text="ROTATE" FontSize="{StaticResource PhoneFontSizeHuge}" Foreground="{StaticResource PhoneAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock.Projection> <PlaneProjection x:Name="planeProjection" /> </TextBlock.Projection> </TextBlock>
<Button Grid.Row="1" Grid.Column="0" Content="Rotate X" Click="RotateXClick" />
<Button Grid.Row="1" Grid.Column="1" Content="Rotate Y" Click="RotateYClick" />
<Button Grid.Row="1" Grid.Column="2" Content="Rotate Z" Click="RotateZClick" /> </Grid>
|
Three storyboards defined in the Resources collection are defined to animate the RotationX, RotationY, and RotationZ properties of the PlaneProjection object:
Example 2. Silverlight Project: PerspectiveRotation File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.Resources> <Storyboard x:Name="rotateX"> <DoubleAnimation Storyboard.TargetName="planeProjection" Storyboard.TargetProperty="RotationX" From="0" To="360" Duration="0:0:5" /> </Storyboard>
<Storyboard x:Name="rotateY"> <DoubleAnimation Storyboard.TargetName="planeProjection" Storyboard.TargetProperty="RotationY" From="0" To="360" Duration="0:0:5" /> </Storyboard>
<Storyboard x:Name="rotateZ"> <DoubleAnimation Storyboard.TargetName="planeProjection" Storyboard.TargetProperty="RotationZ" From="0" To="360" Duration="0:0:5" /> </Storyboard> </phone:PhoneApplicationPage.Resources>
|
The buttons simply start the corresponding storyboards:
Example 3. Silverlight Project: PerspectiveRotation File: MainPage.xaml.cs (excerpt)
void RotateXClick(object sender, RoutedEventArgs args) { rotateX.Begin(); }
void RotateYClick(object sender, RoutedEventArgs args) { rotateY.Begin(); }
void RotateZClick(object sender, RoutedEventArgs args) { rotateZ.Begin(); }
|
Here’s rotation around the Y axis:
The animations are slow
enough that you can click multiple buttons and see the interactions. It
almost looks as if the text is tumbling through the weightlessness of
space.
In 2D space, rotation is relative
to a point; in 3D space, rotation is relative to a line, commonly
referred to as an “axis of rotation.” But the PlaneProjection class prefers to treat this center of rotation using three numbers—the properties CenterOfRotationX, CenterOfRotationY, and CenterOfRotationZ. In effect, these three numbers define a 3D point that remains unchanged during rotation. CenterOfRotationX does not affect rotation around the X axis, and similarly for the other two properties.
The CenterOfRotationX and CenterOfRotationY
properties are relative coordinates based on the size of the element
being rotated, where (0, 0) is the upper-left corner. The default values
are 0.5, indicating the center of the element.
If you set CenterOfRotationX to 0, the RotationY property causes the element to rotate around its left side. If CenterOfRotationY is set to 1, then the RotationX property causes the element to be rotated around its bottom.
The CenterOfRotationZ
property is in absolute coordinates—pixels, in other words—where 0 is
the plane of the screen and positive coordinates come out of the screen
toward the user. For purposes of its internal calculations, the viewer
(you) is assumed to be 1000 pixels in front of the screen. In
PerspectiveRotation, try setting the CenterOfRotationZ property of PlaneProjection to 200:
<TextBlock.Projection>
<PlaneProjection x:Name="planeProjection"
CenterOfRotationZ="200" />
</TextBlock.Projection>
Now try the “Rotate X” and
“RotateY” buttons: You’ll see the text sweep around as if it’s leaving
the screen (where the Z coordinate is 0) and circling around a Z value
of 200, curving in front of the viewer at a Z value of 400. A CenterOfRotationZ
value greater than 500 will cause projections to stop working right.
The projected object will get a Z value of 1000 and strike the viewer
right on the nose.
The other properties of PlaneProjection cause translation in the X, Y, and Z directions: Conceptually, the LocalOffsetX, LocalOffsetY, and LocalOffsetZ properties are applied first, then the element is rotated, then GlobalOffsetX, GlobalOffsetY, and GlobalOffsetZ properties are applied.
Try setting LocalOffsetX or GlobalOffsetX to 200. In either case, the unrotated text is moved to the right by 200 pixels. But GlobalOffsetX is more like the whole screen shifting right. Try setting LocalOffsetX
and rotate the text around the Y axis. The actual offset will begin at
the right, and shift to the left, and then back to the right.
You can use
animated projection transforms for small effects or for big effects. An
example of a big effect is to change the way a new page in your program comes into view. The SweepIntoView program has a MainPage.xaml file containing just a little text:
Example 4. Silverlight Project: SweepIntoView File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock Text="Touch to go to second page" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid>
|
The code-behind file uses touch to navigate to Page2.xaml:
Example 5. Silverlight Project: SweepIntoView File: MainPage.xaml.cs (excerpt)
protected override void OnManipulationStarted(ManipulationStartedEventArgs args) { this.NavigationService.Navigate(new Uri("/Page2.xaml", UriKind.Relative));
args.Complete(); args.Handled = true; base.OnManipulationStarted(args); }
|
For some variety (and to see more clearly what’s happening) Page2.xaml colors its content area with an accented background:
Example 6. Silverlight Project: SweepIntoView File: Page2.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" Background="{StaticResource PhoneAccentBrush}"> <TextBlock Text="Touch to go back" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid>
|
The code-behind file also has an OnManipulationStarted override:
Example 7. Silverlight Project: SweepIntoView File: Page2.xaml.cs (excerpt)
protected override void OnManipulationStarted(ManipulationStartedEventArgs args) { this.NavigationService.GoBack();
args.Complete(); args.Handled = true; base.OnManipulationStarted(args); }
|
But what makes this program different is some additional markup in the Page2.xaml file. This ensures that the page just doesn’t come on the stage in a sudden pop, but dramatically sweeps into view:
Example 8. Silverlight Project: SweepIntoView File: Page2.xaml (excerpt)
<phone:PhoneApplicationPage.Projection> <PlaneProjection x:Name="planeProjection" CenterOfRotationX="0" /> </phone:PhoneApplicationPage.Projection>
<phone:PhoneApplicationPage.Triggers> <EventTrigger> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="planeProjection" Storyboard.TargetProperty="RotationY" From="-90" To="0" Duration="0:0:01" /> </Storyboard> </BeginStoryboard> </EventTrigger> </phone:PhoneApplicationPage.Triggers>
|
The PlaneProjection is set to the Projection property of the whole PhoneApplicationPage element, and the animation is triggered when the page is first loaded. The animation makes the RotationY property go from –90 degrees to zero, with a CenterOfRotationX equal to zero. This causes the page to sweep in almost like a door: