# 从 Matrix 解构出 Translate/Scale/Rotate（平移/缩放/旋转）

### 准备工作

```<Border x:Name="DisplayShape" Background="#FF1B6CB0" Width="200" Height="100">
<UIElement.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="0.8" ScaleY="2"/>
<SkewTransform/>
<RotateTransform Angle="-48.366"/>
<TranslateTransform x:Name="TranslateTransform" X="85" Y="160"/>
</TransformGroup>
</UIElement.RenderTransform>
<TextBlock Foreground="{media:LuminanceForeground TargetName=DisplayShape}" Text="walterlv" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>```

`LuminanceForeground` 的作用可参见我的另一篇文章：计算能在任何背景色上清晰显示的前景色

▲ 一个随便应用了一个变换的控件

`<Rectangle x:Name="TraceShape" Width="200" Height="100" Stroke="#FFE2620A" StrokeThickness="4"/>`

▲ 我们希望计算一组变换分量以便让这个框追踪变换了的控件

```private void OnLoaded(object sender, RoutedEventArgs args)
{
var matrix = DisplayShape.RenderTransform.Value;
var (scaling, rotation, translation) = ExtractMatrix(matrix);
var group = new TransformGroup();
group.Children.Add(new ScaleTransform {ScaleX = scaling.X, ScaleY = scaling.Y});
group.Children.Add(new RotateTransform {Angle = rotation});
group.Children.Add(new TranslateTransform {X = translation.X, Y = translation.Y});
TraceShape.RenderTransform = group;
}

private (Vector Scaling, double Rotation, Vector Translation) ExtractMatrix(Matrix matrix)
{
// 我们希望在这里写出一个方法，以便得到三个变换分量。
}```

`OnLoaded` 是为了让代码运行起来，而 `ExtractMatrix` 才是我们的核心——将变换分量解构出来。

### 思路和初步成果

```private (Vector Scaling, double Rotation, Vector Translation) ExtractMatrix(Matrix matrix)
{
var unitPoints = new[] {new Point(0, 0), new Point(1, 0), new Point(1, 1), new Point(0, 1)};
var transformedPoints = unitPoints.Select(matrix.Transform).ToArray();
var scaling = new Vector(
(transformedPoints[1] - transformedPoints[0]).Length,
(transformedPoints[3] - transformedPoints[0]).Length);
var rotation = Vector.AngleBetween(new Vector(1, 0), transformedPoints[1] - transformedPoints[0]);
var translation = transformedPoints[0] - unitPoints[0];
return (scaling, rotation, translation);
}```

▲ 追踪框完全贴合

### 可以灵活应用计算结果

```private void OnLoaded(object sender, RoutedEventArgs args)
{
var matrix = DisplayShape.RenderTransform.Value;
var (scaling, rotation, translation) = ExtractMatrix(matrix);
var group = new TransformGroup();
TraceShape.Width = DisplayShape.ActualWidth * scaling.X;
TraceShape.Height = DisplayShape.ActualHeight * scaling.Y;
group.Children.Add(new RotateTransform {Angle = rotation});
group.Children.Add(new TranslateTransform {X = translation.X, Y = translation.Y});
TraceShape.RenderTransform = group;
}```

▲ 没有被拉伸的追踪框

### 更通用的方法

• 变换中心不是 `(0, 0)`
• 最终应用的顺序不是 `Scale`->`Rotate`->`Translate`

`<Rectangle x:Name="TraceShape" Width="200" Height="100" Stroke="#FFE2620A" StrokeThickness="4" RenderTransformOrigin="0.5 0.5"/>`

▲ 改变了变换中心

SRT=M

T=S^{-1}R^{-1}M

```public static (Vector Scaling, double Rotation, Vector Translation) MatrixToGroup(Matrix matrix, CenterSpecification specifyCenter = null)
{
// 生成一个单位矩形（0, 0, 1, 1），计算单位矩形经矩阵变换后形成的带旋转的矩形。
// 于是，我们将可以通过比较这两个矩形中点的数据来求出一个解。
var unitPoints = new[] {new Point(0, 0), new Point(1, 0), new Point(1, 1), new Point(0, 1)};
var transformedPoints = unitPoints.Select(matrix.Transform).ToArray();

// 测试单位矩形宽高的长度变化量，以求出缩放比（作为参数 specifyCenter 中变换中心的计算参考）。
var scaling = new Vector((transformedPoints[1] - transformedPoints[0]).Length, (transformedPoints[3] - transformedPoints[0]).Length);
// 测试单位向量的旋转变化量，以求出旋转角度。
var rotation = Vector.AngleBetween(new Vector(1, 0), transformedPoints[1] - transformedPoints[0]);
var translation = transformedPoints[0] - unitPoints[0];

// 如果指定了变换分量的变换中心点。
if (specifyCenter != null)
{
// 那么，就获取指定的变换中心点（缩放中心和旋转中心）。
var (scalingCenter, rotationCenter) = specifyCenter(scaling);

// 如果 S 表示所求变换的缩放分量，R 表示所求变换的旋转分量，T 表示所求变换的平移分量；M 表示传入的目标矩阵。
// 那么，S 将可以通过缩放比和参数指定的缩放中心唯一确定；R 将可以通过旋转角度和参数指定的旋转中心唯一确定。
// S = scaleMatrix; R = rotateMatrix.
var scaleMatrix = Matrix.Identity;
scaleMatrix.ScaleAt(scaling.X, scaling.Y, scalingCenter.X, scalingCenter.Y);
var rotateMatrix = Matrix.Identity;
rotateMatrix.RotateAt(rotation, rotationCenter.X, rotationCenter.Y);

// T 是不确定的，它会受到 S 和 T 的影响；但确定等式 SRT=M，即 T=S^{-1}R^{-1}M。
// T = translateMatrix; M = matrix.
scaleMatrix.Invert();
rotateMatrix.Invert();
var translateMatrix = Matrix.Multiply(rotateMatrix, scaleMatrix);
translateMatrix = Matrix.Multiply(translateMatrix, matrix);

// 用考虑了变换中心的平移量覆盖总的平移分量。
translation = new Vector(translateMatrix.OffsetX, translateMatrix.OffsetY);
}

// 按缩放、旋转、平移来返回变换分量。
return (scaling, rotation, translation);
}```

