大家好,我是前端西瓜哥。今天我们看看对于一款图形编辑器,应该怎么去实现工具,比如绘制矩形、选中工具,以及如何去管理它们的。

项目地址,欢迎 star:

https://github.com/F-star/suika

线上体验:

https://blog.fstars.wang/app/suika/

一款编辑器,有两个很重要的方面,一个是性能,另一个是架构。

因为不知道用户会在画布上画上多少图形,所以需要在渲染引擎上下很大的功夫,去提高绘制的性能。性能决定了编辑器的上限,这也是为什么很多编辑器选择了 Canvas 作为绘制方案。

另一个则是架构,编辑器很复杂,即便是看上去很简单编辑器。因为里面的模块非常多,比如工具管理模块、缩放管理、历史记录、图形树维护、辅助线、标尺、设置、视口管理、热键、光标维护等等。如果模块化不够好,就会导致代码扩展性差,加功能会非常痛苦。

今天西瓜哥谈谈如何设计管理工具类,管理不同的工具。

工具类

工具的交互,通常会集中于用户的鼠标操作

比如绘制矩形,按下鼠标,会确定矩形的 x、y 值,然后拖拽鼠标,调整矩形的宽高,最后放开鼠标,矩形的形状就确认好了,并将这个绘制矩形的操作记录到历史操作中。如下图:

所以,工具类(Tool)的设计为:

export interface ITool {type: string; // 工具类active: () => void; // 切换为当前工具时调用inactive: () => void; // 切换为其他工具时调用start: (event: PointerEvent) => void; // 鼠标按下drag: (event: PointerEvent) => void; // 拖拽end: (event: PointerEvent) => void; // 鼠标释放moveExcludeDrag: (event: PointerEvent) => void; // 拖拽之外的鼠标移动
}class DrawRectTool implements ITool {// ...
}

有点像我们 Rect 和 Vue 中的组件的概念。这是因为工具类本质也是 在生命周期内触发一些钩子(hook),拿到一些信息。

type 表示工具名称,是一个标识符,切换工具时会用到。

active 方法会在切换为当前工具时调用,通常会做的事情有:

  1. 设置光标样式;
  2. 设置一些监听器,比如绘制矩形监听 shift 键是否按下,如果按下,就绘制方形;

inactive 会在切换为其他工具时调用,通常就是将光标设置为默认值,取消监听器。

start 是鼠标按下事件,此时要记录一些初始状态,后面的事件需要基于这个初始状态进行计算。这里其实我没用鼠标事件,而是用了 pointer 指针事件,一种适用范围更广的事件,除了鼠标事件,还支持触控笔和触摸屏幕等场景。因为大家习惯鼠标事件,后面我都用鼠标事件来描述。

drag 就是鼠标拖拽事件。end 是鼠标释放事件。

最后是比较特殊的 moveExcludeDrag,代表除了拖拽场景的鼠标移动,比如选择工具,悬停在一个图形上,我们就可以用这个事件来判断是哪个图形被选中,对它进行高亮。

这就是最基本的工具类,在此上我们可以进行进一步地封装,比如更改光标样式,我们可以配个 normalCursor、dragCursor 属性,让调用者帮我们统一设置光标样式。

这里的调用者就是工具管理类。

工具管理类

工具管理类支持的能力:

  1. 维护映射表,用 type 映射到对应工具实例;
  2. 使用 setTool 方法切换工具,会根据传入的字符串在映射表中找到对应工具实例,然后调用旧的工具的 inactive 方法,再调用新工具的 active 方法,然后设置 this.currentTool 为新工具实例;
  3. 支持事件订阅,监听工具的切换,提供给 UI 层去监听。比如我们用快捷键切换工具时,UI 层就能通过监听获得最新工具标识符,将对应按钮设置为激活状态;
  4. 然后是给 DOM 元素挂载监听器,canvas 上挂载鼠标按下事件,然后是特殊的,给 window 挂载鼠标移动和鼠标。为什么不给 canvas 挂载这些事件,这是因为我们可能会在拖拽时将鼠标移出 canvas 甚至浏览器界面然后释放,会导致拖拽、释放事件没能触发。监听后,就会在何时的时机调用工具类的 start、drag、end 等方法。

