Преобразования Geometry в WinRT

74

Класс Geometry определяет свойство Transform. Возникает естественный вопрос: чем применение преобразования к элементу Path отличается от применения преобразования к объекту Geometry, который задается свойству Data объекта Path?

Основное различие заключается в том, что преобразование, примененное к свойству RenderTransform объекта Path, увеличивает толщину линий, а с применением преобразования к Geometry это не происходит.

Ниже приведен элемент Path, в основу которого заложен тег RectangleGeometry с шириной и высотой 5, но с применением преобразования, увеличивающего его с коэффициентом 40:

<Grid Background="#FF1D1D1D">
        <Path Stroke="LimeGreen" StrokeDashArray="1 1" StrokeThickness="1"
              VerticalAlignment="Center" HorizontalAlignment="Center">
            <Path.Data>
                <RectangleGeometry Rect="0 0 5 5"
                                   Transform="40 0 0 40 0 0"></RectangleGeometry>
            </Path.Data>
        </Path>
</Grid>

Результат выглядит так, словно значение Rect в RectangleGeometry определяется с шириной и высотой 200:

Трансформирование прямоугольника

В следующей разметке XAML используется тот же исходный тег RectangleGeometry, но преобразование применяется к Path:

<Grid Background="#FF1D1D1D">
        <Path Stroke="LimeGreen" StrokeDashArray="1 1" StrokeThickness="1"
              VerticalAlignment="Center" HorizontalAlignment="Center"
              RenderTransform="40 0 0 40 0 0">
            <Path.Data>
                <RectangleGeometry Rect="0 0 5 5" />
            </Path.Data>
        </Path>
</Grid>

Результат выглядит совершенно иначе:

Трансформирование пути

Однако с точки зрения системы формирования макета эти элементы идентичны. Оба элемента Path воспринимаются системой как обладающие шириной и высотой 5.

Преобразования Brush

Класс Brush определяет два свойства, относящихся к преобразованиям: Transform и RelativeTransform. Они отличаются способом задания коэффициентов преобразования: в соответствии с размером кисти в пикселах или относительно ее размера. Как правило, свойство RelativeTransform проще в использовании, если только элементу, которому назначается кисть, не задан конкретный размер в пикселах.

Следующая программа воспроизводит приложение RainbowEight из статьи "Таймеры и анимации", но с использованием анимированного преобразования кисти. Вместо TextBlock я использовал для прорисовки «восьмерки» Path, потому что с TextBlock мне не удалось добиться повторения кисти при помощи свойства SpreadMethod, равного Repeat:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Viewbox>
            <Path StrokeThickness="40" Margin="0 30">
                <Path.Stroke>
                    <LinearGradientBrush StartPoint="0 0" EndPoint="1 1"
                                         SpreadMethod="Repeat">
                        <LinearGradientBrush.RelativeTransform>
                            <TranslateTransform x:Name="tlt" />
                        </LinearGradientBrush.RelativeTransform>

                        <GradientStop Offset="0.00" Color="#ed1515" />
                        <GradientStop Offset="0.14" Color="#ffa904" />
                        <GradientStop Offset="0.28" Color="#fff004" />
                        <GradientStop Offset="0.43" Color="#04ff27" />
                        <GradientStop Offset="0.57" Color="#0468ff" />
                        <GradientStop Offset="0.71" Color="#5704ff" />
                        <GradientStop Offset="0.86" Color="#b504ff" />
                        <GradientStop Offset="1.00" Color="#ed1515" />
                    </LinearGradientBrush>
                </Path.Stroke>
                <Path.Data>
                    <PathGeometry>
                        <PathFigure StartPoint="110 0">
                            <ArcSegment Size="90 90" Point="110 180" 
                                        SweepDirection="Clockwise" />
                            <ArcSegment Size="110 110" Point="110 400" 
                                        SweepDirection="Counterclockwise" />
                            <ArcSegment Size="110 110" Point="110 180" 
                                        SweepDirection="Counterclockwise" />
                            <ArcSegment Size="90 90" Point="110 0" 
                                        SweepDirection="Clockwise" />
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>
            </Path>
        </Viewbox>
    </Grid>

    <Page.Triggers>
        <EventTrigger>
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="tlt"
                                     Storyboard.TargetProperty="Y"
                                     RepeatBehavior="Forever"
                                     EnableDependentAnimation="True"
                                     From="0" To="-1.36" Duration="0:0:8" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Page.Triggers>
