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

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

本文将探究图形编辑器中钢笔工具的功能,算是一篇简单说明书。

只有理解了需求,尤其是复杂的需求,才能更好地进行功能开发,写出诗一样的高鲁棒性代码。

三阶贝塞尔曲线组成的路径

钢笔绘制的是曲线,通常使用 三阶贝塞尔曲线 进行表达。

三阶贝塞尔曲线使用 4 个点:锚点 1、控制点 1、控制点2、锚点 2 来表达一条最小单位的曲线,如下图:

锚点(Anchor Points)是路径上的点,控制点(Control Points)和锚点配对出现,控制曲线的方向和弯曲程度。

原理简单来说,就是这 4 个点依次连接为 3 条线,然后每条线各自从起点向终点,按相同百分比取点位置,然后这些点继续重复连线和取点的操作,最后得到单点。

从起点不断移动到终点,这个点所经过的路径为这个贝塞尔曲线的形状。

一条三阶贝塞尔能表达的曲线还是太简单了。

所以为了表达更复杂的曲线,我们选择 将多个三阶贝塞尔曲线依次首尾相连,表达为 "路径"(Path)

另外,如果保持上一条曲线的控制点 2 和下一条曲线的控制线 基于公共锚点对称,就能有平滑的效果

路径可以表达任何形状,比如矩形、椭圆、多边形。甚至你可以在设计图形类的时候,仅仅用一个 Path 类,完全足够的。

至于铅笔工具,其实就是将连续的多段直线线段通过算法进行平滑化处理,转换为三阶贝塞尔曲线组成的路径。

虽然看起来很方便,但通常会产生大量冗余点,不如用钢笔工具清爽。不过倒是适合配合触控笔使用。

路径数据结构设计

三阶贝塞尔曲线的数据结构有两种设计思路。

(1)curve 表达

一种是参考 SVG 的 Path 元素中对三阶贝塞尔曲线的表达。

css 复制代码
{
  curves: [
    // 起点
    { point: { x: 0, y: 0 } },
    // 锚点 2 和两个控制点。
    // 配合上一个锚点,组成一个最小单位的三阶贝塞尔曲线
    {
      point: { x: 100, y: 10 },
      handle1: { x: 10, y: -40 },
      handle2: { x: 90, y: 30 },
    },
    // ...
  ],
  closed: false, // 是否闭合
};

因为前一段三阶贝塞尔曲线的锚点 2 和后一段的锚点 1 是相同的,我们会忽略掉重复的锚点 1 数据。

handle1 和 handle2 是可选的,没有就是和锚点重合了。handle1 和 handle2 如果都没有,则表达为一条直线。

这种表达更贴近三阶贝塞尔曲线的原始意图。

(2)segment 表达

我们还有另一种表达:分成多个同样的片段。每个片段由 1 个锚点,以及 2 个控制点组成,见下图。

控制点为该锚点在相邻两段三阶贝塞尔曲线的两个控制点:入控制点(handleIn)和出控制点(handleOut)。

handleIn 和 handleOut 可以用绝对坐标,也可以用相对锚带你的坐标。

数据结构:

css 复制代码
{
  segments: [
    {
      point: { x: 0, y: 0 },
      handleOut: { x: 10, y: -40 },
    },
    {
      point: { x: 100, y: 10 },
      handleIn: { x: 90, y: 30 },
      handleOut: { x: 110, y: 30 },
    },
    {
      point: { x: 200, y: 0 },
      handleIn: { x: 190, y: -20 },
      // 这里可保留路径终点趋势
      // handleOut: { x: 220, y: 40 },
    },
    // ...
  ],
  closed: true,
}

我个人更推荐这种表达,它更优雅,且更匹配钢笔的绘制行为的抽象:绘制路径的每次拖拽其实就是创建了一个 segment,这个用 curve 就不好表达,比较碎片。

此外 segment 表达最后一个锚点时,可以用 handleOut 属性自然地保留下一条贝塞尔曲线的趋势,之后从末尾续一条曲线时,就不需要再进行额外操作,去设置控制点 1。路径的起点同理。

如果要用 curve 的表达,也是可以的,比较别扭,需要额外两个属性。

绘制路径