ToolManager 实现如下:

class ToolManager {toolMap = new Map<string, ITool>();currentTool: ITool | null = null;eventEmitter: EventEmitter;_unbindEvent: () => void;constructor(private editor: Editor) {this.eventEmitter = new EventEmitter(); // 模仿 nodejs 的简易版 EventEmitter// 绑定 toolthis.toolMap.set(DrawRectTool.type, new DrawRectTool(editor));this.toolMap.set(DrawEllipseTool.type, new DrawEllipseTool(editor));this.toolMap.set(SelectTool.type, new SelectTool(editor));this.toolMap.set(DragCanvasTool.type, new DragCanvasTool(editor));this.setTool(DrawRectTool.type);this._unbindEvent = this.bindEvent();}getToolName() {return this.currentTool?.type;}bindEvent() {let isPressing = false;const handleDown = (e: PointerEvent) => {if (e.button !== 0) { // 必须是鼠标左键return;}if (!this.currentTool) {throw new Error('未设置当前使用工具');}isPressing = true;this.currentTool.start(e);};const handleMove = (e: PointerEvent) => {if (!this.currentTool) {throw new Error('未设置当前使用工具');}if (isPressing) {this.editor.hostEventManager.disableDragBySpace();this.currentTool.drag(e);} else {this.currentTool.moveExcludeDrag(e);}};const handleUp = (e: PointerEvent) => {if (e.button !== 0) { // 必须是鼠标左键return;}if (!this.currentTool) {throw new Error('未设置当前使用工具');}if (isPressing) {this.editor.hostEventManager.enableDragBySpace();isPressing = false;this.currentTool.end(e);}};const canvas = this.editor.canvasElement;canvas.addEventListener('pointerdown', handleDown);window.addEventListener('pointermove', handleMove);window.addEventListener('pointerup', handleUp);return function unbindEvent() {canvas.removeEventListener('pointerdown', handleDown);window.removeEventListener('pointermove', handleMove);window.removeEventListener('pointerup', handleUp);};}unbindEvent() {this._unbindEvent();this._unbindEvent = noop;}setTool(toolName: string) {const prevTool = this.currentTool;const currentTool = this.currentTool = this.toolMap.get(toolName) || null;if (!currentTool) {throw new Error(`没有 ${toolName} 对应的工具对象`);}prevTool && prevTool.inactive();currentTool.active();this.eventEmitter.emit('change', currentTool.type);}on(eventName: 'change', handler: (toolName: string) => void) {this.eventEmitter.on(eventName, handler);}off(eventName: 'change', handler: (toolName: string) => void) {this.eventEmitter.off(eventName, handler);}destroy() {this.currentTool?.inactive();}
}

结尾

工具管理类基础的设计就是这样。因为是基于生命周期去设计的,所以看起来挺像 React、Vue 的组件写法的。

我是前端西瓜哥,欢迎关注我,学习更多前端知识。

图形编辑器:工具管理和切换相关推荐

  1. 使用图形工具管理Server Core上的账号和组

    1.1 管理Server Core上的账号和组 由于Windows Serverr Core操作系统只有命令行界面,因此用户帐户和组的管理只能在命令行下实现.也可以在有图形界面的Windows Ser ...

  2. GPS数据矢量化JAVA_SVGX矢量化图形编辑器,100%JAVA实现的矢量化图形编辑器

    SVGX矢量化图形编辑器,100%JAVA实现的矢量化图形编辑器 SVGX矢量化图形编辑器是面向工程应用的矢量图形制作软件,基于著名的Eclipse GEF图形编辑框架实现了W3C SVG 1.1规范 ...

