Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >图形编辑器开发:绘制图形工具

图形编辑器开发:绘制图形工具

作者头像
前端西瓜哥
发布于 2023-08-18 05:29:50
发布于 2023-08-18 05:29:50
23200
代码可运行
举报
运行总次数:0
代码可运行

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

今天来介绍如何实现图形绘制工具,实现绘制任意的图形。

编辑器 github 地址: https://github.com/F-star/suika 线上体验: https://blog.fstars.wang/app/suika/

我之前讲过如何实现工具类管理类的:

图形编辑器:工具管理和切换

对应的工具类的实现会围绕用户的 按下鼠标、拖拽、释放 这 3 个行为,图形绘制工具同样如此。

整体框架:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 绘制图形工具类(这里用了抽象类,后面会说为什么)
abstract class DrawGraphTool {
  // 工具被激活
  active() {
    // 通常是设置光标,或是绑定一些事件,比如键盘事件
  }
  // 工具失活
  inactive() {
    // 通常是解绑一些事件
  }
  
  // 鼠标按下
  start() { /* TODO */ }
  // 鼠标拖拽
  drag() { /* TODO */ }
  // 鼠标释放
  end() { /* TODO */ }
}

类似 React / Vue 的生命周期 hook。

模板模式

图形有很多种,矩形、椭圆、三角形、五角星等等。每个图形都实现一遍未免有点繁琐。

西瓜哥我一开始是分别去实现绘制矩形和椭圆的,然后发现有很多相同的逻辑。当又要加一个新的图形时,又要复制粘贴,然后修改少量的不一样的地方,这不利于代码维护。

为解决这个问题,我们要实现一个 绘制图形基类,将共用逻辑放到里面,不同的部分则交给子类去实现

这个在设计模式上叫做 模板模式

所谓模板模式,就是在方法中定义一个 “算法” 骨架,继承的子类在不改变算法整体结构的情况下,重写其中某些步骤(有些步骤有默认实现,可不重写)

模板模式的具体实现,就是用 抽象类(abstract class) 去实现这个基类。

抽象类是一种不能被实例化的特殊类,继承的子类才能实例化。

抽象类的方法可以是普通方法,也可以是只定义了方法类型签名的抽象方法。

子类继承抽象类时,必须提供抽象类的抽象方法的具体实现。

TypeScript 支持抽象类。下面是一个例子。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 抽象类
abstract class AbstractClass {
  say() {
    if (this.shoudISaySomething()) {
      console.log('前端西瓜哥')
    }
  }
  // 抽象方法(不能用 private,因为子类要重写它)
  protected abstract shoudISaySomething(): boolean
}

class A extends AbstractClass {
  shoudISaySomething() {
    // ...假设这里一堆判断
    return true
  }
}

子类不实现抽象方法的话,TS 编译会报错:

如果你用 JavaScript,虽然不能做编译时的检验,但还可以做运行时的检测。

将需要子类继承实现的方法,加入抛出错误的实现。这样子类如果没实现,就会通过原型链的方式,执行基类的方法,然后报错提示给开发者。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class AbstractClass {
  say() {
    if (this.shoudISaySomething()) {
      console.log('前端西瓜哥')
    }
  }
  shoudISaySomething() {
    throw new Error('请实现 shoudISaySomething 方法')
  }
}

class A extends AbstractClass {
  shoudISaySomething() {
    // ...假设这里一堆逻辑
    return true
  }
}

图形绘制工具的实现

我们回到绘制图形的业务逻辑。

我们在鼠标按下时确定起始坐标,拖拽时调整终点坐标,鼠标释放确认终点坐标。

这里产生了一个矩形框,得到 x、y、width、height,通过它们可以确定了一个图形的位置和大小。

当要加一个新的图形时,只要它能够通过 x、y、width、height 这几个属性确定绘制效果,那就可以使用这个基类。

如果这个图形还有其他属性,我们可以在绘制后通过其他方式(比如控制点或者面板修改值)去修改。

鼠标按下

