文章详情

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

请输入下面的图形验证码

提交验证

短信预约提醒成功

图形编辑器开发:缩放和旋转控制点

2024-11-30 02:44

关注

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

挺久没写图形编辑器开发系列了,今天来讲讲控制点,它是图形编辑器的不可缺少的基础功能。

控制点是吸附在图形上的一些小矩形和圆形点击区域,在控制点上拖拽鼠标,能够实时对被选中进行属性的更新

比如使用旋转控制点可以更新图形的旋转角度,使用缩放控制点调整图形的宽高。

这两个都是通用的控制点,此外还有给特定图形使用的专有控制点,像是矩形的圆角控制点,可拖动调整圆角大小。这些比较特别。后面会专门出一篇文章讲这个。

需求描述

选中图形,会出现旋转控制点和缩放控制点,然后操作控制点,调整图形属性。

控制点的类型和位置如下:

缩放控制点有 8 个。

首先是 西北(nw)、东北(ne)、东南(se)、西南(sw)缩放控制点。它们在选中图形包围盒的四个顶点上,拖拽可同时调整图形的宽高。

接着是 东(e)、南(s)、西(w)、北(n)缩放控制点,拖拽它们只更新图形的宽或高。

它们是不可见的,但 hover 上去光标会变成缩放的光标。这几个控制点的点击区域很大。

旋转控制点有 4 个,对应四个角落,分别为:nwRotation、neRotation、seRotation、swRotation。

同样它们是透明的,但 hover 上去光标会变成旋转光标。

旋转控制点有另外一种风格,就是只在图形的某个方向(通常是正上方)有一个可见旋转控制点。下面是 Canva 编辑器的效果:

我更喜欢第一种风格,画面会更清爽一些。

实现思路

整体实现思路很简单:

代码设计

我们需要实现控制点管理类 ControlHandleManager 和控制点类 ControlHandle。

ControlHandle 类记录以下信息:

这里直接用图形编辑器绘制图形用到的图形类。

通常你使用的渲染图形库是会有

创建 ControlHandle 对象。

我们需要创建的控制点对象为:

// 右下角(ns)的控制点  
const se = new ControlHandle({
  graph: new Rect({
    objectName: 'se', // 控制点类型标识,放其他地方也行
    cx: 0, // x 和 y 会根据选中图形的包围盒更新
    cy: 0,
    width: 6,
    height: 6,
    fill: 'white',
    stroke: 'blue',
    strokeWidth: 1,
  }),
  getCursor: (type, rotation) => {
    // ...
    return 'se-rezise'
  } ,
});

这个对象会保存到控制点管理类的 transformHandles 属性中。

transformHandles 是一个映射表,类型标识字符串映射到控制点对象。

class ControlHandleManager {
  visible = false;
  transformHandles;

  constructor() {
    // 映射表 type -> 控制点
    this.transformHandles = {
      se: new ControlHandle(),
      n: new ControlHandle(),
      nwRoation: new ControlHandle(),
      // ...
    }
  }
}

渲染

当我们选中图形时,调用渲染方法。

此时会调用 ControlHandleManager 的 draw 渲染方法,渲染控制点。

根据包围盒计算控制点的中点位置。这个包围盒有 x、y、width、height、rotation 属性。我们需要计算这个包围盒的四个顶点的位置,包围盒外扩一定距离后的四个顶点的位置,四条线段的中点的位置。

class ControlHandleManager {
  // ...
  
  
  draw(rect: IRectWithRotation) {
  
  // calculate handle position
  const handlePoints = (() => {
    const cornerPoints = rectToPoints(rect);
    const cornerRotation = rectToPoints(offsetRect(rect, size / 2 / zoom));
    const midPoints = rectToMidPoints(rect);

    return {
      ...cornerPoints,
      ...midPoints,
      nwRotation: { ...cornerRotation.nw },
      neRotation: { ...cornerRotation.ne },
      seRotation: { ...cornerRotation.se },
      swRotation: { ...cornerRotation.sw },
    };
  })();
 }
}

遍历控制点对象,赋值上对应的中点坐标:cx、cy。调整 n/s/w/e 的宽高,它们的宽高是跟随。

// 整个顺序是有意义的,是渲染顺序
const types = [
  'n',
  'e',
  's',
  'w',
  'nwRotation',
  'neRotation',
  'seRotation',
  'swRotation',
  'nw',
  'ne',
  'se',
  'sw',
] as const;

// 更新 cx 和 cy
for (const type of types) {
  const point = handlePoints[type];
  const handle = this.transformHandles.get(type);
  handle.cx = point.x;
  handle.cy = point.y;
}

