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

【图形编辑器:工具管理和切换】大家好,我是前端西瓜哥 。今天我们看看对于一款图形编辑器,应该怎么去实现工具,比如绘制矩形、选中工具,以及如何去管理它们的 。
项目地址,欢迎 star:
线上体验:
一款编辑器,有两个很重要的方面,一个是性能,另一个是架构 。
因为不知道用户会在画布上画上多少图形,所以需要在渲染引擎上下很大的功夫,去提高绘制的性能 。性能决定了编辑器的上限,这也是为什么很多编辑器选择了作为绘制方案 。
另一个则是架构,编辑器很复杂,即便是看上去很简单编辑器 。因为里面的模块非常多,比如工具管理模块、缩放管理、历史记录、图形树维护、辅助线、标尺、设置、视口管理、热键、光标维护等等 。如果模块化不够好,就会导致代码扩展性差,加功能会非常痛苦 。
今天西瓜哥谈谈如何设计管理工具类,管理不同的工具 。
工具类
工具的交互,通常会集中于用户的鼠标操作 。
比如绘制矩形,按下鼠标,会确定矩形的 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 表示工具名称,是一个标识符,切换工具时会用到 。
方法会在切换为当前工具时调用,通常会做的事情有:
设置光标样式;设置一些监听器,比如绘制矩形监听 shift 键是否按下,如果按下,就绘制方形;
会在切换为其他工具时调用,通常就是将光标设置为默认值,取消监听器 。
start 是鼠标按下事件,此时要记录一些初始状态,后面的事件需要基于这个初始状态进行计算 。这里其实我没用鼠标事件,而是用了指针事件,一种适用范围更广的事件,除了鼠标事件,还支持触控笔和触摸屏幕等场景 。因为大家习惯鼠标事件,后面我都用鼠标事件来描述 。
drag 就是鼠标拖拽事件 。end 是鼠标释放事件 。
图形编辑器:工具管理和切换

文章插图
最后是比较特殊的,代表除了拖拽场景的鼠标移动,比如选择工具,悬停在一个图形上,我们就可以用这个事件来判断是哪个图形被选中,对它进行高亮 。
这就是最基本的工具类,在此上我们可以进行进一步地封装,比如更改光标样式,我们可以配个 、 属性,让调用者帮我们统一设置光标样式 。
这里的调用者就是工具管理类 。
工具管理类
工具管理类支持的能力:
维护映射表,用 type 映射到对应工具实例;使用方法切换工具,会根据传入的字符串在映射表中找到对应工具实例,然后调用旧的工具的方法,再调用新工具的方法,然后设置 this. 为新工具实例;支持事件订阅,监听工具的切换,提供给 UI 层去监听 。比如我们用快捷键切换工具时,UI 层就能通过监听获得最新工具标识符,将对应按钮设置为激活状态;然后是给 DOM 元素挂载监听器,上挂载鼠标按下事件,然后是特殊的,给挂载鼠标移动和鼠标 。为什么不给挂载这些事件,这是因为我们可能会在拖拽时将鼠标移出甚至浏览器界面然后释放,会导致拖拽、释放事件没能触发 。监听后,就会在何时的时机调用工具类的 start、drag、end 等方法 。