Проекционные преобразования в WinRT
116WinRT --- Проекционные преобразования
Ранее я объяснил, почему для графических преобразований на плоскости удобно использовать математическое описание в виде матрицы 3 x 3 и для чего нужен выход в третье измерение. Аналогичным образом пространственное графическое преобразование выражается матрицей 4 x 4, и в Windows Runtime предусмотрена такая возможность.
Пространство имен Windows.UI.Xaml.Media.Media3D состоит всего из двух определений: структура Matrix3D доступна для всех программистов, а класс Matrix3DHelper представляет интерес в основном для программистов C++, для которых недоступны методы, определяемые Matrix3D. Свойства Matrix3D аналогичны свойствам обычной структуры Matrix:
Впрочем, программисты практически никогда не имеют дела с этой матрицей. Большинству из них вполне хватает класса PlaneProjection, упоминавшегося ранее.
Класс PlaneProjection предназначен в основном для выполнения поворота двумерных элементов в трехмерном пространстве. Вращение в трехмерном пространстве всегда выполняется относительно некоторой оси; класс PlaneProjection позволяет вращать элемент вокруг горизонтальной оси (свойство RotationX), вертикальной оси (RotationY) или концептуальной оси Z, выходящей из плоскости экрана. Поворот вокруг оси Z представляет собой обычный двумерный поворот, ничего особенно интересного в нем нет.
Направление вращения можно определить по правилу правой руки: направьте большой палец правой руки в положительном направлении оси (вправо для оси X, вниз для оси Y, из экрана для оси Z). Кривая, образованная пальцами, обозначает направление вращения для положительных углов.
Класс PlaneProjection применяет повороты в порядке X/Y/Z, но обычно вы используете только один из них. При нетривиальном использовании PlaneProjection можно заставить элементы появляться на экране и даже «переворачиваться», открывая изображение на «другой стороне» (вскоре мы рассмотрим пример).
Впрочем, есть и более прямолинейные применения. Следующая программа применяет независимые анимации к свойствам RotationX, RotationY и RotationZ имитируя вращение объекта TextBlock в трехмерном пространстве. Следующий файл XAML создает в нижней части приложения группу управляющих кнопок:
<Page ...>
<Page.Resources>
<Storyboard x:Key="xanimation" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="planePrj"
Storyboard.TargetProperty="RotationX"
From="0" To="360" Duration="0:0:1.9" />
</Storyboard>
<Storyboard x:Key="yanimation" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="planePrj"
Storyboard.TargetProperty="RotationY"
From="0" To="360" Duration="0:0:3.1" />
</Storyboard>
<Storyboard x:Key="zanimation" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="planePrj"
Storyboard.TargetProperty="RotationZ"
From="0" To="360" Duration="0:0:4.3" />
</Storyboard>
</Page.Resources>
<Grid Background="#FF1D1D1D">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="3D-эффект"
FontSize="220"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<TextBlock.Projection>
<PlaneProjection x:Name="planePrj" />
</TextBlock.Projection>
</TextBlock>
<!-- Панель управления -->
<Grid Grid.Row="1" HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="15" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style TargetType="Button">
<Setter Property="Width" Value="140" />
<Setter Property="Margin" Value="14" />
</Style>
</Grid.Resources>
<TextBlock Text="Ось X: " Tag="xanimation" />
<Button Content="Запустить" Grid.Column="1"
Click="Begin_Click" />
<Button Content="Пауза" Grid.Column="2"
IsEnabled="False"
Click="Pause_Click" />
<TextBlock Text="Ось Y: " Grid.Row="1" Tag="yanimation" />
<Button Content="Запустить" Grid.Row="1" Grid.Column="1"
Click="Begin_Click" />
<Button Content="Pause" Grid.Row="1" Grid.Column="2"
Click="Pause_Click" IsEnabled="False" />
<TextBlock Text="Ось Z: " Grid.Row="2" Tag="zanimation" />
<Button Content="Запустить" Grid.Row="2" Grid.Column="1"
Click="Begin_Click" />
<Button Content="Pause" Grid.Row="2" Grid.Column="2"
Click="Pause_Click" IsEnabled="False" />
</Grid>
</Grid>
</Page>
Продолжительности отдельных объектов DoubleAnimation несколько отличаются для предотвращения повторяющихся эффектов при их одновременном воспроизведении. Кнопки в файле фонового кода используют методы Begin, Stop, Pause и Resume класса Storyboard для управления происходящим:
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;
namespace WinRTTestApp
{
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
private void Begin_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
Button button = (Button)sender;
string key = GetSibling(button, -1).Tag.ToString();
Storyboard storyboard = (Storyboard)this.Resources[key];
Button pauseResumeButton = (Button)GetSibling(button, 1);
pauseResumeButton.Content = "Пауза";
if (button.Content as string == "Запустить")
{
storyboard.Begin();
button.Content = "Стоп";
pauseResumeButton.IsEnabled = true;
}
else
{
storyboard.Stop();
button.Content = "Запустить";
pauseResumeButton.IsEnabled = false;
}
}
private void Pause_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
Button button = (Button)sender;
string key = GetSibling(button, -2).Tag.ToString();
Storyboard storyboard = (Storyboard)this.Resources[key];
if (button.Content as string == "Пауза")
{
storyboard.Pause();
button.Content = "Пуск";
}
else
{
storyboard.Resume();
button.Content = "Пауза";
}
}
private FrameworkElement GetSibling(FrameworkElement e, int index)
{
Panel parent = (Panel)e.Parent;
int i = parent.Children.IndexOf(e);
return (FrameworkElement)parent.Children[i + index];
}
}
}
Результат выглядит примерно так:
Класс PlaneProjection также содержит ряд дополнительных свойств. Свойства CenterOfRotationX и CenterOfRotationY задаются в системе координат относительно элемента. По умолчанию используются значения 0,5 - центр элемента, который чаще всего используется на практике. Свойство CenterOfRotationZ задается в пикселах со значением по умолчанию 0, соответствующим поверхности экрана. Во внутренних вычислениях предполагается, что «камера» (или пользователь) рассматривает экран с расстояния 1000 пикселов (около 10 дюймов).
PlaneProjection также определяет три свойства LocalOffset для измерений X, Y и Z и три свойства GlobalOffset - величин сдвига в пикселах. Значения LocalOffset применяются до поворота, значения GlobalOffset применяются после поворота. Вы будете чаще использовать свойства GlobalOffset.
Рассмотрим маленький пример «перекидной панели» - эффекта, который когда-то реализовывался с большими трудностями и требовал применения полноценного 3D-программирования. Представьте панель, которая содержит набор элементов; эту панель можно перевернуть, чтобы использовать другой (хотя и логически связанный) набор элементов. В этом примере для представления «передней» и «обратной» сторон используются две панели Grid с разными цветами фона, каждая из которых содержит поле TextBlock:
<Grid Background="#FF1D1D1D">
<Grid HorizontalAlignment="Center"
VerticalAlignment="Center"
Tapped="OnGridTapped">
<Grid Name="grid1" Background="LimeGreen"
Canvas.ZIndex="1">
<TextBlock Text="Привет"
HorizontalAlignment="Center"
FontSize="180" />
</Grid>
<Grid Name="grid2"
Background="DarkOrange"
Canvas.ZIndex="0">
<TextBlock Text="Windows 8"
FontSize="180" />
</Grid>
<Grid.Projection>
<PlaneProjection x:Name="projection" />
</Grid.Projection>
</Grid>
</Grid>
Обратите внимание на значения Canvas.ZIndex. Они гарантируют, что панель grid1 визуально располагается поверх grid2, даже если она предшествует ей в коллекции потомков их общего родителя.
Секция Resources содержит два определения Storyboard - для прямого и обратного переворота:
<Page.Resources>
<Storyboard x:Key="flipStoryboard">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="projection"
Storyboard.TargetProperty="RotationY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<LinearDoubleKeyFrame KeyTime="0:0:0.99" Value="90" />
<DiscreteDoubleKeyFrame KeyTime="0:0:1.01" Value="-90" />
<LinearDoubleKeyFrame KeyTime="0:0:2" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetName="projection"
Storyboard.TargetProperty="GlobalOffsetZ"
From="0" To="-1000" Duration="0:0:1"
AutoReverse="True" />
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="grid1"
Storyboard.TargetProperty="(Canvas.ZIndex)">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="1" />
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="0" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="grid2"
Storyboard.TargetProperty="(Canvas.ZIndex)">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="0" />
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="1" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="flipBackStoryboard">
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="projection"
Storyboard.TargetProperty="RotationY">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<LinearDoubleKeyFrame KeyTime="0:0:0.99" Value="-90" />
<DiscreteDoubleKeyFrame KeyTime="0:0:1.01" Value="90" />
<LinearDoubleKeyFrame KeyTime="0:0:2" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimation Storyboard.TargetName="projection"
Storyboard.TargetProperty="GlobalOffsetZ"
From="0" To="-1000" Duration="0:0:1"
AutoReverse="True" />
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="grid1"
Storyboard.TargetProperty="(Canvas.ZIndex)">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="0" />
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="1" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="grid2"
Storyboard.TargetProperty="(Canvas.ZIndex)">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="1" />
<DiscreteObjectKeyFrame KeyTime="0:0:1" Value="0" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Page.Resources>
Два объекта Storyboard очень похожи. Каждый из них содержит тег DoubleAnimationUsingKeyFrames для свойства RotationY объекта PlaneProjection. Свойство изменяется от 0 до 90° или -90° (точка, в которой объект находится под прямым углом к пользователю), после чего переключается на 180°, чтобы анимация могла продолжаться в том же направлении обратно до 0.
В то же время свойство GlobalOffsetZ изменяется от 0 до -1000 и обратно до 0. Так создается иллюзия того, что панель уходит за экран, готовясь к выполнению переворота.
На середине пути в каждом объекте Storyboard индексы Canvas.ZIndex меняются местами. Свойство Canvas.ZIndex также часто используется в качестве целевого для ObjectAnimationUsingKeyFrames.
Анимации запускаются касанием, которое обрабатывается в файле фонового кода:
using Windows.UI.Xaml;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;
namespace WinRTTestApp
{
public sealed partial class MainPage : Page
{
Storyboard flipStoryboard, flipBackStoryboard;
bool flipped = false;
public MainPage()
{
this.InitializeComponent();
flipStoryboard = this.Resources["flipStoryboard"] as Storyboard;
flipBackStoryboard = this.Resources["flipBackStoryboard"] as Storyboard;
}
private void OnGridTapped(object sender, TappedRoutedEventArgs args)
{
if (flipStoryboard.GetCurrentState() == ClockState.Active ||
flipBackStoryboard.GetCurrentState() == ClockState.Active)
{
return;
}
Storyboard storyboard = flipped ? flipBackStoryboard : flipStoryboard;
storyboard.Begin();
flipped ^= true;
}
}
}
Большая часть этой логики предотвращает запуск одного объекта Storyboard до того, как завершится выполнение предыдущего объекта. При том способе, которым объекты Storyboard определены в нашей программе, это вызовет расхождения. (Попробуйте удалить команду return из OnGridTapped, чтобы увидеть неудовлетворительный результат.) Я бы предпочел, чтобы касание во время анимации просто переключало операцию на обратное направление, но это потребовало бы более сложной логики.