April 2010

Volume 25 Number 04

UI Frontiers - Projection Transforms Sans Math

By Charles Petzold | April 2010

In pretty much any graphics system, transforms constitute the most important feature that doesn’t actually draw anything. Instead, transforms alter the appearance of visual objects by modifying coordinates with mathematical formulas generally expressed as a matrix multiplication.

The RenderTransform property defined by the UIElement has been in Silverlight from its beginning, and before that, in the Windows Presentation Foundation (WPF). Because the property is defined by UIElement, you can use it with graphical objects as well as text, controls and media. Simply set the RenderTransform to an object of type TranslateTransform, ScaleTransform, RotateTransform, SkewTransform, MatrixTransform (for complete control over the transform matrix) or a TransformGroup for a combination of multiple transforms.

The types of transforms you set with RenderTransform are all examples of two-dimensional (2D) 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. If two lines are parallel before the transform, they’re still parallel after the transform.

Pseudo 3D

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 useful for creating taper transforms, where opposite sides of a square or rectangle taper somewhat in one direction. Figure 1 shows some text with a taper transform realized through a very simple Projection property setting.

Figure 1 Text with a Taper Transform
Figure 1 Text with a Taper Transform

The text appears to be somewhat three dimensional (3D) because the tail end 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 important—no clipping of objects based on their arrangement in 3D space.

Nevertheless, 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 some common and simple use of the Projection property fairly easy.

The Easier Approach

You can set the Projection property to either a Matrix3DProjection object or a PlaneProjection object. The Matrix3DProjection property defines only one object, but it’s a 4x4 Matrix3D structure, which requires lots of mathematics. (For some approaches to this structure, see the blog entries on my Web site—charlespetzold.com—dated July 23, 2009, and July 31, 2009.)

But in this article, I've promised myself to avoid mathematics for the most part, which means I’ll be sticking with the PlaneProjection class. Although the class defines 12 settable properties, I’ll be focusing on only six of them.

In the downloadable source code for this article is PlaneProjectionExperimenter, which lets you interactively experiment with these six properties. Figure 2 shows the program in action. You can run it by compiling the downloadable program, or you can run it at my Web site at http://charlespetzold.com/, along with all the other programs in this article. For now, ignore the blue dot in the middle.


Figure 2 The PlaneProjectionExperimenter Program

The three crucial properties of PlaneProjection are RotationX, RotationY and RotationZ, which you can change using the three ScrollBars. These properties assume a 3D coordinate system where X values increase to the right and Y values increase going down the screen. (This is consistent with the normal Silverlight coordinate system but not with typical 3D systems, where values of Y usually increase going up.) Increasing values of Z seem to come out of the screen toward the viewer. These three properties cause rotation around the three axes.

The ScrollBars for X and Y are positioned perpendicularly to the axis of rotation. For example, the vertical ScrollBar on the left rotates the figure around the X axis, which runs horizontally through the center of the letter. The top and bottom of the letter seem to flip toward or away from the viewer.

At first, it’s best to try each axis of rotation independently of the others and refresh your browser between experimentations. 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. Using PlaneProjection sacrifices some flexibility in these rotations. PlaneProjection always applies RotationX first, then RotationY and finally RotationZ. How can we tell? Try leaving RotationZ at 0 and manipulate RotationX and RotationY. You’ll see that RotationX always rotates the letter around the horizontal axis of the letter itself, whereas RotationY rotates the letter around the vertical axis of the window, meaning that RotationY is applied to the letter already rotated by the RotationX angle. Now, with RotationX and RotationY set to anything, manipulate RotationZ. This rotation is also relative to the window, and doesn’t change the appearance of the letter at all.

In real life, you can simply set one property of Projection and get a reasonable result. The text in Figure 1 is part of the TaperText project and was displayed using the following XAML:

