当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2023-10-20 10:02:58 287观看
导读编辑器 github 地址:https://github.com/F-star/suika线上体验:https://blog.fstars.wang/app/suika/图形的属性图形有几个重要的基础属性,会经常被用到,我们在实现缩放图形前需要理清一下它们。x / ywidth / heightrotat

nIy28资讯网——每日最新资讯28at.com

编辑器 github 地址:nIy28资讯网——每日最新资讯28at.com

https://github.com/F-star/suikanIy28资讯网——每日最新资讯28at.com

线上体验:nIy28资讯网——每日最新资讯28at.com

https://blog.fstars.wang/app/suika/nIy28资讯网——每日最新资讯28at.com

图形的属性

图形有几个重要的基础属性,会经常被用到,我们在实现缩放图形前需要理清一下它们。nIy28资讯网——每日最新资讯28at.com

  • x / y
  • width / height
  • rotation

位置和大小

x 和 y 为图形的左上角位置,注意是旋转前的。nIy28资讯网——每日最新资讯28at.com

x、y 旋转后我们叫做 rotatedX、rotatedY,属性面板中会用到。nIy28资讯网——每日最新资讯28at.com

width 和 height 为图形的宽高,这个没什么好说的。nIy28资讯网——每日最新资讯28at.com

另外,有些图形有些特殊,它的 x、y、width、height 是要通过其他属性计算出来的,比如贝塞尔曲线。nIy28资讯网——每日最新资讯28at.com

旋转

rotation 为图形的旋转度数,通常使用 弧度单位。nIy28资讯网——每日最新资讯28at.com

因为弧度是数学计算中的常客,各种 API 都是要求提供弧度的,比如内置的 Math.sin() 方法。nIy28资讯网——每日最新资讯28at.com

你存角度自然也是可以,但不推荐,但计算时多了一层多余的单位转换,且丢失一些微小的精度。nIy28资讯网——每日最新资讯28at.com

当然 UI 层还是要展示角度,因为是面向用户的,对于数据和 UI 不统一的问题,在 UI 层做一个转换即可。nIy28资讯网——每日最新资讯28at.com

旋转度数通常要配合一个变换中心(origin),这个可以作为一个属性让用户设置。nIy28资讯网——每日最新资讯28at.com

但我更建议将 x、y、width、height 形成的 矩形的中点 作为旋转中心,这样更简单一些,减少用户的心智负担,也防止出现用户设置一些奇怪 origin 的场景。nIy28资讯网——每日最新资讯28at.com

下图中,红色矩形是蓝色矩阵顺时针旋转 45 度得到。nIy28资讯网——每日最新资讯28at.com

nIy28资讯网——每日最新资讯28at.com

旋转度数还要考虑 旋转方向、基准角度、取值范围 问题。nIy28资讯网——每日最新资讯28at.com

(因为弧度不直观,后面会用角度来描述,但数据层依旧还是用的弧度)nIy28资讯网——每日最新资讯28at.com

  • 旋转方向:设置旋转后,图形是会往顺时针方向还是逆时针方向旋转。
  • 基准角度:朝向哪里是 0 度。
  • 取值范围:通常为 [0, 360) 和 (-180, 180]。二者其实等价,只是显示有区别,后者其实只是前者减去 180 度。

通常这些编辑器自己决定就好。像我的项目,向上表示 0 度,顺时针方向为旋转方向,方向取值为 [0, 360)。nIy28资讯网——每日最新资讯28at.com

一些编辑器是支持用户自己设置的,比如 AutoCAD 可通过图形单位命令,设置旋转方向和基准角度。nIy28资讯网——每日最新资讯28at.com

图片nIy28资讯网——每日最新资讯28at.com

缩放实现思路

进入正题,对图形进行缩放。nIy28资讯网——每日最新资讯28at.com

接下来会以通过右下角(也叫东南 se 方向) 缩放控制点缩放为例进行讲解。nIy28资讯网——每日最新资讯28at.com

nIy28资讯网——每日最新资讯28at.com

交互逻辑:nIy28资讯网——每日最新资讯28at.com

选择工具下,当光标落在右下角的缩放控制点上时,光标会变成缩放样式(这个不是本文核心,不讲)。nIy28资讯网——每日最新资讯28at.com

此时按下鼠标,然后进行拖拽,即可对图形以左上角为缩放中心,进行缩放。nIy28资讯网——每日最新资讯28at.com

实现思路:更新 width 和 height,然后确定参照点,修正 x  和 y。nIy28资讯网——每日最新资讯28at.com