首先是鼠标按下的逻辑。逻辑很少,主要是记录起始点。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
abstract class DrawGraphTool {
  commandDesc = 'Add Graph'; // 历史记录的命令描述
  protected drawingGraph: Graph | null = null; // 被绘制的图形对象
  
  
  start(e: PointerEvent) {
    // 这里将光标的视口坐标转成场景坐标
    this.startPoint = this.editor.getSceneCursorXY(e);
    
    // 重置一些状态
    this.drawingGraph = null;
  }
}

鼠标拖拽

拖拽的时候,会判断 this.drawingGraph 是否为 null。

如果是,就会创建一个新的图形对象。如果不是,那就更新 this.drawingGraph 的 x、y、 width、height 属性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
abstract class DrawGraphTool {
  private lastDragPoint!: IPoint;
  
  drag(e: PointerEvent) {
    // 记录终点坐标
    this.lastDragPoint = this.editor.getSceneCursorXY(e);
    this.updateRect();
  }
  
  // 更新矩形选框,并对图形对象进行操作
  private updateRect() {
    const { x, y } = this.lastDragPoint;
    const sceneGraph = this.editor.sceneGraph;
    const { x: startX, y: startY } = this.startPoint;

    const width = x - startX; // 这个可能是负数,还没做标准化
    const height = y - startY; // 同上

    const rect = {
      x: startX,
      y: startY,
      width,
      height,
    };

    // 按住shift键,通过算法把矩形变成方形。
    if (this.editor.hostEventManager.isShiftPressing) {
      this.adjustSizeWhenShiftPressing(rect);
    }

    if (this.drawingGraph) {
      // (1)更新图形逻辑
      this.updateGraph(rect);
    } else {
      // (2)创建图形逻辑
      const element = this.createGraph(rect)!;
      sceneGraph.addItems([element]);

      this.drawingGraph = element;
    }
    // 设置选中对象,并渲染
    this.editor.selectedElements.setItems([this.drawingGraph]);
    sceneGraph.render();
  }
}

创建图形

创建图形对象的方法是 createGraph(),要返回一个图形对象,保存到 this.drawingGraph

这个图形对象需要子类来提供。所以写成抽象方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected abstract createGraph(rect: IRect, noMove?: boolean): Graph | null;

我们的矩形绘制工具,实现如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export class DrawRectTool extends DrawGraphTool implements ITool {
 // ...
  
  // 这里提供实现创建图形对象
  protected createGraph(rect: IRect) {
    rect = normalizeRect(rect);
    return new Rect({
      ...rect,
      fill: [cloneDeep(this.editor.setting.get('firstFill'))],
    });
  }
}

这里用 normalizeRect 对 rect 对象做了标准化,原来 width 和 height 可能为负数,标准化就是改变 x、y,并让 width 和 height 变回正数,变成一个常规的 rect 对象。

这样我们拿到了图形对象通用属性:x、y、width、height,然后这里再补上了一个默认的填充色。

如果要实现绘制直线,就不要提供填充色,而是要补一个默认描边。

更新图形

更新图形通常就是更新一下图形的 x、y、width、height 属性,所以基类会提供一个默认实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 这个是通用逻辑,直接更新 x、y、width、height
 */
protected updateGraph(rect: IRect) {
  // 对矩形标准化
  rect = normalizeRect(rect);

  const drawingShape = this.drawingGraph!;
  drawingShape.x = rect.x;
  drawingShape.y = rect.y;
  drawingShape.width = rect.width;
  drawingShape.height = rect.height;
}

当然有些图形并不是这样的逻辑,那子类就需要重写 updateGraph 方法。

比如绘制直线就比较特殊,它更新的是 width 和 rotation,height 则永远是 0,需要另写一个算法去实现转换。

Shift 模式

这里有个比较特别的效果,就是按住 Shift,会让 图形的宽高比保持一比一

绘制正方形:

绘制圆形:

实现就是找 width 和 height 绝对值大的那一个,然后符号保持不变,两者的绝对值都变成这个最大值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected adjustSizeWhenShiftPressing(rect: IRect) {
  // pressing Shift to draw a square
  const { width, height } = rect;
  
  const size = Math.max(Math.abs(width), Math.abs(height));
  // Math.sign() 方法可能会返回 0,所以要兜底为 1
  rect.height = (Math.sign(height) || 1) * size;
  rect.width = (Math.sign(width) || 1) * size;
}