</Page>

А вот как выглядит изображение:

Анимация кисти с использованием преобразований

В разметке присутствует «волшебное число» - свойство To объекта DoubleAnimation. Это значение применяется к свойству Y объекта TranslateTransform, и оно было выбрано таким образом, чтобы сдвинутая кисть была идентична несдвинутой. Как видите, «волшебное число» равно -1,36, и вам, конечно, захочется узнать, откуда оно взялось.

Если бы кисть LinearGradientBrush проходила сверху вниз - свойство StartPoint равно (0,0), свойство EndPoint равно (0,1), - то значение To было бы равно просто -1. Если бы градиент проходил слева направо - свойство StartPoint равно (0,0), свойство EndPoint равно (1, 0), - то свойство X объекта TranslateTransform было бы целевым свойством анимации, а значение To было бы снова равно 1 или -1.

Но если градиент проходит от одного угла к противоположному - с принятой по умолчанию точкой StartPoint (0,0) и EndPoint (1,1), - начинаются проблемы. Когда элемент Path покрывается кистью, Windows Runtime вычисляет ограничивающим прямоугольник, включающий геометрический размер элемента и толщину линии. Затем кисть растягивается по этому прямоугольнику:

Структура градиента при заливке Path

Линия градиента проходит по диагонали, а линии постоянного цвета располагаются под прямым углом к линии градиента.

Когда кисть использует свойство SpreadMethod с режимом Repeat, то концептуально кисть повторяется за пределами заданных смещений. Это значение SpreadMethod полезно при применении к кисти преобразования TranslateTransform, потому что кисть повторяется независимо от ее сдвига.

Если сдвинуть кисть вверх на высоту элемента (значение Y, равное -1, в TranslateTransform), то нижний край непреобразованной кисти становится верхним краем преобразованной кисти; но, как видно из следующей иллюстрации, результат не совпадает с предыдущим:

Сдвиг градиента при задании свойства SpreadMethod

Для получения плавной анимации необходимо еще сдвинуть ее вверх. Но насколько? Расширим рисунок, чтобы на нем помещалась часть повторяющейся кисти. Обозначим ширину элемента w, высоту - h, диагональ - d, а приращение высоты - Δh:

Размеры градиента при трансформировании

Значение Δh можно вычислить многими способами, но, пожалуй, самый простой основан на подобии треугольников:

из чего легко выводится формула:

а в конечном итоге и интересующее нас число:

Попробуйте подставить числа из приведенного выше определения Path. К ширине и высоте геометрического объекта необходимо прибавить StrokeThickness. С шириной 270 и высотой 450 значение Δh равно 162. Прибавьте его к h, разделите результат на h - и вы получите волшебное число 1,36.

А хотите узнать более простой способ? Просто используйте в Storyboard два объекта DoubleAnimation: для свойства Y и для свойства X. Задайте свойству To обоих объектов значение -1, и кисть на каждом цикле будет сдвигаться влево и вверх.

Анимация CompositeTransform

Ранее я упоминал о том, что вычисленное значение Matrix доступно из TransformGroup, но не из других источников, от которых это можно было бы ожидать. Например, было бы логично предположить, что класс GeneralTransform (производным от которого являются Transform и все остальные классы преобразований) должен содержать свойство Matrix, но у него такого свойства нет.

Однако класс GeneralTransform содержит метод TransformPoint() и метод TransformBounds(), который применяет преобразование к значению Rect, и в некоторых обстоятельствах эти методы оказываются очень полезными.

Допустим, элемент размещен на панели. Панель ответственна за позиционирование элемента относительно себя, но к элементу также может быть применено преобразование RenderTransform со сдвигом, масштабированием, поворотом или отклонением. Для целей проверки принадлежности (hit-testing) местонахождение и ориентация элемента известны системе. Но может ли ваша программа узнать, где фактически расположен элемент?

Да! Класс UIElement определяет очень важный (хотя и не очевидный) метод TransformToVisual(). Обычно этот метод вызывается для элемента с аргументом, содержащим родителя или другого предка элемента:

GeneralTransform gtransform = el.TransformToVisual(parent);

Объект GeneralTransform, возвращаемый методом, отображает координаты элемента в родительские координаты. При этом увидеть само преобразование вы не сможете - объект не предоставляет значение Matrix. Ваши возможности ограничиваются вызовом TransformPoint() или TransformBounds() или использованием свойства Inverse. Впрочем, часто этого оказывается достаточно.