按下鼠标时,我们要把当前图形的 x、y、width、height、rotation 记录下来。之后的缩放是基于这个初始状态进行的。nIy28资讯网——每日最新资讯28at.com

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

拖拽时,调用我们将要实现的 movePoint 方法,去更新这个图形。nIy28资讯网——每日最新资讯28at.com

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

下面就是核心方法 movePoint 的实现逻辑了。nIy28资讯网——每日最新资讯28at.com

更新 width 和 height

首先是更新矩形宽高。nIy28资讯网——每日最新资讯28at.com

因为有一个旋转,所以算法不会这么直观。nIy28资讯网——每日最新资讯28at.com

我们要意识到这里有一个变换。看到的图形,是做过变换(基于矩形中心旋转)之后的,但我们需要修改的 width、height、x、y 则是旋转前的。nIy28资讯网——每日最新资讯28at.com

所以我们需要把光标位置给旋转回来,然后再减去 x 和 y 去得到真正的 width 和 height。nIy28资讯网——每日最新资讯28at.com

nIy28资讯网——每日最新资讯28at.com

看看代码nIy28资讯网——每日最新资讯28at.com

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    // ...  }}

看看只更新宽高的效果。nIy28资讯网——每日最新资讯28at.com

nIy28资讯网——每日最新资讯28at.com

可以看到是有问题的,因为修改宽高后,矩形的中心点也发生了变化,导致缩放中心错误。所以我们要修正一下 x 和 y。nIy28资讯网——每日最新资讯28at.com

修正 x 和 y

接着我们就要修正 x 和 y 的值。nIy28资讯网——每日最新资讯28at.com

重点就一句话:缩放前的参考点和缩放后的参考点的位置要保持一致。这个参考点其实就是图形缩放过程中的缩放中心。nIy28资讯网——每日最新资讯28at.com

对于右下角缩放控制点,它的缩放中心就是左上角,即 x 和 y 经过旋转的位置。nIy28资讯网——每日最新资讯28at.com

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 可能为负数,这里要做一个标准化,然后赋值给图形属性即可。nIy28资讯网——每日最新资讯28at.com

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

其他缩放控制点

对于其他类型缩放控制点,比如左上、右上、左下缩放控制点,它们的大框架是一样的,只是 width 和 height 计算方式不同,以及参考点不同。nIy28资讯网——每日最新资讯28at.com

不同类型下 width 和 height 的设置:nIy28资讯网——每日最新资讯28at.com

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

新旧参考点设置:nIy28资讯网——每日最新资讯28at.com

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

暂时没实现正北、正南、正西、正东的逻辑,逻辑大差不差。nIy28资讯网——每日最新资讯28at.com

锁定缩放比

按住 shift 可以锁定缩放比。nIy28资讯网——每日最新资讯28at.com

做法是对比新旧图形宽高比,将 width 和 height 其中一个进行修正即可。注意正负号。nIy28资讯网——每日最新资讯28at.com

方法需要多传一个 keepRatio 的参数:nIy28资讯网——每日最新资讯28at.com

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 的情况..nIy28资讯网——每日最新资讯28at.com

优化点

本文的实现是考虑的是比较简单的缩放图形场景,一些更复杂的场景并未实现。nIy28资讯网——每日最新资讯28at.com

缩放还有另一种策略,就是会产生 反向颠倒 的缩放。要实现这个效果,需要引入缩放属性,复杂度会提升很多。nIy28资讯网——每日最新资讯28at.com

另外就是选中多个图形,然后缩放的场景我没实现。这种场景下,通常是要锁定宽高比的。nIy28资讯网——每日最新资讯28at.com

否则就会出现图形的斜切效果,这个如果要实现,我们还要引入斜切属性,复杂度再一次提升。nIy28资讯网——每日最新资讯28at.com

下面是 Figma 的效果,真是让人头扁。nIy28资讯网——每日最新资讯28at.com

nIy28资讯网——每日最新资讯28at.com

按住 Alt 实现图形中心缩放也没做,这个比较简单,有空再做。nIy28资讯网——每日最新资讯28at.com

读者如果看懂我这篇文章,心里应该有思路的:width、height 的计算要加入图形中点参数,参照点设置为图形中点。nIy28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-14344-0.html图形编辑器开发:实现缩放图形

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 深入理解 Netty FastThreadLocal

下一篇: 23种软件设计模式综述

标签:
  • 热门焦点
Top