4、二维缩放变换:关于原点的缩放,关于任意点的缩放

缩放变换,说白了就是把图形拉大或缩小。你想想看,在UI适配里,一个按钮从120x40变成60x20,这就是缩放。但这里有个坑——缩放中心不同,结果完全不同。

我个人习惯把缩放分成两类来理解:关于原点的缩放和关于任意点的缩放。前者是基础,后者是实战中真正用到的。

4.1 关于原点的缩放

这是最简单的缩放形式。假设有一个点P(x, y),我们想把它相对于原点(0,0)缩放s倍。新坐标就是:

x' = x * s
y' = y * s

写成矩阵形式更清爽:

[x']   [s  0] [x]
[y'] = [0  s] [y]

嗯,这里要注意:s是缩放因子。s > 1是放大,0 < s < 1是缩小。如果s是负数,那就是镜像翻转了——我在项目中遇到过,有次不小心传了个负值,整个UI图标都翻了个个儿,排查了半天才发现。

核心要点:关于原点的缩放,就是每个坐标分量乘以缩放因子。简单,但局限性很大——现实中很少以原点为缩放中心。

4.2 关于任意点的缩放

这才是实战中的常态。比如你要缩放一个按钮,通常是以按钮中心为基准,而不是以屏幕左上角。为什么?因为以中心缩放,按钮位置变化最小,用户体验最好。

关于任意点Pc(xc, yc)的缩放,步骤其实就三步:

  1. 平移:把Pc移到原点
  2. 缩放:关于原点缩放
  3. 平移回去:把原点移回Pc

写成矩阵连乘就是:

[x']   [1  0  xc] [s  0  0] [1  0  -xc] [x]
[y'] = [0  1  yc] [0  s  0] [0  1  -yc] [y]
[1 ]   [0  0  1 ] [0  0  1] [0  0   1 ] [1]

化简后得到:

x' = s * x + (1 - s) * xc
y' = s * y + (1 - s) * yc

我的经验:在实际编码中,我从来不直接套用这个公式。而是用矩阵连乘的方式写代码,这样后续如果要叠加旋转、平移,只需要在矩阵链里加一项就行,维护起来特别方便。

4.3 缩放变换的矩阵表示

用齐次坐标,缩放矩阵可以统一表示为:

// 关于原点的缩放矩阵
S(sx, sy) = | sx  0   0 |
            | 0   sy  0 |
            | 0   0   1 |

// 关于任意点Pc的缩放矩阵
S(Pc, s) = T(Pc) * S(s, s) * T(-Pc)

这里sx和sy可以不同,实现非均匀缩放。我在做嵌入式UI适配时,经常遇到屏幕分辨率不是正方形的情况,这时候就需要用非均匀缩放来适配。

我曾经踩过的坑:非均匀缩放时,如果sx和sy相差太大,图形会严重变形。比如一个圆形图标,sx=2, sy=1,就变成了椭圆。在UI设计中,除非你刻意追求这种效果,否则尽量保持sx = sy。

4.4 实战:UI适配中的缩放应用

举个具体的例子。假设你有一个设计稿,分辨率是1920x1080。现在要适配到1280x720的屏幕上。缩放因子怎么算?

sx = 1280 / 1920 ≈ 0.667
sy = 720 / 1080 ≈ 0.667

如果两个因子相等,那就是等比例缩放。如果不相等,就需要考虑是裁剪还是留黑边了。

我个人习惯的做法是:

  • 等比例缩放:取min(sx, sy),保证内容完整显示
  • 非等比例缩放:分别用sx和sy,但要注意UI元素不要变形

你想想看,一个按钮在1920屏幕上居中的,缩放后还能居中吗?这就要用到关于任意点的缩放了——以屏幕中心为缩放点,位置关系才能保持。

4.5 知识体系图

下面这张图展示了缩放变换的核心逻辑:

二维缩放变换知识体系 二维缩放变换 关于原点的缩放 关于任意点的缩放 公式:x' = x * s, y' = y * s 矩阵:S(s) = [[s,0],[0,s]] 三步法:平移→缩放→平移 矩阵:T(Pc)·S(s)·T(-Pc) 实战:UI适配中的等比例/非等比例缩放

4.6 代码实现示例

下面是我在嵌入式项目中常用的缩放函数,用C语言写的:

// 关于任意点缩放
void scale_around_point(float* x, float* y, float cx, float cy, float s) {
    // 平移至原点
    float tx = *x - cx;
    float ty = *y - cy;
    
    // 缩放
    tx *= s;
    ty *= s;
    
    // 平移回去
    *x = tx + cx;
    *y = ty + cy;
}

// 矩阵方式实现(推荐)
void scale_matrix(float* x, float* y, float cx, float cy, float s) {
    // 直接使用化简后的公式
    *x = s * (*x) + (1 - s) * cx;
    *y = s * (*y) + (1 - s) * cy;
}

我的建议:在性能敏感的嵌入式环境中,用化简后的公式直接计算,比矩阵连乘快得多。但如果你的代码需要频繁叠加多种变换,还是用矩阵方式更清晰,维护成本更低。

好了,关于二维缩放变换就聊这么多。记住一句话:缩放中心决定一切。搞清楚了原点缩放和任意点缩放的关系,你在做UI适配时就能游刃有余了。

可可精品资料库