<TextBlock Text="TAPER"

           FontFamily="Arial Black"

           FontSize="144"

           HorizontalAlignment="Center"

           VerticalAlignment="Center">

    <TextBlock.Projection>

        <PlaneProjection RotationY="-60" />

    </TextBlock.Projection>

</TextBlock>

As with RenderTransform, Projection doesn’t affect layout. The layout system sees an un-transformed and un-projected element.

Of course, the RotationX, RotationY and RotationZ properties are all backed by dependency properties, so they can also become animation targets.

The FlipPanel

With just this much knowledge of PlaneProjection, it’s possible to code a “flip panel,” which is a technique to minimize an application footprint onscreen by organizing controls on the front and back of a panel (or so it seems). In WPF, a FlipPanel control requires switching back and forth between 2D and 3D. In Silverlight, the FlipPanel becomes quite simple.

The FlipPanelDemo project includes a FlipPanel control derived from UserControl. The code part shown in Figure 3 defines two new properties named Child1 and Child2 of type UIElement, and public methods named Flip and FlipBack.

Figure 3 The FlipPanel.xaml.cs File

using System;

using System.Windows;

using System.Windows.Controls;



namespace FlipPanelDemo

{

    public partial class FlipPanel : UserControl

    {

        public static readonly DependencyProperty Child1Property =

            DependencyProperty.Register("Child1",

                typeof(UIElement),

                typeof(FlipPanel),

                new PropertyMetadata(null, OnChild1Changed));



        public static readonly DependencyProperty Child2Property =

            DependencyProperty.Register("Child2",

                typeof(UIElement),

                typeof(FlipPanel),

                new PropertyMetadata(null, OnChild2Changed));



        public FlipPanel()

        {

            InitializeComponent();

        }



        public UIElement Child1

        {

            set { SetValue(Child1Property, value); }

            get { return (UIElement)GetValue(Child1Property); }

        }



        public UIElement Child2

        {

            set { SetValue(Child2Property, value); }

            get { return (UIElement)GetValue(Child2Property); }

        }



        public void Flip()

        {

            flipStoryboard.Begin();

        }



        public void FlipBack()

        {

            flipBackStoryboard.Begin();

        }



        static void OnChild1Changed(DependencyObject obj,

                        DependencyPropertyChangedEventArgs args)

        {

            (obj as FlipPanel).child1Container.Content = args.NewValue;

        }



        static void OnChild2Changed(DependencyObject obj,

                        DependencyPropertyChangedEventArgs args)

        {

            (obj as FlipPanel).child2Container.Content = args.NewValue;

        }

    }

}

The XAML part in Figure 4 shows how Child1 and Child2 occupy the same space and have Projection transforms applied. In the initial position, the PlaneProjection transform for Child2 has RotationX set to -90 degrees, which means that it’s at right angles to the viewer and is effectively invisible. The “flip” animations simply swing Child1 out of view by animating RotationX to 90; they swing Child2 into view by animating RotationX to 0. The “flip back” animations reverse those actions.

Figure 4 The FlipPanel.xaml File

<UserControl x:Class="FlipPanelDemo.FlipPanel"

             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 

             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <UserControl.Resources>

        <Storyboard x:Name="flipStoryboard">

            <DoubleAnimation Storyboard.TargetName="planeProjection1"

                             Storyboard.TargetProperty="RotationX"

                             To="90"

                             Duration="0:0:0.5" />

            

            <DoubleAnimation Storyboard.TargetName="planeProjection2"

                             Storyboard.TargetProperty="RotationX"

                             To="0"

                             BeginTime="0:0:0.5"

                             Duration="0:0:0.5" />

        </Storyboard>



        <Storyboard x:Name="flipBackStoryboard">

            <DoubleAnimation Storyboard.TargetName="planeProjection1"

                             Storyboard.TargetProperty="RotationX"

                             To="0"

                             BeginTime="0:0:0.5"

                             Duration="0:0:0.5" />

            <DoubleAnimation Storyboard.TargetName="planeProjection2"

                             Storyboard.TargetProperty="RotationX"

                             To="-90"

                             Duration="0:0:0.5" />

        </Storyboard>



    </UserControl.Resources>

    

    <Grid x:Name="LayoutRoot" Background="White">

        <ContentControl Name="child1Container">

            <ContentControl.Projection>

                <PlaneProjection x:Name="planeProjection1" />

            </ContentControl.Projection>

        </ContentControl>

        <ContentControl Name="child2Container">

            <ContentControl.Projection>

                <PlaneProjection x:Name="planeProjection2" 

                                 RotationX="-90" />

            </ContentControl.Projection>

        </ContentControl>

    </Grid>

