Математические расчеты преобразований в WinRT

98

Ранее я говорил, что преобразование представляет результат применения математической формулы, которая переводит точку (x, y) в (x', y'), ко всем точкам элемента. Пришло время познакомиться с математическими вычислениями, лежащими в основе преобразований.

Допустим, свойствам X и Y объекта TranslateTransform заданы значения TX и TY. В формуле преобразования сдвига эти значения прибавляются к x и y:

Если свойствам ScaleX и ScaleY объекта ScaleTransform заданы значения SX и SY, формулы преобразования также достаточно очевидны:

Итак, разобравшись с основами, можно браться за объединение преобразований - например, в группы TransformGroup. Если преобразование ScaleTransform выполняется первым, а за ним следует TranslateTransform, формулы будут выглядеть так:

Но если первым применяется преобразование сдвига, а масштабирование выполняется после него, результат получается несколько иным:

Величина сдвига умножается на коэффициент масштабирования. Класс ScaleTransform определяет не только свойства ScaleX и ScaleY, но и свойства CenterX и CenterY. Ранее я уже говорил о том, как центральная точка используется для построения двух сдвигов. Первый сдвиг выполняется с отрицательной величиной, затем выполняется масштабирование или поворот и после него следует положительный сдвиг. Допустим, свойствам CenterX и CenterY присваиваются значения CX и CY. Формулы масштабирования принимают вид:

Как нетрудно убедиться, точка (CX, CY) преобразуется в точку (CX, CY), а этой характеристикой обладает центр вращения: точка, которую преобразование оставляет неизменной.

Во всех рассмотренных случаях точка x' вычислялась умножением и сложением констант с x, а точка y'- умножением и сложением констант с y. С поворотами ситуация усложняется, потому что как x', так и y' начинает зависеть от x и y. Если свойство Angle объекта RotateTransform равно A, формулы преобразования выглядят так:

Формулы легко проверяются для простых случаев. Если А равно нулю, то от формул остается:

Если А равно 90°, то синус равен 1, косинус равен 0, соответственно:

Например, точка (1,0) преобразуется в (0,1), а точка (0,1) преобразуется в (-1,0). Преобразование представляет собой отражение относительно начала координат, а того же эффекта можно добиться с преобразованием ScaleTransform, у которого оба свойства ScaleX и ScaleY равны -1. Для A = 270°:

Первая фаза приведенного ранее преобразования отклонения выглядит так:

Использование искажения

Формулы этого конкретного преобразования (свойство AngleX равно 45°):

Для значений y, равных 0 (верхний край фигуры), значение x' просто равно x, а значение y' равно y. Но с перемещением вниз по фигуре значения y увеличиваются, поэтому значения x' становятся все больше x. Обобщенные формулы преобразования SkewTransform, у которого свойство AngleX равно AX, а свойство AngleY равно AY, выглядят так:

Когда вы начинаете анализировать комбинации поворотов с другими преобразованиями, такая запись становится слишком громоздкой. К счастью, на помощь приходит матричная алгебра. Если отдельные преобразования выражаются в виде матриц, объединенные преобразования могут вычисляться посредством хорошо известного процесса матричного умножения.

Пусть точка (x, y) выражается матрицей 2x1:

Преобразование описывается матрицей 2x2:

Применение преобразования в этом случае может быть выражено посредством умножения матриц. Результат умножения представляет собой преобразованную точку:

Умножение матриц выполняется по следующим формулам:

Формула также работает для масштабирования, если свойство ScaleX равно M11, свойство ScaleY равно M22, а M21 и M12 равны нулю. Она подойдет и для поворотов с отклонениями, так как обе операции используют коэффициенты, умножаемые на x и y.

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

Величина сдвига прибавляется сама по себе, без умножения на x или y. Как представить обобщенное преобразование в виде матрицы, если она не поддерживает сдвиг - пожалуй, самый простой из всех видов преобразований?

Интересное решение задачи основано на введении третьего измерения. Кроме осей X и Y в плоскости экрана монитора добавляется концептуальная ось Z, выходящая из экрана. Предположим, графический вывод осуществляется на двумерной плоскости, но эта плоскость существует в трехмерном пространстве с постоянной координатой Z, равной 1. Таким образом, точка (x, y) в действительности является точкой (x, y, 1), и для ее представления может использоваться матрица 3x1:

Теперь преобразование описывается матрицей 3x3, а умножение выглядит так:

А формулы матричного умножения приходят к виду:

Переход можно считать отчасти успешным, потому что формулы преобразования теперь включают величины сдвига M31 и M32. Эти два числа не умножаются ни на x, ни на y. Однако назвать успех полным все же нельзя, потому что величина z' обычно не равна 1, а это значит, что мы выходим за пределы плоскости, в которой координата z всегда равна 1. Для возвращения к плоскости достаточно задать «лишним» z' значение 1. Но разве точки, находящиеся на большом расстоянии от плоскости z = 1, не должны отличаться от точек, расположенных поблизости?

Один умный способ присвоить z значение 1 основан на делении всех трех координат результирующей матрицы 3x1 на z':

Способ представления двумерных преобразований в трехмерных координатах - так называемые однородные координаты - был разработан Августом Мебиусом в 1820-х годах для представления бесконечности, которая образуется при z' = 0. Но для нас бесконечные координаты создают проблему, и если мы хотим избежать их, значение z' не должно быть равно нулю. Более того, можно полностью избежать деления на z' - достаточно принять меры к тому, чтобы z' всегда было равно 1.

Для этого можно присвоить элементам матрицы M13 и M23 значение 0, а M33 - значение 1. Теперь преобразование представляется формулами, которые остаются полностью в одной плоскости:

Это стандартное матричное представление двумерного аффинного преобразования. (С любыми другими значениями в третьем столбце преобразование будет неаффинным.)