子类如果比较特殊(没错说的就是你,直线工具),可重写该方法。

顺带一提,还有一种 Alt 模式,会将起始点作为图形的中心点进行绘制,这个我还没去实现。

鼠标释放

鼠标释放时,主要逻辑是将新的状态保持到历史记录中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
end(e: PointerEvent) {
  if (this.drawingGraph) {
    // 记录新的状态
    this.editor.commandManager.pushCommand(
      new AddShapeCommand(this.commandDesc, this.editor, [this.drawingGraph]),
    );
  }
}

结尾

模板模式的优点是复用和扩展。相同的主体框架逻辑不变,暴露几个方法让子类实现,有些是必须实现,有些是可实现可不实现(不实现用默认算法),对我们实现一种通用的绘制图形工具很有帮助。

实现了这个图形绘制基类后,我们理论上就可以绘制任何图形了,甚至用户自定义的图形,只要这些图形对象使用 x、y、 width、height。

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

相关阅读,

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

图形编辑器:工具管理和切换

图形编辑器:底层设计

图形编辑器:对齐功能的实现

图形编辑器:历史记录设计

图形编辑器:防误操作之拖拽阻塞

图形编辑器:修改图形x、y、width、height、rotation

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
图形编辑器开发:基于相交策略选中图形
我开发的图形编辑器,原本选中图形是基于选区是否完全包含对应图形来判断其是否被选中,使用的是矩形包含判断。
前端西瓜哥
2023/08/18
1830
图形编辑器开发:基于相交策略选中图形
图形编辑器开发:最基础但却复杂的选择工具
光标停留在图形上方,按下鼠标左键,这个图形就被选中了。这就是一个简单的选中了单个图形的场景。
前端西瓜哥
2023/08/18
3720
图形编辑器开发:最基础但却复杂的选择工具
图形编辑器开发:缩放和旋转控制点
挺久没写图形编辑器开发系列了,今天来讲讲控制点,它是图形编辑器的不可缺少的基础功能。
前端西瓜哥
2023/11/14
2680
图形编辑器开发:缩放和旋转控制点
图形编辑器开发:实现缩放图形
另外,有些图形有些特殊,它的 x、y、width、height 是要通过其他属性计算出来的,比如贝塞尔曲线。
前端西瓜哥
2023/09/11
2110
图形编辑器开发:实现缩放图形
图形编辑器开发:模块间如何通信?
图形编辑器,随着功能的增加,通常都会愈发复杂,良好的架构是保证图形编辑器持续开发高效的重要技术。
前端西瓜哥
2023/08/18
1700
图形编辑器开发:模块间如何通信?
图形编辑器开发:一些会用到的简单几何算法
开发图形编辑器,你会经常要解决一些算法问题。本文盘点一些我开发图形编辑器时常用到的简单几何算法。
前端西瓜哥
2023/08/18
2430
图形编辑器开发:一些会用到的简单几何算法
图形编辑器开发:实现图形的复制粘贴
如果只支持粘贴到当前编辑器下,方案很简单:只需要监听 Ctrl + C 键盘事件深拷贝一份选中图形对象,然后再监听 Ctrl + V 事件,将拷贝出来的对象添加到图形树的末尾。
前端西瓜哥
2023/08/18
3450
图形编辑器开发:实现图形的复制粘贴
图形编辑器开发:缩放至适应画布
之前我们实现了画布缩放的功能,本文来讲讲如何让内容缩放至适应画布大小(Zoom to fit)。
前端西瓜哥
2023/08/18
2830
图形编辑器开发:缩放至适应画布
基于 HTML5 Canvas 的简易 2D 3D 编辑器
不管在任何领域,只要能让非程序员能通过拖拽来实现 2D 和 3D 的设计图就是很牛的,今天我们不需要 3dMaxs 等设计软件,直接用 HT 就能自己写出一个 2D 3D 编辑器,实现这个功能我觉得成
HT for Web
2018/01/03
2.3K0
基于 HTML5 Canvas 的简易 2D 3D 编辑器
简单介绍一下我在做的图形编辑器
我写的一系列图形编辑器的文章,是基于我一个叫做 suika 的个人项目总结抽象而来的。
前端西瓜哥
2023/09/11
4590
简单介绍一下我在做的图形编辑器
图形编辑器开发:自定义光标
它是一个指针,悬浮在屏幕的最上层。除了可以标记出指针的当前位置,同时也会通过它独特的样式,提示用户此时可以执行怎么的操作。
前端西瓜哥
2023/11/20
3300
图形编辑器开发:自定义光标
5-3 绘制图形
基本形状的绘制,我们可以从图形类提供的方法中找到解决方案,比如三角形即画三条相互连接的直线,心形则依次画几个半圆形组合,关键问题是找准其中的连接点位置,常见图形都可以通过基本方法调用画出。但是一些数学曲线的处理就较为繁琐,不是标准的形状组成,需要两点一线逐一绘制,这里我们以一些常用曲线及图表为例。
py3study
2020/01/08
1.5K0
5-3  绘制图形
图形编辑器开发:钢笔工具功能说明书
只有理解了需求,尤其是复杂的需求,才能更好地进行功能开发,写出诗一样的高鲁棒性代码。
前端西瓜哥
2024/01/26
2790
图形编辑器开发:钢笔工具功能说明书
创建canvas设置canvas尺寸绘制图形Canvas库
Canvas是常见的前端技术,但是由于API众多,使用复杂,且对程序员的数学功底、空间想象能力乃至审美都有一定要求,所以真正擅长canvas的前端并不多,但并不代表大家就学不好canvas。我在此将常用的canvas使用场景罗列出来希望能帮助到大家。
MudOnTire
2020/05/12
4.5K0
创建canvas设置canvas尺寸绘制图形Canvas库
如何在 Canvas 上实现图形拾取?
图形拾取,指的是用户通过鼠标或手指在图形界面上能选中图形的能力。图形拾取技术是之后的高亮图形、拖拽图形、点击触发事件的基础。
前端西瓜哥
2022/12/21
1.3K0
如何在 Canvas 上实现图形拾取?
Canvas 性能优化:脏矩形渲染
使用 Canvas 做图形编辑器时,我们需要自己维护自己的图形树,来保存图形的信息,并定义元素之间的关系。
前端西瓜哥
2022/12/21
1.4K0
Canvas 性能优化:脏矩形渲染
浅谈JavaScript的Canvas(绘制图形)
  HTML5中新增加的一个元素canvas,要使用canvas元素,浏览器必须支持html5。通过canvas标签来创建元素,并需要为canvas指定宽度和高度,也就是绘图区域的大小。 <canvas id="mycanvas" style="width:500px;height:500px;"></canvas> 。要在canvas上画图,需要取得canvas的上下文,通过getContext方法来获取上下文。 var context=canvas.getContext("2d"); 在使用getCon
