当前位置:首页 >知识 >图形编辑器开发:绘制图形工具 相同的编辑主体框架逻辑不变

图形编辑器开发:绘制图形工具 相同的编辑主体框架逻辑不变

2024-06-30 22:46:56 [百科] 来源:避面尹邢网

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

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

大家好,图形我是编辑前端西瓜哥。

图形编辑器开发:绘制图形工具 相同的编辑主体框架逻辑不变

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

图形编辑器开发:绘制图形工具 相同的编辑主体框架逻辑不变

编辑器 github 地址:

图形编辑器开发:绘制图形工具 相同的编辑主体框架逻辑不变

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

线上体验:

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

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

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

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

整体框架:

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

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

模板模式

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

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

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

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

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

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

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

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

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

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

// 抽象类abstract class AbstractClass {   say() {     if (this.shoudISaySomething()) {       console.log('前端西瓜哥')    }  }  // 抽象方法(不能用 private,因为子类要重写它)  protected abstract shoudISaySomething(): boolean}class A extends AbstractClass {   shoudISaySomething() {     // ...假设这里一堆判断    return true  }}

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

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

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

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 这几个属性确定绘制效果,那就可以使用这个基类。

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

鼠标按下

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

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 属性。

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。

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

protected abstract createGraph(rect: IRect, noMove?: boolean): Graph | null;

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

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 属性,所以基类会提供一个默认实现。

/** * 这个是通用逻辑,直接更新 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 绝对值大的那一个,然后符号保持不变,两者的绝对值都变成这个最大值。

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 模式,会将起始点作为图形的中心点进行绘制,这个我还没去实现。

鼠标释放

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

end(e: PointerEvent) {   if (this.drawingGraph) {     // 记录新的状态    this.editor.commandManager.pushCommand(      new AddShapeCommand(this.commandDesc, this.editor, [this.drawingGraph]),    );  }}

结尾

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

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

责任编辑:姜华 来源: 前端西瓜哥 图形编辑器开发绘制图形工具

(责任编辑:探索)

    推荐文章
    热点阅读