使用钢笔工具绘制路径,其交互为:

  1. 鼠标按下,确定新曲线锚点 1 位置,以及上一个曲线;

  2. 鼠标按下不放,然后移动进行拖拽,确定控制点 1 位置。此时可以使用对称策略更新上一条曲线的控制点 2;

  3. 鼠标释放,此时移动光标,会有一个 预测曲线,表示如果在当前位置按下鼠标,所产生的新曲线形状。

  4. 鼠标如果点在起点附近,会将路径进行闭合。

编辑路径

路径曲线画好了,可能有瑕疵,需要微调。

对于绘制好的路径,需支持的常用编辑操作有如下几种。

1、修改锚点位置,对应的控制点也会移动,需要一起修改。如果控制点使用相对位置,甚至不用改。

2、修改锚点,修改曲线的弯曲程度。

因为线条大多情况下要求平滑,所以默认会使用 "锚点对称+长度相等" 效果,此外还有 "锚点对称" 和 "不对称"。

3、修改某段曲线的位置,等价于移动曲线的两个锚点。

4、 添加锚点,在一段曲线的中间某个位置加一个锚点,并保存操作前后形状不变。

4、减少锚点,该锚点会丢弃,然后它的前后两个锚点连接,因为信息变少了,通常无法保持原来的形状。

5、删除锚点或曲线,选中后按下删除键。它会将一条路径从中间断开,如果没有闭合会断开为两个路径,如果闭合就会变成一条不闭合的路径;

6、弯曲(Bend)效果:可以在一段曲线上的某个点拖动,光标所在点会保持在新的曲线形状上。

Figma 的加强版钢笔工具

这里再简单介绍下 Figma 的钢笔工具。

Figma 的钢笔工具,和一般的钢笔工具有点不一样,它做了一些 遥遥领先的创新

它也是使用了三阶贝塞尔曲线,但画的不再是路径,而是网格了。

Figma 称这种特殊的曲线为 Vector Network(矢量网格)

路径是一条线,由多个小的曲线依次连接而成,从起点出发,会经过所有的锚点,最后到达终点,所形成的这么一条线。

Figma 的矢量网格是图(graph),它在路径的基础上做了增强,可以有分岔,如下图。

矢量网格对设计师来说是友好的,它让绘制 UI 变得更得心应手,不习惯也能使用原来的绘制路径的方式,矢量网格完全兼容路径。

但对开发来说,表达矢量网格的数据结构就复杂得多,需要使用一种类似邻接表的表达,要考虑的场景更多了。

(开发:你这个,不太好实现。。产品你是不是有。。额。。。诶,原)

首先是用数组记录好所有顶点的数据,数组的索引值为顶点的唯一标识。

css 复制代码
vertices: [
  /* 点 0 */ { x: 0, y: 0 },
  /* 点 1 */ { x: 30, y: 5 },
  // ...
]

然后是找点与点之间的邻接关系,假设对于点 0,它连接到了点 1、点 2、点 3,产生了 3 条贝塞尔曲线,那它的表达如下。

css 复制代码
segments: [
  {
    // 点 1 和 3 相连,并设置它们的控制点,并使用相对位置
    start: { vertex: 0, dx: 38, dy: -48 },
    end: { vertex: 1, dx: -37, dy: -22 }
  },
  {
    start: { vertex: 1, dx: 47, dy: 28 },
    end: { vertex: 2, dx: 38, dy: -17 }
  },
  {
    start: { vertex: 0, dx: -23, dy: 37 },
    end: { vertex: 3, dx: -47, dy: -40 }
  }
  //...
]

最后是子区域的划分,从这些线中找一些线组成成的闭合子区域,如果设置了填充色,就会往这些区域填充颜色。

css 复制代码
regions: [
  {
    windingRule: 'NONZERO', // 绕数非零填充规则
    loops: [
      {
        // 组成子区域的 4 个顶点
        segments: [0, 4, 5, 1],
      },
    ],
  },
];

结尾

以上就是钢笔工具的功能说明书,看完就应该知道怎么实现钢笔工具了吧,赶紧开写吧。

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


相关阅读,

贝塞尔曲线是什么?如何用 Canvas 绘制三阶贝塞尔曲线?

剖析 Figma 数据结构:不同图形的特有属性

学到了!Figma 原来是这样表示矩形的

什么?Figma 的 fig 文件格式居然解析出来了

Figma 是如何做协同编辑的?

图形编辑器开发:是否要像 Figma 一样上 wasm

相关推荐
zqx_71 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己1 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称2 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普3 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H3 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍3 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai3 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端