图形编辑器开发:实现缩放图形

大家好,我是前端西瓜哥,今天我们来实现图形的缩放。

本文只讨论缩放单个图形的情况。

编辑器 github 地址:

github.com/F-star/suik...

线上体验:

blog.fstars.wang/app/suika/

图形的属性

图形有几个重要的基础属性,会经常被用到,我们在实现缩放图形前需要理清一下它们。

  1. x / y

  2. width / height

  3. rotation

位置和大小

x 和 y 为图形的左上角位置,注意是旋转前的。

x、y 旋转后我们叫做 rotatedX、rotatedY,属性面板中会用到。

通过属性面板输入框修改属性:

图形编辑器:修改图形x、y、width、height、rotation

width 和 height 为图形的宽高,这个没什么好说的。

另外,有些图形有些特殊,它的 x、y、width、height 是要通过其他属性计算出来的,比如贝塞尔曲线。

旋转

rotation 为图形的旋转度数,通常使用 弧度单位

因为弧度是数学计算中的常客,各种 API 都是要求提供弧度的,比如内置的 Math.sin() 方法。

你存角度自然也是可以,但不推荐,但计算时多了一层多余的单位转换,且丢失一些微小的精度。

当然 UI 层还是要展示角度,因为是面向用户的,对于数据和 UI 不统一的问题,在 UI 层做一个转换即可。

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

旋转度数通常要配合一个变换中心(origin),这个可以作为一个属性让用户设置。

但我更建议将 x、y、width、height 形成的 矩形的中点 作为旋转中心,这样更简单一些,减少用户的心智负担,也防止出现用户设置一些奇怪 origin 的场景。

下图中,红色矩形是蓝色矩阵顺时针旋转 45 度得到。

旋转度数还要考虑 旋转方向基准角度取值范围 问题。

(因为弧度不直观,后面会用角度来描述,但数据层依旧还是用的弧度)

  • 旋转方向:设置旋转后,图形是会往顺时针方向还是逆时针方向旋转;

  • 基准角度:朝向哪里是 0 度;

  • 取值范围:通常为 [0, 360)(-180, 180]。二者其实等价,只是显示有区别,后者其实只是前者减去 180 度。

通常这些编辑器自己决定就好。像我的项目,向上表示 0 度,顺时针方向为旋转方向,方向取值为 [0, 360)

一些编辑器是支持用户自己设置的,比如 AutoCAD 可通过图形单位命令,设置旋转方向和基准角度。

缩放实现思路

进入正题,对图形进行缩放。

接下来会以通过右下角(也叫东南 se 方向) 缩放控制点缩放为例进行讲解。

交互逻辑:

选择工具下,当光标落在右下角的缩放控制点上时,光标会变成缩放样式(这个不是本文核心,不讲)。

此时按下鼠标,然后进行拖拽,即可对图形以左上角为缩放中心,进行缩放。

实现思路:更新 width 和 height,然后确定参照点,修正 x 和 y

按下鼠标时,我们要把当前图形的 x、y、width、height、rotation 记录下来。之后的缩放是基于这个初始状态进行的。

js 复制代码
const mousedown = (e) => {
  // ...
  
  // 缩放前图形的属性,之后我们会直接更新图形属性,导致原来的属性丢失,所以要记录下这个快照。
  prevElement = {
    x: item.x,
    y: item.y,
    width: item.width,
    height: item.height,
    rotation: item.rotation ?? 0,
  }
}

拖拽时,调用我们将要实现的 movePoint 方法,去更新这个图形。

js 复制代码
const drag = (e) = {
  // ...
  
  selectElement.movePoint(
    'se', // 缩放控制点类型:右下(或东南)
    lastPoint, // 当前光标位置(基于场景坐标系)
    prevElement, // 缩放前的属性快照
  );
}

下面就是核心方法 movePoint 的实现逻辑了。

更新 width 和 height

首先是更新矩形宽高。

因为有一个旋转,所以算法不会这么直观。

我们要意识到这里有一个变换。看到的图形,是做过变换(基于矩形中心旋转)之后的,但我们需要修改的 width、height、x、y 则是旋转前的。

所以我们需要把光标位置给旋转回来,然后再减去 x 和 y 去得到真正的 width 和 height。

看看代码

js 复制代码
class Graph {
  // ...

  // 根据缩放点更新图形
  movePoint(type, newPos, oldBox) {
    // 1. 计算 width 和 height
    // 计算缩放中心(也就是矩形的中点)
    const cx = oldBox.x + oldBox.width / 2;
    const cy = oldBox.y + oldBox.height / 2;

    // 计算反向旋转的光标位置
    const { x: posX, y: poxY } = transformRotate(
      newPos.x,
      newPos.y,
      -(oldBox.rotation || 0), // 注意这里是负数
      cx,
      cy
    );
    
    let width = 0;
    let height = 0;
    if (type === 'se') {
      // 参照点为左上角(x 和 y)
      // 新的宽高自然就是光标位置减去 x、y
      width = posX - oldBox.x;
      height = poxY - oldBox.y;
    }
    // 其他控制点的逻辑暂且省略...
    
    // 2. 计算 x 和 y
    // ...
  }
}

看看只更新宽高的效果。

可以看到是有问题的,因为修改宽高后,矩形的中心点也发生了变化,导致缩放中心错误。所以我们要修正一下 x 和 y。

修正 x 和 y

接着我们就要修正 x 和 y 的值。

重点就一句话:缩放前的参考点和缩放后的参考点的位置要保持一致。这个参考点其实就是图形缩放过程中的缩放中心。

对于右下角缩放控制点,它的缩放中心就是左上角,即 x 和 y 经过旋转的位置。

js 复制代码
class Graph {
  // ...