水击三千
2018/02/27
1.7K0
浅谈JavaScript的Canvas(绘制图形)
图形编辑器开发:以光标为中心缩放画布
通过它,我们可以像举着一台摄影机,在图形所在的世界到处游逛,透过镜头,可以只看自己想看的图形;可以拉近摄影机,看到图形的细节;也可以拉远摄影机,总览多个图形之间的关系。
前端西瓜哥
2023/08/18
2390
图形编辑器开发:以光标为中心缩放画布
图形编辑器开发:参考线吸附效功能,让图形自动对齐
这里的参照线,指的是在移动目标图形时,当靠近其他图形的包围盒的延长线(看不见)时,会(1)绘制出最近的延长线和延长线上的点,(2)并将目标图形吸附上去,轻松实现(3)对齐的效果。
前端西瓜哥
2023/08/18
5780
图形编辑器开发:参考线吸附效功能,让图形自动对齐
(10月最新) 前端图形学实战: 从零开发几何画板(vue3 + vite版)
本文是 100+前端几何学应用案例 专栏的第二篇文章, 在第一篇文章几何学在前端边界计算中的应用和原理分析 中我介绍了几何学在前端领域里的应用, 同时用 vue3 带大家一起实现了常见图形的边界计算算法, 并且分享了如何用几何原理和Web Dom生成任意三角形的方式:
徐小夕
2022/12/22
9240
(10月最新) 前端图形学实战: 从零开发几何画板(vue3 + vite版)
相关推荐
图形编辑器开发:基于相交策略选中图形
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验