【canvas学习笔记】2 - 图形编辑器主模块Editor的设计与实现

166 阅读5分钟

前言

在上一篇文章当中我简单介绍了编辑器的整体情况,感兴趣的小伙伴可以点击查看。

# 【canvas学习笔记】1 - 如何用canvas实现一个图形编辑器

这一篇文章我将会对主模块Editor做一个细致地讲解。

主模块的作用

Editor类其实并不复杂,目前实现它的代码大概也就在400行左右。但是它确实整个编辑器最重要的基石,因为编辑器所有的功能模块都会挂载在Editor实例下,也就是说Editor实现了一个串联的作用,将各司其职的模块们集中管理了起来。

Editor类包含哪些属性

class Editor {
  // 最底层canvas 负责绘制背景与网格
  rootCanvas: HTMLCanvasElement;
  private rootCtx: CanvasRenderingContext2D;

  // 主canvas,负责绘制各个图形
  mainCanvas: HTMLCanvasElement;
  private mainCtx: CanvasRenderingContext2D;

  // 顶层canvas,负责绘制选中的图形的边框、吸附线等其它交互相关的效果
  topCanvas: HTMLCanvasElement;
  private topCtx: CanvasRenderingContext2D;

  // 事件管理系统,负责鼠标、键盘事件的监听以及分发
  eventManager: EventManager;
  // 视口管理器,负责管理视口,包括平移、缩放等
  viewportManager: ViewportManager;
  // 操作管理器,负责管理操作历史,包括撤销、重做等
  actionManager: ActionManager;
  // 选中管理器,负责管理选中的图形,包括连选、点选、反选等操作
  selectionManager: SelectionManager;
  // 网格管理器,负责绘制网格
  grid: Grid;

  // 脏列表,记录哪些图形发生了变化,需要重新绘制
  private dirtyList: Set<Model> = new Set();
  // 标记是否全量绘制(当窗口发生平移、缩放时,需要标记全量重绘)
  private paintAll: boolean = true;
  // 在每次图形被重绘时,将其的renderRect缓存起来,用来做脏矩形检测
  private frameRects: Map<string, IRect> = new Map();
  // 插件
  private plugins: EditorPlugin<BasePluginEvents>[] = [];
  // 标记插件是否需要重绘
  private pluginsDirty = true;
  // 标记是否显示性能数据
  private showPerformance: boolean = true;
  // 管理所有图形的容器
  public box = new Box();
}

Editor在实例化时做了哪些事情

class Editor {
  constructor(public container: HTMLElement) {
    container.style.position = 'relative';
    [this.rootCanvas, this.rootCtx] = this.createCanvas();
    [this.mainCanvas, this.mainCtx] = this.createCanvas();
    [this.topCanvas, this.topCtx] = this.createCanvas();
    this.resize();

    this.actionManager = new ActionManager();

    this.eventManager = new EventManager(this);
    this.installPlugin(this.eventManager);

    this.selectionManager = new SelectionManager(this);
    this.installPlugin(this.selectionManager);

    this.viewportManager = new ViewportManager(this);
    this.installPlugin(this.viewportManager);
    this.viewportManager.on(CommonEvents.change, () => {
      this.paintAll = true;
      this.pluginsDirty = true;
    });

    this.grid = new Grid(this);
    this.installPlugin(this.grid);

    this.box.on(BoxEvents.modelsChange, models => {
      models.forEach(m => this.dirtyList.add(m));
    });
    this.box.on(BoxEvents.removeModels, models => {
      models.forEach(m => this.frameRects.delete(m.id));
    });

    requestAnimationFrame(this.repaint);
  }
}

根据代码可以看到,在Editor的构造函数里,先是生成了三个canvas然后插入到container容器中,随后是调用了resize方法设置canvas的大小。

随后是对各个子模块进行了实例化,并将部分模块放入了插件列表当中。

完成实例化以后,Editor监听了Box里的元素变化事件,这个事件由移除元素、添加元素以及元素自身的change事件触发。Editor会将发生了变化的元素加入到脏列表中,并在下一次repaint的时候重绘这些元素。

同时Editor也监听了移除元素的事件,当元素被移除时,将其对应的renderRect缓存也一起移除。

重要的方法

  1. repaint 负责图形与插件的绘制,每一帧都会调用。
  2. getElementAt 获取当前鼠标下的图形。
  3. fullRepaint 全量重绘。
  4. partRepaint 部分重绘。
  5. findDirtyRect 根据脏元素集合,通过不断地遍历所有矩形来找到需要重绘的图形。例如A元素的外观发生改变了,当A需要重绘时,如果B元素与A相连,那么B元素也应该被重新绘制。
  6. getSceneData: 获取当前场景数据,包括每一个图形的json描述以及当前视图坐标、大小缩放值等。
  7. parseSceneData: 将json数据解析为图形并渲染。
  8. dispose: 释放销毁所有的模块。

插件

关于编辑器的插件设计,我目前的想法是,对于网格绘制、框选的绘制、所有选中元素的包围盒绘制以及吸附时的辅助线绘制,这些内容我希望交给插件处理,因为如果把这些逻辑全部放在Editor当中的话,势必会导致Editor模块过于复杂,违背了【高内聚低耦合】的设计原则。

所以我希望有一个机制来让其它模块能够参与到Editor的绘制中去,而这个机制目前就是靠插件系统来实现。

插件的类型定义:

import { EventEmitter } from '@stom/shared';
import { CommonEvents } from './models';

export interface BasePluginEvents {
  [CommonEvents.REPAINT](): void;
  [K: string]: any;
}

export interface EditorPlugin<T extends BasePluginEvents> extends EventEmitter<T> {
  paintTop(ctx: CanvasRenderingContext2D): void;
  paintRoot(ctx: CanvasRenderingContext2D): void;
  dispose(): void;
}

插件必须要继承EventEmitter类,并且有一个REPAINT事件,这个事件是用来让Editor知道该在什么时候重新绘制插件的。

插件还必须要包含三个方法,分别是

  1. paintTop:在顶层canvas上的绘制逻辑。
  2. paintRoot:在底层canvas上的绘制逻辑。
  3. dispose:销毁事件。

目前的这个设计虽然已经基本达到了我的目的,但是仍然有许多缺陷:

  1. 对于大部分插件来讲,paintTop和paintRoot方法是否只需要有一个就够了,毕竟很少有插件需要在两层canvas上都进行绘制。
  2. 插件的重绘该如何优化,目前的实现是只要任意一个插件触发了REPAINT事件,那么所有的插件都会重新绘制,这可能会造成不必要的性能损耗。
  3. 如果有开发者需要对编辑器进行拓展,那么他应该怎么去二次开发一个插件并完成优雅地注册?

目前这三个问题我暂时都没有想到好的解决方案,如果哪位大佬有什么建议也希望能够在评论区留言。

结束语

editor.ts文件的完整代码: github.com/kkagura/sto…

希望各位看官大佬们可以帮忙点个赞,如果有什么想法的话也欢迎评论区留言。