# 教材位置：第6章，第6.3节；图 6-2。
# 单位、变量与形状：各变换矩阵均为 (2,2)；坐标使用统一长度单位，变换系数无量纲。
# 学习目标：把二维矩阵看作旋转、缩放、剪切等线性变换，
# 理解行列式的面积意义，以及矩阵复合时顺序为何重要。
# 核心数学关系：列向量变换为 y=Mx；若多个点按行存放，统一变换写作
# points @ M.T；面积放大倍数为 abs(det(M))，一般有 S@R != R@S。
# 数据与输出：对中心位于原点的正方形应用四种变换，打印行列式，
# 保存 e21_transforms.png；所有矩阵都是形状 (2,2)。
# 运行方式：安装 NumPy、Matplotlib 后，运行 python e21_transforms.py；
# 图片写入运行命令时的当前工作目录。

import numpy as np
import matplotlib.pyplot as plt
# 在创建第一幅图之前配置可跨系统选择的中文字体。
from plot_style import configure_chinese_plot

configure_chinese_plot()
# 三角函数使用弧度，因此先把 30 度转换为 pi/6。
theta = np.deg2rad(30)
# 标准二维旋转矩阵把列向量逆时针旋转 theta，保持长度和夹角不变。
R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
# diag 从一维元素生成对角矩阵：横坐标放大 2 倍，纵坐标缩小为 0.6 倍。
# 两方向伸缩不同，因此一般不保持角度，也不会与旋转矩阵交换。
S = np.diag([2.0, 0.6])
# 剪切给出 x'=x+0.8*y、y'=y；水平移动量随原来的高度 y 变化。
H = np.array([[1.0, 0.8], [0.0, 1.0]])
# eye(2) 是单位矩阵，对应“保持原图”；两个列表按相同顺序配对。
matrices = [np.eye(2), R, S, H]
names = ["恒等变换", "旋转变换", "缩放变换", "剪切变换"]
# 五行坐标包含四个顶点和重复的起点，确保轮廓闭合，形状为 (5,2)。
# 标量 0.5 广播到所有坐标，使方形中心从 (0.5,0.5) 平移到原点。
# 这一步是构造输入图形；后续 M 本身仍是关于原点的线性变换。
points = np.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]) - 0.5
# 列表推导对每个矩阵求行列式；round 仅用于让打印结果更易读。
# det 的绝对值是面积倍数，符号表示是否翻转方向；本例行列式都为正。
print("各变换矩阵的行列式 =", np.round([np.linalg.det(M) for M in matrices], 6))
# R.T 表示转置；R.T@R=I 验证旋转矩阵的列是单位正交向量。
assert np.allclose(R.T @ R, np.eye(2))
# 单位变换/旋转/剪切保持面积；缩放的面积倍数为 2*0.6=1.2。
assert np.allclose([np.linalg.det(M) for M in matrices], [1, 1, 1.2, 1])
# 在列向量约定下 S@R 表示先旋转后缩放；R@S 表示先缩放后旋转。
# 这两个结果不同，说明矩阵乘法通常不满足交换律。
assert not np.allclose(S @ R, R @ S)
fig, axes = plt.subplots(1, 4, figsize=(6.3, 2.8))
# zip 按位置把坐标轴、矩阵、名称组合起来，逐一绘制四种变换。
for ax, M, name in zip(axes, matrices, names):
    # 每个点存为一行 p，列向量公式 M@p 对应的行形式是 p@M.T。
    # (5,2)@(2,2) 得到 (5,2)，一次完成五个点的同一变换。
    transformed = points @ M.T
    # [:,0] 选所有点的横坐标，[:,1] 选纵坐标；黑色虚线作原图参照。
    ax.plot(points[:, 0], points[:, 1], "k--", alpha=0.4)
    # fill 把变换后的顶点连成填充多边形，透明度便于观察和原图的重叠。
    ax.fill(transformed[:, 0], transformed[:, 1], alpha=0.4)
    # 四个子图使用同样的范围，避免自动缩放掩盖真实伸缩差异。
    ax.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), title=name)
    # 坐标轴等比例显示，才能直观看出旋转的角度与形状是否保持。
    ax.set_aspect("equal")
    ax.grid(alpha=0.2)
# 保存 180 dpi 图片并关闭图，释放资源。
fig.tight_layout()
fig.savefig("e21_transforms.png", dpi=180, bbox_inches="tight")
plt.close(fig)