</UserControl>

Commonly, Child1 and Child2 would be set to panels of some sort covered with controls. In the FlipPanelDemo program, Child1 and Child2 simply have different colors and a single button to trigger the flip. The flipping action seems to be more comforting to the user than navigating to a new page because it implies that a set of controls aren’t disappearing irrevocably but can easily be retrieved.

Center of Rotation

All rotation is relative to a center. Rotation in two dimensions is relative to a point. Rotation in three dimensions is relative to a line in 3D space—often referred to as an “axis of rotation.” For convenience—and perhaps to avoid introducing 3D lines and 3D vectors into Silverlight—the projection transform is considered to be relative to a 3D point.

The PlaneProjection class supports changing the center of rotation using three properties:

  • CenterOfRotationX (relative coordinate; default is 0.5)
  • CenterOfRotationY (relative coordinate; default is 0.5)
  • CenterOfRotationZ (absolute coordinate; default is 0)

Let’s look at the first two first. Like the RenderTransformOrigin property, these values are relative to the upper-left corner of the element being transformed. They differ in that the default value of RenderTransformOrigin is the point (0, 0), whereas the default values for PlaneProjection cause the rotations to be centered on the element.

In PlanProjectionExperimenter, you can change CenterOfRotationX and CenterOfRotationY using the blue dot. (A tooltip indicates the current values.) You’ll probably notice right away that CenterOfRotationX does not affect rotation around the X axis, and CenterOfRotationY does not affect rotation around the Y axis.

If you make CenterOfRotationX equal to 0, rotation around the Y axis is relative to the left side of the element; similarly, if you set it to 1, rotation is around the right side. You can make the value less than 0 or greater than 1 for some very wide swings. The element seems to get very close to the viewer and go very far away.

Try this: Make CenterOfRotationY approximately equal to 0. Now, set RotationX equal to 90 degrees or thereabouts. You’ll notice that the element is still visible. In the FlipPanel, a rotation of 90 degrees or -90 degrees causes the element to be invisible because it’s viewed on its edge. The mathematical calculations going on inside the PlaneProjection class assume that the viewer’s eye (or the metaphorical camera) is always aligned in the center of the element looking straight back in the negative-Z direction. If something is off-center and rotated back 90 degrees, it’s still going to be visible from the center of the element.

The CenterOfRotationZ axis is handled differently from X and Y. It’s in Silverlight absolute coordinates rather than relative coordinates, and the default is 0. In PlaneProjectionExperimenter, you can change this value using the mouse wheel. The blue dot grows and shrinks to represent the change in the property.

Rotation with a non-default CenterOfRotationZ is probably the hardest to visualize mentally, so it’s worth the time to experiment a bit.

The element being projected is assumed to sit in the XY plane—that is, the plane in 3D space where Z equals 0. If you leave the CenterOfRotationZ property at its default value of 0 and manipulate RotationX or RotationY, parts of the letter get larger as they move into positive-Z space, and parts get smaller as they move into negative-Z space. Now increase CenterOfRotationZ to a value of about 200 and manipulate RotationY. The letter will get larger because it’s rotating around the center where Z equals 200 from an area of space where Z equals 0 to the area where Z equals 400. When you set CenterOfRotationZ to 500 or greater, the whole thing stops working well because the internal mathematics of PlaneProjection assume that the viewer (or camera) is located 1,000 units from XY plane. With a rotation center of 500, the element is actually 
being projected behind the camera.

