學到了!Figma 原來是這樣表示矩形的
大家好,我是前端西瓜哥。
今天我們來研究一下 Figma 是如何表示圖形的,這里以矩形為切入點進行研究。
明白最簡單的矩形的表示后,研究其他的圖形就可以舉一反三。
矩形的一般表達
如果讓我設計一個矩形圖形的物理屬性,我會怎么設計?
我張口就來:x、y、width、height、rotation。
對一些簡單的圖形編輯操作,這些屬性基本上是夠用的,比如白板工具,如果你不考慮或者不希望圖形可以翻轉(flip) 的話。
Figma 需要考慮翻轉的情況的,此外還有斜切的情況。
翻轉的場景:
還有斜切的場景,在選中多個圖形然后縮放時有發生。
這些表達光靠上面的幾個屬性是不夠的,我們看看 Figma為了表達這些效果,是怎么去設計矩形的。
Figma 矩形物理屬性
與物理信息相關的屬性如下:
{
"size": {
"x": 100,
"y": 100
},
"transform": {
"m00": 1,
"m01": 3,
"m02": 5,
"m10": 2,
"m11": 4,
"m12": 6
},
// 省略其他無關屬性
}
沒有位置屬性,這個屬性默認是 (0, 0),實際它轉移到 transform 的矩陣的位移子矩陣上了。
size 表示寬高,但屬性名用的是 x(寬) 和 y(高),理論上 width 和 height 語義更好,這樣應該是用了矢量類型。
size 表示寬高,理論上 width 和 height 語義更好,這樣應該是用了平面矢量類型的結構體,所以是 x 和 y。
transform 表示一個 3x3 的變換矩陣。
m00 | m01 | m02
m10 | m11 | m12
0 | 0 | 1
上面的 transform 屬性的值所對應的矩陣為:
1 | 3 | 5
2 | 4 | 6
0 | 0 | 1
屬性面板
再看看這些屬性對應的右側屬性面板。
x、y 分別是 5 和 6,它是 (0, 0) 進行 transform 后的結果,這個直接對應 transform.m02 和 tansfrom.m12。
import { Matrix } from "pixi.js";
const matrix = new Matrix(1, 2, 3, 4, 5, 6);
const topLeft = matrix.apply({ x: 0, y: 0 }); // { x: 5, y: 6 }
// 或直接點
const topLeft = { x: 5, y: 6 }
這里引入了 pixi.js 的 matrix 類,該類使用列向量方式進行表達。
文末有 demo 源碼以及線上 demo,可打開控制臺查看結果驗證正確性。
然后這里的 width 和 height,是 223.61 和 500, 怎么來的?
它們對應的是矩形的兩條邊變形后的長度,如下:
uiWidth 為 (0, 0) 和 (width, 0) 進行矩陣變換后坐標點之間的距離。
const distance = (p1, p2) => {
const a = p1.x - p2.x;
const b = p1.y - p2.y;
return Math.sqrt(a * a + b * b);
};
const matrix = new Matrix(1, 2, 3, 4, 5, 6);
const topLeft = { x: 5, y: 6 }
const topRight = matrix.apply({ x: 100, y: 0 });
distance(topRight, topLeft); // 223.60679774997897
最后計算出 223.60679774997897,四舍五入得到 223.61。
高度計算同理。
uiHeight 為 (0, 0) 和 (0, height) 進行矩陣變換后坐標點之間的距離。
const matrix = new Matrix(1, 2, 3, 4, 5, 6);
const topLeft = { x: 5, y: 6 }
const bottomLeft = matrix.apply({ x: 0, y: 100 });
distance(bottomLeft, topLeft); // 500
旋轉角度
最后是旋轉角度,它是寬度對應的矩形邊向量,逆時針旋轉 90 度的向量所對應的角度。
先計算寬邊向量,然后逆時針旋轉 90 度得到旋轉向量,最后計算旋轉向量對應的角度。
const wSideVec = { x: topRight.x - topLeft.x, y: topRight.y - topLeft.y };
// 逆時針旋轉 90 度,得到旋轉向量
const rotationMatrix = new Matrix(0, -1, 1, 0, 0, 0);
const rotationVec = rotationMatrix.apply(wSideVec);
const rad = calcVectorRadian(rotationVec);
const deg = rad2Deg(rad); //
這里用了幾個工具函數。
// 計算和 (0, -1) 的夾角
const calcVectorRadian = (vec) => {
const a = [vec.x, vec.y];
const b = [0, -1]; // 這個是基準角度
// 使用點積公式計算夾腳
const dotProduct = a[0] * b[0] + a[1] * b[1];
const d =
Math.sqrt(a[0] * a[0] + a[1] * a[1]) * Math.sqrt(b[0] * b[0] + b[1] * b[1]);
let rad = Math.acos(dotProduct / d);
if (vec.x > 0) {
// 如果 x > 0, 則 rad 轉為 (-PI, 0) 之間的值
rad = -rad;
}
return rad;
}
// 弧度轉角度
const rad2Deg = (rad) => (rad * 180) / Math.PI;
Figma 的角度表示比較別扭。
特征為:基準角度朝上,對應向量為 (0, -1),角度方向為逆時針,角度范圍限定為 (-180, 180]
,計算向量角度時要注意這個特征進行調整。
完整代碼實現
線上 demo:
https://codepen.io/F-star/pen/WNPVWwQ?editors=0012。
代碼實現:
import { Matrix } from "pixi.js";
// 計算和 (0, -1) 的夾角
const calcVectorRadian = (vec) => {
const a = [vec.x, vec.y];
const b = [0, -1];
const dotProduct = a[0] * b[0] + a[1] * b[1];
const d =
Math.sqrt(a[0] * a[0] + a[1] * a[1]) * Math.sqrt(b[0] * b[0] + b[1] * b[1]);
let rad = Math.acos(dotProduct / d);
if (vec.x > 0) {
// 如果 x > 0, 則 rad 為 (-PI, 0) 之間的值
rad = -rad;
}
return rad;
}
// 弧度轉角度
const rad2Deg = (rad) => (rad * 180) / Math.PI;
const distance = (p1, p2) => {
const a = p1.x - p2.x;
const b = p1.y - p2.y;
return Math.sqrt(a * a + b * b);
};
const getAttrs = (size, transform) => {
const width = size.x;
const height = size.y;
const matrix = new Matrix(
transform.m00, // 1
transform.m10, // 2
transform.m01, // 3
transform.m11, // 4
transform.m02, // 5
transform.m12 // 6
);
const topLeft = { x: transform.m02, y: transform.m12 };
console.log("x:", topLeft.x)
console.log("y:", topLeft.y)
const topRight = matrix.apply({ x: width, y: 0 });
console.log("width:", distance(topRight, topLeft)); // 223.60679774997897
const bottomLeft = matrix.apply({ x: 0, y: height });
console.log("height:", distance(bottomLeft, topLeft)); // 500
const wSideVec = { x: topRight.x - topLeft.x, y: topRight.y - topLeft.y };
// 逆時針旋轉 90 度,得到旋轉向量
const rotationMatrix = new Matrix(0, -1, 1, 0, 0, 0);
const rotationVec = rotationMatrix.apply(wSideVec);
const rad = calcVectorRadian(rotationVec);
const deg = rad2Deg(rad);
console.log("rotation:", deg); // -63.43494882292201
};
getAttrs(
// 寬高
{ x: 100, y: 100 },
// 變換矩陣
{
m00: 1,
m01: 3,
m02: 5,
m10: 2,
m11: 4,
m12: 6,
}
);
運行一下,結果和屬性面板一致。
結尾
Figma 只用寬高和變換矩陣來表達矩形,在數據層可以用精簡的數據表達豐富的變形,此外在渲染的時候也能將矩陣運算交給 GPU 進行并行運算,是不錯的做法。