  movePoint(type, newPos, oldBox) {
    // 1. 计算 width 和 height
    // ...
    

    // 2. 计算 x 和 y

    // 设置参照点,不同缩放类型的参照点不同
    let prevOriginX = 0;
    let prevOriginY = 0;
    let originX = 0;
    let originY = 0;
    if (type === "se") {
      prevOriginX = oldBox.x;
      prevOriginY = oldBox.y;
      originX = oldBox.x;
      originY = oldBox.y;
    }
    // 其他缩放类型暂且省略

    // 缩放前的参考点位置
    const { x: prevRotatedOriginX, y: prevRotatedOriginY } = transformRotate(
      prevOriginX,
      prevOriginY,
      oldBox.rotation || 0,
      cx,
      cy
    );
    // 缩放后的参考点位置
    const { x: rotatedOriginX, y: rotatedOriginY } = transformRotate(
      originX,
      originY,
      oldBox.rotation || 0,
      oldBox.x + width / 2, // 旋转中心是新的
      oldBox.y + height / 2
    );
    // 计算新旧两个参考点的差值,对 x、y 进行补正
    const dx = rotatedOriginX - prevRotatedOriginX;
    const dy = rotatedOriginY - prevRotatedOriginY;
    const x = oldBox.x - dx;
    const y = oldBox.y - dy;
  }
}

width 和 height 可能为负数,这里要做一个标准化,然后赋值给图形属性即可。

js 复制代码
this.setAttrs(
  normalizeRect({
    x,
    y,
    width,
    height,
  }),
);

其他缩放控制点

对于其他类型缩放控制点,比如左上、右上、左下缩放控制点,它们的大框架是一样的,只是 width 和 height 计算方式不同,以及参考点不同。

不同类型下 width 和 height 的设置:

js 复制代码
let width = 0;
let height = 0;
if (type === 'se') { // 右下
  width = posX - oldBox.x;
  height = poxY - oldBox.y;
} else if (type === 'ne') { // 右上
  width = posX - oldBox.x;
  height = oldBox.y + oldBox.height - poxY;
} else if (type === 'nw') {
  width = oldBox.x + oldBox.width - posX;
  height = oldBox.y + oldBox.height - poxY;
} else if (type === 'sw') {
  width = oldBox.x + oldBox.width - posX;
  height = poxY - oldBox.y;
}

新旧参考点设置:

js 复制代码
let prevOriginX = 0;
let prevOriginY = 0;
let originX = 0;
let originY = 0;
if (type === 'se') {
  prevOriginX = oldBox.x; // 右下缩放点,参考点为左上角
  prevOriginY = oldBox.y;
  originX = oldBox.x;
  originY = oldBox.y;
} else if (type === 'ne') { // 右上缩放点,参考点为左下角
  prevOriginX = oldBox.x;
  prevOriginY = oldBox.y + oldBox.height;
  originX = oldBox.x;
  originY = oldBox.y + height;
} else if (type === 'nw') {
  prevOriginX = oldBox.x + oldBox.width;
  prevOriginY = oldBox.y + oldBox.height;
  originX = oldBox.x + width;
  originY = oldBox.y + height;
} else if (type === 'sw') {
  prevOriginX = oldBox.x + oldBox.width;
  prevOriginY = oldBox.y;
  originX = oldBox.x + width;
  originY = oldBox.y;
}

暂时没实现正北、正南、正西、正东的逻辑,逻辑大差不差。

锁定缩放比

按住 shift 可以锁定缩放比。

做法是对比新旧图形宽高比,将 width 和 height 其中一个进行修正即可。注意正负号。

方法需要多传一个 keepRatio 的参数:

js 复制代码
class Graph {
  // ...

  movePoint(type, newPos, oldBox, keepRatio = false) {
    // 1. 计算 width 和 height
    // ...
    
    if (keepRatio) {
      const ratio = oldBox.width / oldBox.height;
      const newRatio = Math.abs(width / height);
      if (newRatio > ratio) {
        height = (Math.sign(height) * Math.abs(width)) / ratio;
      } else {
        width = Math.sign(width) * Math.abs(height) * ratio;
      }
    }
    
    // 2. 计算 x 和 y
    // ...
  }
}

貌似没考虑除数 height 为 0 的情况..

优化点

本文的实现是考虑的是比较简单的缩放图形场景,一些更复杂的场景并未实现。

缩放还有另一种策略,就是会产生 反向颠倒 的缩放。要实现这个效果,需要引入缩放属性,复杂度会提升很多。

另外就是选中多个图形,然后缩放的场景我没实现。这种场景下,通常是要锁定宽高比的。

否则就会出现图形的斜切效果,这个如果要实现,我们还要引入斜切属性,复杂度再一次提升。

下面是 Figma 的效果,真是让人头扁。

按住 Alt 实现图形中心缩放也没做,这个比较简单,有空再做。

读者如果看懂我这篇文章,心里应该有思路的:width、height 的计算要加入图形中点参数,参照点设置为图形中点。

结尾

本文实现了图形缩放的功能,希望对你有所帮助。

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


相关阅读,

计算机图形学:变换矩阵

图形编辑器开发:最基础但却复杂的选择工具

图形编辑器:历史记录设计

图形编辑器开发:模块间如何通信?

图形编辑器:工具管理和切换

图形编辑器:底层设计

图形编辑器:防误操作之拖拽阻塞

相关推荐
EricWang135815 分钟前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning15 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人25 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
小马哥编程1 小时前
【前端基础】CSS基础
前端·css
嚣张农民1 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
周亚鑫2 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf
Justinc.2 小时前
CSS3新增边框属性(五)
前端·css·css3
neter.asia2 小时前
vue中如何关闭eslint检测?
前端·javascript·vue.js