  3. 配置编辑器工具 (SvcConfigEditor.exe)位置和简介

    配置编辑器工具 (SvcConfigEditor.exe) 通过 Windows Communication Foundation (WCF) 服务配置编辑器 (SvcConfigEditor.exe ...

  4. 数据库、数据库管理系统、SQL和图形界面工具的关系

    写在前面:博主是一只经过实战开发历练后投身培训事业的"小山猪",昵称取自动画片<狮子王>中的"彭彭",总是以乐观.积极的心态对待周边的事物.本人的技 ...

  5. 如何使用图形界面Webmin管理linux服务器

    出处:http://linux.cn/thread/11992/1/1/ 如何使用图形界面Webmin管理linux服务器 一台典型的linux服务器运行命令行环境中,并已经包括了一些用于安装和配置各 ...

  6. WPF学习12:基于MVVM Light 制作图形编辑工具(3)

    本文是WPF学习11:基于MVVM Light 制作图形编辑工具(2)的后续 这一次的目标是完成 两个任务. 本节完成后的效果: 本文分为三个部分: 1.对之前代码不合理的地方重新设计. 2.图形可选 ...

  7. Oracle(3)——Oracle图形界面工具创建数据库

    具体操作步骤详情: 1.图形界面工具首界面 Database Configuration Assistant 点击下一步 2.默认 点击下一步 3.默认 点击下一步 4.设置全局数据库名.SID 为新 ...

  8. canvas图形编辑器

      原文地址:http://jeffzhong.space/2017/11/02/drawboard/   使用canvas进行开发项目,我们离不开各种线段,曲线,图形,但每次都必须用代码一步一步去实 ...

  9. Grafana 仪表盘和图形编辑器

    Grafana 是一个跨平台.开源的数据可视化网络应用程序平台.用户配置连接的数据源之后,Grafana 可以在浏览器显示数据图表和警告.该软件的企业版本提供更多的扩展功能.扩展功能通过插件的形式提供 ...

最新文章

  1. springboot tomcat配置_用了 10 多年的 Tomcat 居然有bug !
  2. php打开gd和mysql_PHP怎么开启mysql, gd, curl, mbstring支持?
  3. Kotlin——初级篇(一):最详细的环境搭建
  4. C#_基础:排序算法
  5. linux纯没网安装mysql_实用性Linux安装mysql
  6. 西门子滚筒洗衣机教程_西门子洗衣机优缺点
  7. Androidstudio控制台分层输出接口日志.类似BeJSON,HiJson格式化JSON
  8. oracle数据库基础知识
  9. Codesys中国官网下载中心
  10. 长连接、短连接和心跳(有图有案例)
  11. 相机的光圈、快门、ISO到底是什么鬼?
  12. C语言程序设计实践教程 邹显春pdf
  13. office365:PowerPoint新功能设计器,让排版更轻松
  14. 好用的可视化报表在线生成工具
  15. 同翔网浅析RoCE网络技术
  16. linux 常用命明
  17. 揭秘网易云音乐的个性化推荐算法【黑科技】
  18. 通信原理 实验:加入m序列、扰码、扩频、卷积码以及维特比译码功能的数字基带系统仿真
  19. 收集一些优秀的甲方安全开源项目
  20. 【IM】极光简单的聊天测试

热门文章

  1. 《ELK Stack权威指南 》第3章 场景示例
  2. 应对高薪中年失业和留学断供潮危机的建议
  3. layui自定义校验提示文字
  4. 2022-23 年电子邮箱哪个好用?邮箱大全测评来了,请及时查看哦
  5. 苹果公司布局流媒体业务 | 经济学人全球早报精选
  6. 信奥中的数学:平面直角坐标系
  7. 流量壁垒消失后,电商风口重启
  8. 传谷歌计划将亚洲总部搬离上海
  9. 【整理】Word OpenXML常用标签
  10. 搜狗林飞:effevo优化升级 云协同提升用户体验