// n/s/w/e 比较特殊,n/s 的宽和包围盒宽度相等,w/e 高等于包围盒高。
const neswHandleWidth = 9;
const n = this.transformHandles.get('n')!;
const s = this.transformHandles.get('s')!;
const w = this.transformHandles.get('w')!;
const e = this.transformHandles.get('e')!;
n.graph.width = s.graph.width = rect.width * zoom;
n.graph.height = s.graph.height = neswHandleWidth;
w.graph.height = e.graph.height = rect.height * zoom;
w.graph.width = e.graph.width = neswHandleWidth;

接着就是遍历 transformHandles,基于 cx 和 cy 更新图形的 x/y,然后绘制。

this.transformHandles.forEach((handle) => {
  // 场景坐标转视口坐标
  const { x, y } = this.editor.sceneCoordsToViewport(handle.cx, handle.cy);
  const graph = handle.graph;
  graph.x = x - graph.width / 2;
  graph.y = y - graph.height / 2;
  graph.rotation = rect.rotation;

  // 不可见的图形不渲染(本地调试的时候可以让它可见)
  if (!graph.getVisible()) {
    return;
  }

  graph.draw();
});

渲染逻辑到此结束。

控制点拾取

在选择工具下,选中图形,控制点出现。

接着 hover 到控制点上,更新光标。并且在按下鼠标时,能够拿到对应的控制点类型,进行对应的旋转或缩放操作。

这里我们需要判断光标的位置是否在控制点上,即控制点拾取。

控制点拾取逻辑为:

以渲染顺序相反的方向遍历控制点,调用 hitTest 方法检测光标是否在控制点的点击区域上。

如果在,返回 type 和 cursor;否则返回 null。

class ControlHandleManager {
  // ...

  
  getHandleInfoByPoint(hitPoint: IPoint) {
    const hitPointVW = this.editor.sceneCoordsToViewport(
      hitPoint.x,
      hitPoint.y,
    );
    
    for (let i = types.length - 1; i >= 0; i--) {
      const type = types[i];
      const handle = this.transformHandles.get(type);
 
      // 是否点中当前控制点
      const isHit = handle.graph.hitTest(
        hitPointVW.x,
        hitPointVW.y,
        handleHitToleration,
      );

      if (isHit) {
        return {
          handleName: type, // 控制点类型
          cursor: handle.getCursor(type, rotation), // 光标
        };
      }
    }
  }  
}

反向很重要,应为可能会有控制点发生重叠,此时应该是在更上方的控制点,也就是后渲染的控制点优先被选中。

光标

getCursor 返回的光标值是动态的,会因为包围盒的角度不同而变化,这里会有一个简单的转换。

const getResizeCursor = (type: string, rotation: number): ICursor => {
  let dDegree = 0;
  switch (type) {
    case 'se':
    case 'nw':
      dDegree = -45;
      break;
    case 'ne':
    case 'sw':
      dDegree = 45;
      break;
    case 'n':
    case 's':
      dDegree = 0;
      break;
    case 'e':
    case 'w':
      dDegree = 90;
      break;
    default:
      console.warn('unknown type', type);
  }

  const degree = rad2Deg(rotation) + dDegree;
  // 这个 degree 精度是很高的,
  // 设置光标时会做一个舍入,匹配一个合法的接近光标值,比如 ne-resize
  return { type: 'resize', degree };
}

旋转光标同理。

此外,浏览器支持的 resize 光标值是有限的。

为了更好的效果是实现 resize0 ~ resize179 代表不同角度的一共 180 个自定义 resize 光标。

或者做一个 “四舍五入”,转为浏览器支持的那几种 resize 角度,但这样光标效果不是很好,看起来光标并没有和控制点垂直,算是一种妥协。

旋转光标更是不存在了,我们要设计 rotation0 ~ rotation179 共 360 个自定义光标。当然我们可以让精度降一下,比如只实现偶数值的旋转角度的光标,比如 rotation0、rotation2、rotation4,也要 180 个。

关于自定义光标的实现方案,本文不深入讲解,会单独写一篇文章讨论。

坐标系

有个容易忽略的问题,就是控制点是绘制在哪个坐标系中的?

是场景坐标系,还是视口坐标系。

如果在场景坐标系中,图形会随画布的缩放或移动 “放大缩小”,比如一根 2px 的线条,在 zoom 为 50% 的画布下,显示的效果是 1px。

控制点的宽高是不应该跟随  zoom 而变化的。

如果你绘制在视口坐标系,宽高不需要考虑,只要转换一下 x,y。如果在场景坐标中,x、y 不用转换,但是宽高要除以 zoom。

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

免责声明:

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

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

软考中级精品资料免费领

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

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

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

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

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

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

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