С записью, использовавшейся ранее, преобразование ScaleTransform, у которого свойству ScaleX задано значение SX, a ScaleY - значение SY, выглядит так:

Преобразование TranslateTransform с коэффициентами TX и TY:

Преобразование ScaleTransfom с центром (CX, CY) фактически представляет собой умножение трех преобразований 3*3:

Аналогичным образом преобразование RotateTransform с углом А и центром (CX, CY) также формируется из трех преобразований:

Осталось преобразование SkewTransform с углами AX и AY и центром:

Как известно, матричное умножение не обладает свойством коммутативности. Порядок умножения влияет на результат операции. Это обстоятельство уже было продемонстрировано ранее на примере сдвига или масштабирования. Если сначала выполняется сдвиг, величины сдвига также масштабируются.

Впрочем, некоторые виды преобразований можно безопасно выполнять в любом порядке:

Кроме того, если преобразование ScaleTransform выполняется с одинаковыми свойствами ScaleX и ScaleY, его можно умножить на RotateTransform или SkewTransform в любом порядке.

В Windows Runtime определяется структура Matrix с шестью свойствами, соответствующими ячейкам матрицы:

Последняя строка матрицы содержит фиксированные значения. Структура Matrix не может использоваться для определения неаффинных преобразований или чего-либо более экзотичного, чем то, что мы уже видели. OffsetX и OffsetY - свойства преобразования.

По умолчанию элементы M11 и M22 содержат 1, а значения по умолчанию остальных четырех свойств равны 0. Результат представляет собой матрицу тождественного преобразования (единичную матрицу):

Структура Matrix имеет статическое свойство Identity, которое возвращает это значение, и свойство IsIdentity, которое возвращает true, если значение Matrix является тождественной матрицей.

Наряду с «простыми» потомками Transform - такими, как ScaleTransform и RotateTransform - также существует низкоуровневая альтернатива MatrixTransform со свойством типа Matrix. Если матричное преобразование вам известно, его можно задать непосредственно в виде шести чисел в порядке M11, M12, M21, M22, OffsetX, OffsetY. Один из способов задания преобразования выглядит так:

<TextBlock ...>
    <TextBlock.RenderTransform>
         <TransformGroup>
               <MatrixTransform Matrix="5 2 0 5 0 90" />
         </TransformGroup>
    </TextBlock.RenderTransform>
</TextBlock>

Это преобразование осуществляет масштабирование по горизонтали с коэффициентом 5 (M11) и по вертикали с коэффициентом 5 (M22), после чего сдвигает TextBlock вниз на 90 пикселов (OffsetY). Однако преобразование также можно задать непосредственно в свойстве RenderTransform:

<TextBlock ... RenderTransform="5 2 0 5 0 90" />

Microsoft Visual Studio без восторга относится к этому синтаксису, но он не создает проблем для компилятора или Windows 8.

Неявная форма MatrixTransform хорошо подходит для выполнения нескольких последовательных преобразований поворота, как показано в следующей программе. В каждом элементе TextBlock выводится преобразование, примененное к нему:

<Page ...>

    <Page.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="24" />
            <Setter Property="RenderTransformOrigin" Value="0 0.5" />
            <Setter Property="Foreground" Value="LimeGreen" />
        </Style>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">

        <!-- Перемещение начала координат в центр -->
        <Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center">

            <TextBlock Text="  RenderTransform='1 0 0 1 0 0'" 
                       RenderTransform="1 0 0 1 0 0" />

            <TextBlock Text="  RenderTransform='.7 .7 -.7 .7 0 0'"
                       RenderTransform=".7 .7 -.7 .7 0 0" />

            <TextBlock Text="  RenderTransform='0 1 -1 0 0 0'"
                       RenderTransform="0 1 -1 0 0 0" />

            <TextBlock Text="  RenderTransform='-.7 .7 -.7 -.7 0 0"
                       RenderTransform="-.7 .7 -.7 -.7 0 0" />

            <TextBlock Text="  RenderTransform='-1 0 0 -1 0 0'" 
                       RenderTransform="-1 0 0 -1 0 0" />

            <TextBlock Text="  RenderTransform='-.7 -.7 .7 -.7 0 0'"
                       RenderTransform="-.7 -.7 .7 -.7 0 0" />

            <TextBlock Text="  RenderTransform='0 -1 1 0 0 0'"
                       RenderTransform="0 -1 1 0 0 0" />

            <TextBlock Text="  RenderTransform='.7 -.7 .7 .7 0 0"
                       RenderTransform=".7 -.7 .7 .7 0 0" />
        </Canvas>
    </Grid>
</Page>

Частые вхождения 0.7 следовало бы заменить значением 0.707 - синус и косинус 45° (половина квадратного корня из 2). С этими восемью преобразованиями каждый элемент TextBlock поворачивается на дополнительные 45° по отношению к предыдущему:

Пример использования матричных преобразований

Если вы работаете в программном коде, структура Matrix содержит метод Transform(), который применяет преобразование к значению Point и возвращает преобразованную точку Point.

Однако в структуре Matrix не хватает многих полезных функций. В частности, отсутствует оператор умножения, который бы позволил легко выполнить собственные умножения в коде. Вы можете написать код умножения самостоятельно или же использовать класс TransformGroup, который выполняет внутреннее умножение и предоставляет результат в виде доступного только для чтения свойства Value типа Matrix. Если вам потребуется выполнить матричное умножение, создайте TransformGroup в коде, добавьте пару инициализированных экземпляров классов, производных от Transform, и обратитесь к свойству Value.

Матричные операции играют исключительно важную роль в определении центров вращения и масштабирования при манипуляциях с объектами на сенсорном экране.

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