深圳幻海软件技术有限公司 欢迎您!

图形编辑器:修改图形X、Y、Width、Height、Rotation

2023-02-28

大家好,我是前端西瓜哥。图形编辑器的一个需求,就是可以通过属性面板的输入框设置选中元素的属性值。项目地址,欢迎star:https://github.com/F-star/suika线上体验:https://blog.fstars.wang/app/suika/最终效果如下:元素对象的结构:复制in

大家好,我是前端西瓜哥。图形编辑器的一个需求,就是可以通过属性面板的输入框设置选中元素的属性值。

项目地址,欢迎 star:

https://github.com/F-star/suika

线上体验:

https://blog.fstars.wang/app/suika/

最终效果如下:

元素对象的结构:

interface IGraph {
  x: number;
  y: number;
  width: number;
  height: number;
  rotation: number; // 旋转角度,单位为弧度
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

设置 x / y

UI 界面显示上说的 x / y,指的是旋转后的 x(即 rotatedX / rotatedY)。

为什么不是对应真正的 x 和 y 呢?因为需要对应用户的视角。

开发者理解底层,理解一个图形是先有基本的物理信息(x、y、width、height),然后再做变换(旋转、缩放等)后得到新的坐标再进行绘制。

而用户看到的则是直观的绘制出来的图形,并希望图形的左上角坐标能够对上他设置的坐标。旋转前的 x 和 y 是无法直观体现在画布上的,用户也不会在意。

OK,先看看怎么修改 rotatedX。图形对象上没有 rotatedX 属性,本质还是要修改 x 值。

先看看 rotatedX 和 rotatedY 是怎么计算出来的,其实就是计算 x 和 y 基于图形的中点旋转后的结果:

// 对坐标做旋转
function transformRotate(x, y, radian, cx ,cy) {
  if (!radian) {
    return [x, y];
  }
  const cos = Math.cos(radian);
  const sin = Math.sin(radian);
  return [
    (x - cx) * cos - (y - cy) * sin + cx,
    (x - cx) * sin + (y - cy) * cos + cy,
  ];
}

// 计算旋转后的 x 和 y
const [rotatedX, rotatedY] = transformRotate(x, y, rotation, cx, cy);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

计算一个元素 rotatedX / rotatedY 的方法实现:

// 计算中点
function getRectCenterPoint({x, y, width, height}) {
  return [x + width / 2, y + height / 2];
}

// 计算 rotatedX / rotatedY
export function getElementRotatedXY(element) {
  const [cx, cy] = getRectCenterPoint(element);
  return transformRotate(element.x, element.y, element.rotation || 0, cx, cy);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

所以,设置新的 rotatedX,其实就是加上一个移动前后 rotatedX 的偏移值,将其加到 x 上就行了。

class Graph {
 // ...
  setRotatedX(rotatedX) {
    const [prevRotatedX] = getElementRotatedXY(this);
    const dx = rotatedX - prevRotatedX;
    this.x += dx;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

rotatedY 同理:

class Graph {
 // ...
  setRotatedY(rotatedY: number) {
    const [, prevRotatedY] = getElementRotatedXY(this);
    const dy = rotatedY - prevRotatedY;
    this.y += dy;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

设置 width / height

首先修改width 和 height。

但是这样会导致 rotatedX 和 rotatedY 发生偏移,我们需要修正一下。

修正方式有两种思路:

思路 1:计算修改 width 前后的 rotatedX / rotatedY 之间的差值,给元素进行修正。

const [preRotatedX, preRotatedY] = getElementRotatedXY(el); // 修改 width 前的
el.width = width;
const [rotatedX, rotatedY] = getElementRotatedXY(el); // 修改 width 后的
const dx = rotatedX - preRotatedX;
const dy = rotatedY - preRotatedY;
el.x -= dx; // "-" 是因为要复原状态
el.y -= dy;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

思路 2:确定后最终的 rotatedX / rotatedY,然后对之前的 transformRotate 方法中的等式,进行逆推导,通过  rotatedX、rotatedY、radian、width、height 计算出对应的 x 和 y。这个思路比上一个思路有点复杂。

const [rotatedX, rotatedY] = getElementRotatedXY(el);
el.width = width;
const [x, y] = getOriginXY(
  rotatedX,
  rotatedY,
  el.rotation || 0,
  width,
  el.height
);
el.x = x;
el.y = y;

/**
 * 计算旋转前的 x、y
 * transformRotate 的反推
 */
function getOriginXY(rotatedX, rotatedY, radian, width, height) {
  if (!radian) {
    return [rotatedX, rotatedY];
  }
  const cos = Math.cos(radian);
  const sin = Math.sin(radian);
  const halfWidth = width / 2;
  const halfHeight = height / 2;
  return [
    rotatedX - halfWidth - halfHeight * sin + halfWidth * cos,
    rotatedY - halfHeight + halfHeight * cos + halfWidth * sin,
  ];
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

我一开始用的思路 2 实现的,后面写这篇文章梳理时,相处了思路 1 的解法,因为更简单更好理解,就换成思路 1 的实现了。

修改 rotation

修改 rotation 就很简单了,直接改就好了。

但需要注意将度数转成弧度,以及通过取余来限定弧度范围。

// 角度转弧度
function degree2Radian(degree: number) {
  return (degree * Math.PI) / 180;
}

/**
 * 标准化角度
 */
const PI_DOUBLE = 2 * Math.PI;
export const normalizeAngle = (angle) => {
  return angle % PI_DOUBLE;
};


element.rotation = normalizeAngle(degree2Radian(rotation));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

结尾

算法实现上并不复杂。