Следующий файл XAML применяет анимацию к свойствам CompositeTransform, заставляя элемент TextBlock прыгать по экрану:

<Page ...>

    <Grid Name="contentGrid" Background="#FF1D1D1D">
        <TextBlock Name="txb"
                   Text="Нажмите, чтобы остановить"
                   FontSize="60"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   RenderTransformOrigin="0.5 0.5">
            <TextBlock.RenderTransform>
                <CompositeTransform x:Name="compositeTransform" />
            </TextBlock.RenderTransform>
        </TextBlock>

        <Polygon Name="polygon" Stroke="Blue" />
        <Path Name="path" Stroke="Red" />
    </Grid>

    <Page.Triggers>
        <EventTrigger>
            <BeginStoryboard>
                <Storyboard x:Name="storyboard">
                    <DoubleAnimation Storyboard.TargetName="compositeTransform"
                                     Storyboard.TargetProperty="TranslateX"
                                     From="-300" To="300" Duration="0:0:2.09"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="compositeTransform"
                                     Storyboard.TargetProperty="TranslateY"
                                     From="-300" To="300" Duration="0:0:2.21"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="compositeTransform"
                                     Storyboard.TargetProperty="Rotation"
                                     From="0" To="360" Duration="0:0:2.49"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="compositeTransform"
                                     Storyboard.TargetProperty="ScaleX"
                                     From="1" To="2" Duration="0:0:2.75"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="compositeTransform"
                                     Storyboard.TargetProperty="ScaleY"
                                     From="1" To="2" Duration="0:0:3.05"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="compositeTransform"
                                     Storyboard.TargetProperty="SkewX"
                                     From="-30" To="30" Duration="0:0:3.29"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                    <DoubleAnimation Storyboard.TargetName="compositeTransform"
                                     Storyboard.TargetProperty="SkewY"
                                     From="-30" To="30" Duration="0:0:3.51"
                                     AutoReverse="True" RepeatBehavior="Forever" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Page.Triggers>
</Page>

Обратите внимание: Grid содержит синий объект Polygon и красный объект Path, но без координатных точек.

Файл фонового кода использует событие Tapped для создания «снимка» TextBlock посредством вызова TransformToVisual() и приостановки Storyboard (выполнение продолжается при следующем касании). TransformToVisual() возвращает объект GeneralTransform, описывающий отношения между TextBlock и Grid. Программа использует его для преобразования четырех углов TextBlock в Grid-координаты объекта Polygon, в результате чего TextBlock заключается в прямоугольник:

using Windows.Foundation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        bool storyboardPaused;

        public MainPage()
        {
            this.InitializeComponent();
        }

        protected override void OnTapped(TappedRoutedEventArgs args)
        {
            if (storyboardPaused)
            {
                storyboard.Resume();
                storyboardPaused = false;
                return;
            }

            GeneralTransform gtrs = txb.TransformToVisual(contentGrid);

            // Рисование синего многоугольника вокруг элемента
            polygon.Points.Clear();
            polygon.Points.Add(gtrs.TransformPoint(new Point(0, 0)));
            polygon.Points.Add(gtrs.TransformPoint(new Point(txb.ActualWidth, 0)));
            polygon.Points.Add(gtrs.TransformPoint(new Point(txb.ActualWidth, txb.ActualHeight)));
            polygon.Points.Add(gtrs.TransformPoint(new Point(0, txb.ActualHeight)));

            // Рисование красного ограничивающего прямоугольника
            path.Data = new RectangleGeometry
            {
                Rect = gtrs.TransformBounds(new Rect(new Point(0, 0), txb.DesiredSize))
            };

            storyboard.Pause();
            storyboardPaused = true;
            base.OnTapped(args);
        }
    }
}

Вызов TransformBounds возвращает другую информацию: ограничивающий прямоугольник с горизонтальными и вертикальными сторонами, в котором помещается элемент. Ограничивающий прямоугольник выводится красным цветом:

Анимированное перемещение элемента с получением его позиции

Ограничивающий прямоугольник легко вычисляется по максимальным и минимальным координатам X и Y четырех преобразованных углов, но иметь его в готовом виде все же удобнее.

Пройди тесты
Лучший чат для C# программистов