```/// <summary>
/// 为 <see cref="MatrixToGroup"/> 方法提供变换中心的指定方法。
/// </summary>
/// <param name="scalingFactor">先进行缩放后进行旋转时，旋转中心的计算可能需要考虑前面缩放后的坐标。此参数可以得知缩放比。</param>
/// <returns>绝对坐标的缩放中心和旋转中心。</returns>
public delegate (Point ScalingCenter, Point RotationCenter) CenterSpecification(Vector scalingFactor);```

```private void OnLoaded(object sender, RoutedEventArgs args)
{
var matrix = DisplayShape.RenderTransform.Value;
var (scaling, rotation, translation) = TransformMatrix.MatrixToGroup(matrix,
scalingFactor => (new Point(), new Point(
DisplayShape.ActualWidth * scalingFactor.X / 2,
DisplayShape.ActualHeight * scalingFactor.Y / 2)));
TraceShape.RenderTransform = ScaleAtZeroRotateAtCenter(scaling, rotation, translation, DisplayShape.RenderSize, TraceShape.RenderTransformOrigin);
}

public static TransformGroup ScaleAtZeroRotateAtCenter(Vector scaling, double rotation, Vector translation, Size originalSize, Point renderTransformOrigin = default(Point))
{
var group = new TransformGroup();
var scaleTransform = new ScaleTransform
{
ScaleX = scaling.X,
ScaleY = scaling.Y,
CenterX = -originalSize.Width * renderTransformOrigin.X,
CenterY = -originalSize.Height * renderTransformOrigin.Y,
};
var rotateTransform = new RotateTransform
{
Angle = rotation,
CenterX = originalSize.Width * (scaling.X / 2 - renderTransformOrigin.X),
CenterY = originalSize.Height * (scaling.Y / 2 - renderTransformOrigin.Y),
};
group.Children.Add(new TranslateTransform {X = translation.X, Y = translation.Y});
return group;
}```

```private void OnLoaded(object sender, RoutedEventArgs args)
{
var matrix = DisplayShape.RenderTransform.Value;
var (scaling, rotation, translation) = TransformMatrix.MatrixToGroup(matrix,
scalingFactor => (new Point(), new Point(
DisplayShape.ActualWidth * scalingFactor.X / 2,
DisplayShape.ActualHeight * scalingFactor.Y / 2)));
TraceShape.Width = DisplayShape.ActualWidth * scaling.X;
TraceShape.Height = DisplayShape.ActualHeight * scaling.Y;
TraceShape.RenderTransform = NoScaleButRotateAtOrigin(
rotation, translation, DisplayShape.RenderSize);
}

public static TransformGroup NoScaleButRotateAtOrigin(double rotation, Vector translation, Size originalSize)
{
var group = new TransformGroup();
group.Children.Add(new RotateTransform {Angle = rotation});
group.Children.Add(new TranslateTransform {X = translation.X, Y = translation.Y});
return group;
}```

▲ 设置了 `RenderTransformOrigin` 依然有用

196 篇文章33 人订阅

0 条评论

## 相关文章

### 自定义Interpolator

nterpolator这个东西很难进行翻译，直译过来的话是补间器的意思，它的主要作用是可以控制动画的变化速率，比如去实现一种非线性运动的动画效果。那么什么叫做非...

22470

35840

### Shader、Draw Call和渲染管线（Rendering Pipeline）

《Real-Time Rendering, Third Edition》   （PDF的配图链接）将一个渲染流程分为三个阶段：

15840

20560

### 小蛇学python（9）matplotlib的基本使用

matplotlib作为python中可视化最经典的库，是个不得不学习的东西。尽管长江后浪推前浪，涌现出了很多更好的可视化库，比如Plotly。不过，它们几乎全...

19630

### 终于等到你——ggplot2树状图

2017年8月份的R语言更新包中，默默地加入了支持ggplot2树状图的新几何对象，从此在R语言中制作树状图，不用再求助于第三方包的辅助了。 该包既有Cran...

45660

20820

50530

23970

15320