Can we make something similar to a FlipPanel with three sides rather than just two? Yes, but it will require a tiny bit of trigonometry and a whole lot of messing around with Z indices.

Figure 5 shows a top view of what I envision. It’s a view from a negative position on the Y axis—above the monitor, so to speak—looking down. The thick lines represent the three panels. All these panels are actually positioned in the same spot. Only the Projection transform causes them to appear in different locations. The visible one in front has no rotations applied to it. The other two have been subjected to rotations where RotationY is set to 120 (for the one on the right) and -120 degrees (on the left). Both rotations are centered on a negative CenterOfRotationZ value, which corresponds to the black dot. What is that value?

Figure 5 A Proposed Three-Panel Carousel
Figure 5 A Proposed Three-Panel Carousel

If you connect that dot with a dotted line to the vertex at the right, the dotted line makes an angle of 30 degrees with the X axis. The tangent of 30 degrees is 0.577. That’s the ratio of the distance of the dot to the X axis, divided by half the width of the panel. If the panel is 200 units wide, that dot is at -57.7. Set CenterOfRotationZ to that value.

Now, to rotate the carousel from one panel to the next, just animate RotationY to increase it by 120 degrees for all three panels.

Well, not exactly. Even though we’re picturing these panels in 3D space, they really still exist in 2D space, and they’re actually stacked on top of one another. As they’re rotating, the Canvas.ZIndex attached property of each of the three panels must be changed to reorder the panels. This is how elements seemingly can be moved “in front of” or “behind” other elements.

Let’s assume rotation is clockwise based on the Figure 5 diagram. To begin, the panel in front should have a Z index of 2 (foreground), the one on the right has a Z index of 1 and the one on the left has a Z index of 0 (background). Now start the 120-degree rotation. Halfway through—that is, when each panel has been rotated 60 degrees and two panels are equally visible—the Z indices must be changed. The panel moving into view should have its Z index set to 2, and the one moving out of view should have its Z index set to 1.

This is all implemented in the ThreePanelCarousel project. In actual use, of course, the three panels would be covered with controls; in this demonstration program, they just have different colors and one button. I’ve left the panels partially transparent so you can see what’s going on. Figure 6 shows the panels in action.

Figure 6 The ThreePanelCarousel Program as It’s Spinning
Figure 6 The ThreePanelCarousel Program as It’s Spinning

You may wonder if it’s possible to apply a single projection rotation to the composite group of three panels. No, it’s not. Multiple projection transforms can’t be compounded over child elements.

Other Effects

As you experiment with the Projection property, don’t forget about other effects to “enhance” the 3D-ishness of your application. A little shadow always helps, and it’s easy to simulate a shadow—even a moving shadow—with a gradient brush.

The AsTheDaysGoBy program uses a combination of animations to simulate the days flying off a daily desk calendar, as if in an old movie to suggest the passing of time. The calendar page has its Projection property set to a PlaneProjection object with the CenterOfRotationX and CenterOfRotationY properties both set to 0. RotationX and RotationY are both animated to move the page up and to the left while an animated gradient brush sweeps up the page. Finally, the page just seems to disappear as its Opacity property is animated to 0. Figure 7 shows the program in action.

Figure 7 The AsTheDaysGoBy Program
Figure 7 The AsTheDaysGoBy Program

The animation lasts a second, so the program goes through 60 days every minute, or about 10 years per hour. After about a month, the program will be approaching the maximum DateTime value, and it will soon terminate with an exception. Until then, enjoy.


Charles Petzold  is a long-time contributing editor to MSDN Magazine*. His most recent book is “The Annotated Turing: A Guided Tour Through Alan Turing’s Historic Paper on Computability and the Turing Machine” (Wiley, 2008). Petzold blogs on his Web site charlespetzold.com.*