Cube transition

Cube transition

This example illustrates how you can create a neat 3D transition effect between two screens.

The viewport

To get proper 3D perspective projection, we place everything inside a Viewport at the root of the app. Note that we set CullFace="Back". This makes it so that only the front face of any element is visible.

The Perspective property controls the distance (in points) between the camera and the Z = 0 plane (where graphics are drawn by default). The field of view is then calculated based on this distance and the size of the viewport. By setting PerspectiveRelativeTo="Width", the Perspective property is treated as a factor, and the final distance is computed as Perspective * Viewport width. Now the camera distance is always proportional to the width of the viewport, making the field of view independent of the width of the viewport – which is what we want in this case.

<Viewport Perspective="1.7" PerspectiveRelativeTo="Width" CullFace="Back">

The box

Here’s the secret: our box is not actually a box. It’s simply two Panels that line up correctly at a 90 degree angle. To make the Panels line up, we set our TransformOrigin to VerticalBoxCenter.

The TransformOrigin property determines the origin of transformation for an element. VerticalBoxCenter makes an element act as the front face of a box. It achieves this by measuring the width of the element, and interpreting that as the depth of an “imaginary” box. The resulting point of origin will be at X = Y = -Z = width / 2, since we want to transform around the center of the box.

Inside our box panel, we have a second panel called sidePage. It also has the VerticalBoxCenter TransformOrigin, but is rotated 90 degrees around the Y axis. This places it as the left face of the box.

<Panel ux:Name="box" TransformOrigin="VerticalBoxCenter">
    <Panel ux:Name="sidePage" TransformOrigin="VerticalBoxCenter">
        <Rotation DegreesY="90" />

The transition

The transition itself is actually very simple. We rotate the box 90 degrees counter-clockwise around the Y-axis, so that the sidePanel faces the “camera”. We also fade out the content, fade in the menu, and tell the hamburger icon to morph into a close icon.

<WhileTrue ux:Name="menuIsOpen" Value="false">
    <Rotate Target="box" DegreesY="-90" Duration=".7" Delay="0" DelayBack="0" Easing="ExponentialOut" EasingBack="ExponentialIn" />
    <Change content.Opacity="0" Duration=".7" DelayBack="0" Easing="ExponentialOut" EasingBack="ExponentialIn" />
    <Change menu.Opacity="1" Duration=".3" DelayBack="0" Easing="QuarticIn" EasingBack="QuarticIn" /> 
    <Change hamburger.IsOpen="true" DelayBack="0" />
</WhileTrue>

The search field

Although the search button and the other menu items look the same initially, they’re implemented completely differently. To avoid copying the same code between the search field and the rest of the menu items, we’ve created a tiny MenuItem component that applies the same height and underline to both.

<Size ux:Global="MenuItemHeight" ux:Value="40" />
<Panel ux:Class="MenuItem" Height="MenuItemHeight">
    <Rectangle Alignment="Bottom" Height="2" Color="#fff2" />
</Panel>

The search field itself is a MenuItem, containing a TextInput set up to look like the other menu items.

There are two cases where we want the search field to be expanded – while it’s either in focus, or has a non-empty value. We implement this using a WhileFocused and a WhileString trigger, which both activate the same searchFieldActive trigger.

<MenuItem ux:Name="searchField">
    <TextInput ux:Name="searchInput" FontSize="28" PlaceholderText="search" PlaceholderColor="ForegroundColor" TextColor="ForegroundColor" CaretColor="#fffa" Alignment="Top">
        <WhileFocused>
            <Change searchFieldActive.Value="true" />
        </WhileFocused>
        <WhileString Test="IsNotEmpty" Value="{ReadProperty searchInput.Value}">
            <Change searchFieldActive.Value="true" />
        </WhileString>
    </TextInput>
    
    <LayoutAnimation>
        <Resize X="1" Y="1" RelativeTo="SizeChange" DurationBack=".3" DelayBack=".2" Easing="ExponentialInOut" />
        <Move X="1" Y="1" RelativeTo="WorldPositionChange" DelayBack="0" Duration=".3" Easing="QuarticIn" />
    </LayoutAnimation>
</MenuItem>

searchFieldActive performs the transition from menu to search layout. Most of the animation is simply setting the LayoutMaster and Alignment of the search field, which will trigger the LayoutAnimation above. We also move the close button and the other menu items out of view.

<WhileTrue ux:Name="searchFieldActive" Value="false">
    <Change searchField.LayoutMaster="searchFieldLayoutMaster" Delay=".07" DelayBack=".07" />
    <Change transitionToSearchLayout.TargetProgress="1" />
</WhileTrue>

<Timeline ux:Name="transitionToSearchLayout">
    <Move Target="normalMenuItems" Y="1" RelativeTo="Size" Delay=".1" Duration=".5" Easing="CubicInOut" />
    <Change normalMenuItems.Opacity="0" Delay=".1" Duration=".3" Easing="CubicInOut" />
    <Move Target="hamburger" Y="-2" RelativeTo="Size" Duration=".4" Easing="CubicIn" />
    <Change searchInput.PlaceholderColor="#ccc6" Duration=".5" Easing="QuadraticInOut" />
    <Change searchCloseButton.Opacity="1" Duration=".2" DelayBack="0" Easing="QuadraticInOut" />
</Timeline>

<DockPanel ux:Name="searchLayout" Alignment="Top" Margin="30" Height="MenuItemHeight">
    <Panel ux:Name="searchFieldLayoutMaster" />
    <Rectangle ux:Name="searchCloseButton" Dock="Right" Opacity="0" HitTestMode="LocalBounds" Margin="15,0,0,0">
        <Clicked>
            <Set searchInput.Value="" />
        </Clicked>

And that’s pretty much it for this example! As always, feel free to download and play around with the code yourself.