文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-11-30 06:31

关注

编辑器 github 地址:

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

线上体验:

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

图形的属性

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

位置和大小

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

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

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

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

旋转

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

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

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

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

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

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

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

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

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

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

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

缩放实现思路

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

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

交互逻辑:

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

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

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

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

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

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

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

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

更新 width 和 height

首先是更新矩形宽高。

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

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

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

看看代码

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 经过旋转的位置。

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 可能为负数,这里要做一个标准化,然后赋值给图形属性即可。

this.setAttrs(
  normalizeRect({
    x,
    y,
    width,
    height,
  }),
);

其他缩放控制点

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

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

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;
}

新旧参考点设置:

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 的参数:

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 的计算要加入图形中点参数,参照点设置为图形中点。

来源:前端西瓜哥内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