Sometimes, it would be nice to flatten (merge) multiple transforms into a single transform. Therefore, I thought it would be fun to figure out how to do this and share it with others.
Posts in this series:
A transform consists of the following operations, performed in the following order:
- Translate
- Rotate about a center point
- Scale
Note: This task is difficult enough when flattening transforms with center points of 0,0. Having to account for transformations with center points not at the origin is just too much. Therefore, it is assumed that all transformations have their center points zeroed out as described in Zeroing the Center of a CompositeTransform.
In the above render, the red rectangle is transformed Tx=100
, Ty=30
, and R=45
. Remember that all center points are 0,0
. The green rectangle is positioned using a group of two transforms, Tx=100
, Ty=30
, R=45
and then Tx=120
, Ty=-80
, and R=-60
. The goal of this post is to come up with an equivalent single transformation that results in the same location as the green rectangle. The answer is the transformation Tx=241
, Ty=58
and R=-15
, and rendered as the blue border.
FlattenExample.xaml
<Page
x:Class="PanViewDemoApp.FlattenExample"
IsTabStop="false"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:PanViewDemoApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
FontSize="20"
Foreground="Black">
<Canvas>
<Border
Height="120"
Width="200"
Background="#40FF0000">
<Border.RenderTransform>
<CompositeTransform
x:Name="J"
TranslateX="100"
TranslateY="30"
Rotation="45" />
</Border.RenderTransform>
<TextBlock
Foreground="Red"
Margin="5">Tx=100 Ty=30 R=45</TextBlock>
</Border>
<Border
Height="120"
Width="200"
Background="#4000FF00">
<Border.RenderTransform>
<TransformGroup>
<CompositeTransform
x:Name="K"
TranslateX="120"
TranslateY="-80"
Rotation="-60" />
<CompositeTransform
TranslateX="100"
TranslateY="30"
Rotation="45" />
</TransformGroup>
</Border.RenderTransform>
<TextBlock
Foreground="Green"
Margin="3">Tx=100
Ty=30 R=45<LineBreak />Tx=120 Ty=-80 R=-60</TextBlock>
</Border>
<Border
Height="120"
Width="200"
BorderBrush="Blue"
BorderThickness="3">
<Border.RenderTransform>
<CompositeTransform
x:Name="L"
TranslateX="241"
TranslateY="58"
Rotation="-15" />
</Border.RenderTransform>
<TextBlock
Foreground="Blue"
Margin="3"
VerticalAlignment="Bottom">Tx=241 Ty=58 R=-15</TextBlock>
</Border>
</Canvas>
</Page>
Above is the XAML that created the rendering.
From my perspective, the order of transforms specified in the TransformGroup
are applied in reverse order. I have no idea whether this is a bug or by design. In any case, we have to work with what we have.
Now that we named the transformations, let me restate the goal of this post in different terms: Our goal is to modify the transform J
so that it is equivalent to the group of the transformations J
and K
.
<CompositeTransform
x:Name="J"
TranslateX="100"
TranslateY="30"
Rotation="45" />
<CompositeTransform
x:Name="K"
TranslateX="120"
TranslateY="-80"
Rotation="-60" />
The easy part is the rotation. Rotations are simply additive. Therefore:
j.Rotation += k.Rotation;
The harder part is applying the translation, since translations in the x
and y
direction of transform K
are now rotated 45° (because transform J
has already been applied). The easiest way to apply the translation is to first convert the translations of K
from rectangular (x,y)
to polar form. In this example, moving 120x,-80y
is the same as moving 144.2
pixels in a direction of -33°
. Therefore, we need to translate 144.2
pixels in a direction of (45-33)°
or 12°
.
By the way, it is important to note that a move in the positive y
direction is upwards in trigonometry and downward in Windows. A rotation in a positive direction is counter-clockwise in trigonometry and clockwise in Windows. Therefore. it is easy to make direction mistakes when using trigonometry functions in Windows. Yet, these two inconsistencies can actually cancel each other out very nicely. Just keep it in mind when reading the code below. Oh, and one more thing: rotations in Windows are specified in degrees and angles used in trigonometric functions are all specified in radians.
The first number, 144.2
, is computed as:
var d = Math.Sqrt(k.TranslateX * k.TranslateX + k.TranslateY * k.TranslateY);
The second number, -33°
, is computed as:
var a = Math.Atan2(k.TranslateY, k.TranslateX);
We now need to add 45°
to the angle, making sure to convert 45°
to radians first.
a += j.Rotation *
Math.PI / 180;
Now that we have the final angle for K
, we need to break it back down into screen x
and y
distances for K
:
var x = Math.Cos(a) * d;
var y = Math.Sin(a) * d;
Finally, we can add those translations back into J:
j.TranslateX += x;
j.TranslateY += y;
Here is the C# code that performs the flattening of two transformations:
TransformExtensions.cs
static void InternalAppend(ICompositeTransform j, ICompositeTransform k)
{
InternalZeroCenterPoint(j);
InternalZeroCenterPoint(k);
var d = Math.Sqrt(k.TranslateX * k.TranslateX + k.TranslateY * k.TranslateY);
var a = Math.Atan2(k.TranslateY, k.TranslateX);
a += j.Rotation * Math.PI / 180;
var x = Math.Cos(a) * d;
var y = Math.Sin(a) * d;
j.Rotation += k.Rotation;
j.TranslateX += x;
j.TranslateY += y;
}