前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >图形编辑器开发:钢笔工具的实现

图形编辑器开发:钢笔工具的实现

作者头像
前端西瓜哥
发布2024-04-19 18:51:11
580
发布2024-04-19 18:51:11
举报

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

这次讲解钢笔工具的实现思路。

先看一下整体效果:

我正在开发的 suika 图形编辑器: https://github.com/F-star/suika 线上体验: https://blog.fstars.wang/app/suika/

钢笔工具的作用

钢笔工具的作用是:绘制一些复杂的图形

这种图形叫做路径 Path,你也可以理解为多段线。

它将多条相对简单的线连接并做节点的光滑处理,最终变成一条灵活复杂的线。

像是 SVG 的 Path 的元素,单段的线有直线、圆弧、椭圆弧、二阶贝塞尔曲线、三阶段贝塞尔曲线等。

为数据标准化,以及简化用户操作,我们常常选择灵活性和表现力优秀的 三阶段贝塞尔曲线 来表达路径。

关于钢笔工具更多功能说明,可以看我的这篇钢笔工具说明书:

图形编辑器开发:钢笔工具功能说明书

这里会假设你已经看过这篇文章,后面的内容不会过多讲解一些专业术语。

Path 图形类

首先要定义好钢笔所绘制的图形:Path。

我选择 segment 的表达方式,如图。

设计的 Path 的数据结构为:

代码语言:javascript
复制
// 注意这里存的是数组
type PathData = PathItem[];

interface Segment {
  point: { x: number; y: number };   // 锚点
  in: { x: number; y: number }; // 入点(这里用的相对坐标,相对锚点)
  out: { x: number; y: number }; // 出点(同上)
}

interface PathItem {
  segs: Segment[];
  closed: boolean;
}

这是一个 复杂 Path,由多个简单 Path 组成。

比如对下面 Path 图形,

它的表达大致为:

代码语言:javascript
复制
const pathData = [
  {
    // pathItem 0
    segs: [
      {
        point: { x: 84.2705, y: 194.5 },
        in: { x: 145.5, y: 37 },
        out: { x: -145.5, y: -37 },
      },
      // ...
    ],
    closed: false, // 不闭合
  },
  // pathItem 1
  {
    segs: [
      {
        point: { x: 251.2708, y: 66 },
        in: { x: 0, y: 0 },
        out: { x: 0, y: 0 },
      },
      // ...
    ],
    closed: true, // 闭合
  },
];

至于渲染,基本所有渲染引擎都支持 Path 的渲染,只要把数据结构转换一下就可以了。

比如,对于 SVG 可以用 Path 元素的 C 命令;对于 Canvas 2D 可以用 bezierCurveTo 方法。

另外,如果要做高级版的 Path:Figma 的矢量网格,是需要自己实现渲染器逻辑的,这也是我没选择实现它而是使用更通用的 Path 的原因。

Path 编辑器

图形编辑器有很多子模块,比如快捷键、工具的管理。

这样我们就可以通过 delete 键删除图形,将当前工具切换为绘制矩形工具以绘制矩形。

当绘制 Path 的时候,需要进入 Path 编辑器,此时我们需要 接管改写原来编辑器的一些功能

1、临时禁用一些工具包括它们的快捷键,只开启和 Path 编辑相关的工具。

代码语言:javascript
复制
// 记录好原来的可用工具,后面退出 Path 编辑器时需要复原
this.prevToolKeys = editor.toolManager.getEnableTools();

// 只开启 Path 相关工具
editor.toolManager.setEnableHotKeyTools([
  PathSelectTool.type,
  DrawPathTool.type,
]);

此时其他工具无法通过任何方式进行切换,比如快捷键。这么做是防止用户误操作,不小心退出还没完成的 Path 的编辑。

另外可以考虑通过事件的方式通知 UI 层,只显示当前能用的工具。

2、禁用一些功能。

比如高亮选中图形的轮廓,悬停在某个图形上,通知图层面板高亮对应 item。

代码语言:javascript
复制
editor.sceneGraph.showSelectedGraphsOutline = false;
editor.sceneGraph.highlightLayersOnHover = false;

3、覆盖或新增一些快捷键。

比如 Esc 键,原来的效果是回到选择工具以及取消图形选中,现在要改写为取消 Path 控制点的选中状态,以及退出 Path 编辑器。

此外还有 Enter 键,注册为退出 Path 编辑器。Delete 原来是删除选中的图形,要改写为删除选中的曲线片段。等等。

因为我的快捷键管理使用的是 短路模式(匹配到一个就结束),所以额外注册一个高优先级的事件响应函数就完事了。

退出 Path 编辑器后,这些功能覆写都需要进行还原

Path 控制点管理

此外,我们还要维护选中的 Path 的控制点,所以我们声明一个 SelectedControl 类,放到 PathEditor 下。

该模块的作用是,维护已经被选中的控制点,计算 Path 上需要渲染的控制点进行渲染。

SelectedControl 记录当前 Path 上被选中的控制点:

代码语言:javascript
复制
const selected = [
  // 锚点控制点,在索引值为 0 的 path 上的索引值为 1 的 seg 上
  { type: 'anchor', pathIdx: 0, segIdx: 1 }
  // ...
  { type: 'anchor', pathIdx: 0, segIdx: 2 }
]

控制点除了 anchor、还有 in、out、curve。

首先我们要基于当前 Path,渲染出所有的锚点(这里用白心蓝边表示)。

被选中控制点的相邻 segment 的 handleIn 和 handleOut 控制点会被绘制。

in 和 out 到对应的锚点的连线也要绘制,这样我们才知道它们属于哪一个 Segment。

选中控制点本身会渲染为选中状态(图中的蓝心白边圆)。

被选中的控制点,可以进行类似被选中图形的操作:

  1. 拖拽移动,同时改变多个控制点的位置;
  2. 删除,按下 delete 键,将一个闭合的 Path 变成非闭合,或者将一个非闭合 Path 变成两个 Path;
  3. ...

更多请阅读文章开头提及的文章。

按下 Esc 键,如果有选中的控制点,清空;如果已经没有选中控制点,退出 Path 编辑器。

绘制 Path 工具

点击钢笔工具按钮,此时 Path 编辑器还没有激活,因为我们目前还没有创建 Path。

当我们按下鼠标,绘制第一个锚点时,会创建一个 Path。

此时开启 Path 编辑器,并将这个 Path 传过去

设置 handleIn 和 handleOut

此时按住鼠标不放,然后拖拽,就会更新Path 控制点的 in 和 out 的位置。

默认 in 和 out 长度角度对称,按住 Alt 键会变成不对称,相互独立。按住shift强制极轴追踪(45 度的倍数)

是否绘制下一个 PathItem

每画完一个锚点,该锚点会被选中。

我们会 基于当前选中锚点,且为 PathItem 的一个末点,去绘制它的相邻的下一个锚点

因此,你可能需要考虑 把选中控制点这种行为,也保持到历史记录里

如果当前没有锚点被选中或不是末点,那就绘制一个新的 PathItem。

注意这个 PathItem 和其他 PathItem 是属于同一个复杂 Path 的。

预测曲线

在准备绘制下一个锚点的时候,移动鼠标,会绘制两个特殊的控制点:

  1. 光标所爱的点;
  2. 光标到上一个锚点的形成的三阶贝塞尔曲线;

表示如果你按下鼠标,新的一段曲线的形状就会是这样子的。

闭合 PathItem

当光标落在 PathItem 的某一个末点上,光标进行更换,表示点下去会闭合当前 PahtItem。

Path 选择工具

类似选择工具,选择工具选中操作的是图形,而 Path 选择工具选中和操作的是 Path 上的控制点。

这里类似同样需要实现一套点选、框选、连选的逻辑。

这里不多说,基本上和选择工具大同小异,可以看这篇文章:

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

Path 编辑器的进入和退出

虽然但是,Path 的进入和退出的场景有很多种,你需要注意有没有漏掉一些。

进入 Path 编辑器

  1. 钢笔工具绘制第一个锚点,从零到一绘制一个新 Path;
  2. 双击 Path 或选中 Path 后回车;
  3. 撤销后重做,在执行创建 Path 的命令前。需要让命令管理类支持 beforeRedo 的事件钩子。

退出 path 编辑器

  1. 点击左上角的 “完成” 按钮;
  2. 按下 enter 回车键,这个快捷键在激活 path 编辑器时进行了注册;
  3. 按下 Esc 键,且此时没有被选中的 Path 控制点;
  4. 撤销操作,撤销到创建 Path 命令之前。同样需要命令管理类支持 beforeUndo 钩子;
  5. 重做操作,重做到绘制编辑完 Path 的命令之后;
  6. 图层面板选中了其他图形,需要监听选中图形改变事件,当发现选中图形不是当前 Path 时退出。

其他

只画了一个锚点就结束编辑了怎么办?在结束编辑后追加一个删除 Path 命令。

绘制第一个锚点时,有创建 Path 命令和修改 handleIn 和 handleOut 命令,这两个命令,撤销两次才能取消一个 segment,怎么解决?

可以通过将两个命令标记为批量执行,撤销重做时连续执行,有点类似宏命令。需要对命令管理类进行改造,供高级用法。

右侧属性面板可以显示选中控制点的位置信息,并支持通过输入框修改。

...

最后

钢笔工具(和 Path 选择工具)是复杂工具,属于图形编辑器的核心工具,它有非常多的功能需要实现,目前我只搭了个框架而已。

它的背后其实是一个 Path 编辑器,一套不同的另一套编辑器体系,会接管改写原来图形编辑器部分能力。

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端西瓜哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 钢笔工具的作用
  • Path 图形类
  • Path 编辑器
  • Path 控制点管理
  • 绘制 Path 工具
    • 是否绘制下一个 PathItem
      • 预测曲线
      • 闭合 PathItem
      • Path 选择工具
      • Path 编辑器的进入和退出
        • 进入 Path 编辑器
          • 退出 path 编辑器
          • 其他
          • 最后
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档