大家好,我是前端西瓜哥。
前段时间对自己的图形编辑器项目做了一次改造。
改用 transform 表达图形的变形,并废弃掉了原来的 rotation、x、y 属性。
然后再补上了图形的翻转的支持,以及斜切的支持。图形的变形操作算是补完了。
这里我简单说说这么做的原因。
我正在开发的 suika 图形设计工具:
线上体验:
旋转、缩放和斜切
首先图形有基础的属性 x、y、width、height。
有些图形没有这 4 个属性,但其实也有,因为可以通过计算包围盒计算出来(比如贝塞尔曲线)。
然后我们要支持旋转,于是加了一个 rotation 属性(通常基于中心旋转。如果想随意指定,可以再补一个 pivot 属性指定旋转中心点)。
后来又要支持图形缩放。一般情况下,我们更改 width 和 height 就好了。
但有一种情况就不够用了,那就是 “翻转”,有两种情况:水平翻转和垂直翻转。
这时候我们就要引入缩放属性 scaleX 和 scaleY。
scaleX 如果是 1 表示不翻转,如果是 -1,表示水平翻转;scaleY 同理,不同的是它是垂直翻转。
如果都是 -1,那其实就是旋转了 180 度。
最后我们可能要 支持斜切 ,一般来说这种形变的情况是很少见的,甚至说有些编辑器极力避免这种情况的发生。
比如 Canva 图片编辑器会避免斜切的出现。如果同时缩放多个图形,图形只会改宽和高。
但如果一定要支持斜切(比如 Figma),我们只能上 transform 了。
虽说貌似可以补上一个 skewX 和 skewY 属性,但和 rotation 有一些冲突,后面会说为什么。
下面是 Figma 缩放多个图形的效果。
transform 矩阵
上面这些图形的变形属性,其实都可以用 transform 矩阵表示出来。或者叫模型矩阵。
变形矩阵用 6 个数值表示。
| a | c | tx|
| b | d | ty|
| 0 | 0 | 1 |
a 和 d 对应缩放值 scaleX、scaleY。
tx 和 ty 表示位移量,x 和 y 表示图形的位置。所以这里我把图形的 x 和 y 属性也丢掉了,默认为 (0, 0),放到 tx 和 ty 上了。
rotation 值如果对应旋转矩阵,可根据特性求。但 transfrom 不保证符合旋转矩阵的特征。
旋转矩阵其实是斜切中的特例。
所以还是不要太依赖旋转矩阵的特性。
计算 rotation,我们可以选择对一个基准方向的向量(比如 (1, 0)
),应用 transform 得到新向量,作为这个图形的方向向量,计算出对应的 rotation。
const getTransformAngle = (tf, angleBase = { x: 1, y: 0 }) => {
// 丢掉位移的影响,因为向量和点不同,位移后还是原来的向量
tf = new Matrix(tf.a, tf.b, tf.c, tf.d, 0, 0);
const angleVec = tf.apply(angleBase);
return getSweepAngle(angleBase, angleVec);
};
最后是 skewX、skewY,对应是 c 和 b。基本没有什么用。
transform 有很多好处,首先它是底层属性,所有渲染引擎(比如 SVG、Canvas 2D)都支持用矩阵对图形表示形变。
其次也方便做多个形变的复合运算。
比如对一个已经形变的图形做中心旋转,只要给原来的变形矩阵左乘一个 “位移-旋转-位移” 的复合矩阵就可以了。
const dRotate = (dRotation, originTf, center) => {
const rotateMatrix = new Matrix()
.translate(-center.x, -center.y)
.rotate(dRotation)
.translate(center.x, center.y);
// 记得要 “左乘” 新的矩阵
return rotateMatrix.append(originTf);
}
这个思路的方向,要比带着一堆 rotation、scaleX 乱七八糟的属性基于几何算法写出一套复杂的逻辑简单多了。当然前提是你得理解矩阵到底在干什么,这个是基础,建议你花时间弄懂。
最后
选择 transform 矩阵的一些优点:
-
它是更底层的表达,能够非常精炼地表达一个图形的形变(虽然一眼看过去不是很直观);
-
同时基于矩阵运算,也很方便计算二次形变结果,左乘一个新的变形矩阵即可;
-
更容易兼容其他的用了 transform 风格的图形数据,比如 SVG;
我是前端西瓜哥,关注我,学习更多图形编辑器知识。
相关阅读,