注册
web

学到了!Figma 原来是这样表示矩形的

大家好,我是前端西瓜哥。


今天我们来研究一下 Figma 是如何表示图形的,这里以矩形为切入点进行研究。


明白最简单的矩形的表示后,研究其他的图形就可以举一反三。


矩形的一般表达


如果让我设计一个矩形图形的物理属性,我会怎么设计?


我张口就来:x、y、width、height、rotation。


对一些简单的图形编辑操作,这些属性基本上是够用的,比如白板工具,如果你不考虑或者不希望图形可以翻转(flip) 的话。


Figma 需要考虑翻转的情况的,此外还有斜切的情况。


翻转的场景:


图片


还有斜切的场景,在选中多个图形然后缩放时有发生。


图片


这些表达光靠上面的几个属性是不够的,我们看看 Figma为了表达这些效果,是怎么去设计矩形的。


Figma 矩形物理属性


上篇文章我们用 Figma-To-JSON 成功解析了 fig 文件,借助这个工具,我们得到了矩形图形的属性。


与物理信息相关的属性如下:


{
  "size": {
    "x"100,
    "y"100
  },
  "transform": {
    "m00"1,
    "m01"3,
    "m02"5,
    "m10"2,
    "m11"4,
    "m12"6
  },
  // 省略其他无关属性
}


没有位置属性,这个属性默认是 (0, 0),实际它转移到 transform 的矩阵的位移子矩阵上了。


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.m02tansfrom.m12


import { Matrix } from "pixi.js";

const matrix = new Matrix(123456);
const topLeft = matrix.apply({ x0y0 }); // { x: 5, y: 6 }

// 或直接点
const topLeft = { x5y6 }


这里引入了 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(123456);
const topLeft = { x5y6 }

const topRight = matrix.apply({ x100y0 });
distance(topRight, topLeft); // 223.60679774997897

最后计算出 223.60679774997897,四舍五入得到 223.61。


高度计算同理。


uiHeight 为 (0, 0)(0, height)  进行矩阵变换后坐标点之间的距离。


const matrix = new Matrix(123456);
const topLeft = { x5y6 }

const bottomLeft = matrix.apply({ x0y100 });
distance(bottomLeft, topLeft); // 500

旋转角度


最后是旋转角度,它是宽度对应的矩形边向量,逆时针旋转 90 度的向量所对应的角度。


图片


先计算宽边向量,然后逆时针旋转 90 度得到旋转向量,最后计算旋转向量对应的角度。


const wSideVec = { x: topRight.x - topLeft.xy: topRight.y - topLeft.y };
// 逆时针旋转 90 度,得到旋转向量
const rotationMatrix = new Matrix(0, -11000);
const rotationVec = rotationMatrix.apply(wSideVec);
const rad = calcVectorRadian(rotationVec);
const deg = rad2Deg(rad); // -63.43494882292201

这里用了几个工具函数。


// 计算和 (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:


codepen.io/F-star/pen/…


代码实现:


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.m02y: transform.m12 };
  console.log("x:", topLeft.x)
  console.log("y:", topLeft.y)

  const topRight = matrix.apply({ x: width, y0 });
  console.log("width:"distance(topRight, topLeft)); // 223.60679774997897

  const bottomLeft = matrix.apply({ x0y: height });
  console.log("height:"distance(bottomLeft, topLeft)); // 500

  const wSideVec = { x: topRight.x - topLeft.xy: topRight.y - topLeft.y };
  // 逆时针旋转 90 度,得到旋转向量
  const rotationMatrix = new Matrix(0, -11000);
  const rotationVec = rotationMatrix.apply(wSideVec);

  const rad = calcVectorRadian(rotationVec);
  const deg = rad2Deg(rad);
  console.log("rotation:", deg); // -63.43494882292201
};

getAttrs(
  // 宽高
  { x100y100 },
  // 变换矩阵
  {
    m001,
    m013,
    m025,
    m102,
    m114,
    m126,
  }
);


运行一下,结果和属性面板一致。


图片


结尾


Figma 只用宽高和变换矩阵来表达矩形,在数据层可以用精简的数据表达丰富的变形,此外在渲染的时候也能将矩阵运算交给 GPU 进行并行运算,是不错的做法。


我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。




相关阅读,


计算机图形学:变换矩阵


什么?Figma 的 fig 文件格式居然解析出来了


求向量的角度


图形编辑器开发:属性显示与格式转换


作者:前端西瓜哥
来源:juejin.cn/post/7314488568969478154

0 个评论

要回复文章请先登录注册