自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

圖形編輯器開發(fā):繪制圖形工具

開發(fā) 前端
模板模式的優(yōu)點(diǎn)是復(fù)用和擴(kuò)展。相同的主體框架邏輯不變,暴露幾個(gè)方法讓子類實(shí)現(xiàn),有些是必須實(shí)現(xiàn),有些是可實(shí)現(xiàn)可不實(shí)現(xiàn)(不實(shí)現(xiàn)用默認(rèn)算法),對(duì)我們實(shí)現(xiàn)一種通用的繪制圖形工具很有幫助。

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

今天來(lái)介紹如何實(shí)現(xiàn)圖形繪制工具,實(shí)現(xiàn)繪制任意的圖形。

編輯器 github 地址:

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

線上體驗(yàn):

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

我之前講過(guò)如何實(shí)現(xiàn)工具類管理類的:

《圖形編輯器:工具管理和切換》

對(duì)應(yīng)的工具類的實(shí)現(xiàn)會(huì)圍繞用戶的 按下鼠標(biāo)、拖拽、釋放 這 3 個(gè)行為,圖形繪制工具同樣如此。

整體框架:

// 繪制圖形工具類(這里用了抽象類,后面會(huì)說(shuō)為什么)
abstract class DrawGraphTool {
  // 工具被激活
  active() {
    // 通常是設(shè)置光標(biāo),或是綁定一些事件,比如鍵盤事件
  }
  // 工具失活
  inactive() {
    // 通常是解綁一些事件
  }
  
  // 鼠標(biāo)按下
  start() { /* TODO */ }
  // 鼠標(biāo)拖拽
  drag() { /* TODO */ }
  // 鼠標(biāo)釋放
  end() { /* TODO */ }
}

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

模板模式

圖形有很多種,矩形、橢圓、三角形、五角星等等。每個(gè)圖形都實(shí)現(xiàn)一遍未免有點(diǎn)繁瑣。

西瓜哥我一開始是分別去實(shí)現(xiàn)繪制矩形和橢圓的,然后發(fā)現(xiàn)有很多相同的邏輯。當(dāng)又要加一個(gè)新的圖形時(shí),又要復(fù)制粘貼,然后修改少量的不一樣的地方,這不利于代碼維護(hù)。

為解決這個(gè)問(wèn)題,我們要實(shí)現(xiàn)一個(gè) 繪制圖形基類,將共用邏輯放到里面,不同的部分則交給子類去實(shí)現(xiàn)。

這個(gè)在設(shè)計(jì)模式上叫做 模板模式。

所謂模板模式,就是在方法中定義一個(gè)  “算法” 骨架,繼承的子類在不改變算法整體結(jié)構(gòu)的情況下,重寫其中某些步驟(有些步驟有默認(rèn)實(shí)現(xiàn),可不重寫)。

模板模式的具體實(shí)現(xiàn),就是用 抽象類(abstract class) 去實(shí)現(xiàn)這個(gè)基類。

抽象類是一種不能被實(shí)例化的特殊類,繼承的子類才能實(shí)例化。

抽象類的方法可以是普通方法,也可以是只定義了方法類型簽名的抽象方法。

子類繼承抽象類時(shí),必須提供抽象類的抽象方法的具體實(shí)現(xiàn)。

TypeScript 支持抽象類。下面是一個(gè)例子。

// 抽象類
abstract class AbstractClass {
  say() {
    if (this.shoudISaySomething()) {
      console.log('前端西瓜哥')
    }
  }
  // 抽象方法(不能用 private,因?yàn)樽宇愐貙懰?  protected abstract shoudISaySomething(): boolean
}

class A extends AbstractClass {
  shoudISaySomething() {
    // ...假設(shè)這里一堆判斷
    return true
  }
}

子類不實(shí)現(xiàn)抽象方法的話,TS 編譯會(huì)報(bào)錯(cuò):

如果你用 JavaScript,雖然不能做編譯時(shí)的檢驗(yàn),但還可以做運(yùn)行時(shí)的檢測(cè)。

將需要子類繼承實(shí)現(xiàn)的方法,加入拋出錯(cuò)誤的實(shí)現(xiàn)。這樣子類如果沒(méi)實(shí)現(xiàn),就會(huì)通過(guò)原型鏈的方式,執(zhí)行基類的方法,然后報(bào)錯(cuò)提示給開發(fā)者。

class AbstractClass {
  say() {
    if (this.shoudISaySomething()) {
      console.log('前端西瓜哥')
    }
  }
  shoudISaySomething() {
    throw new Error('請(qǐng)實(shí)現(xiàn) shoudISaySomething 方法')
  }
}

class A extends AbstractClass {
  shoudISaySomething() {
    // ...假設(shè)這里一堆邏輯
    return true
  }
}

圖形繪制工具的實(shí)現(xiàn)

我們回到繪制圖形的業(yè)務(wù)邏輯。

我們?cè)谑髽?biāo)按下時(shí)確定起始坐標(biāo),拖拽時(shí)調(diào)整終點(diǎn)坐標(biāo),鼠標(biāo)釋放確認(rèn)終點(diǎn)坐標(biāo)。

這里產(chǎn)生了一個(gè)矩形框,得到 x、y、width、height,通過(guò)它們可以確定了一個(gè)圖形的位置和大小。

當(dāng)要加一個(gè)新的圖形時(shí),只要它能夠通過(guò) x、y、width、height 這幾個(gè)屬性確定繪制效果,那就可以使用這個(gè)基類。

如果這個(gè)圖形還有其他屬性,我們可以在繪制后通過(guò)其他方式(比如控制點(diǎn)或者面板修改值)去修改。

鼠標(biāo)按下

首先是鼠標(biāo)按下的邏輯。邏輯很少,主要是記錄起始點(diǎn)。

abstract class DrawGraphTool {
  commandDesc = 'Add Graph'; // 歷史記錄的命令描述
  protected drawingGraph: Graph | null = null; // 被繪制的圖形對(duì)象
  
  
  start(e: PointerEvent) {
    // 這里將光標(biāo)的視口坐標(biāo)轉(zhuǎn)成場(chǎng)景坐標(biāo)
    this.startPoint = this.editor.getSceneCursorXY(e);
    
    // 重置一些狀態(tài)
    this.drawingGraph = null;
  }
}

鼠標(biāo)拖拽

拖拽的時(shí)候,會(huì)判斷 this.drawingGraph 是否為 null。

如果是,就會(huì)創(chuàng)建一個(gè)新的圖形對(duì)象。如果不是,那就更新  this.drawingGraph 的 x、y、 width、height 屬性。

abstract class DrawGraphTool {
  private lastDragPoint!: IPoint;
  
  drag(e: PointerEvent) {
    // 記錄終點(diǎn)坐標(biāo)
    this.lastDragPoint = this.editor.getSceneCursorXY(e);
    this.updateRect();
  }
  
  // 更新矩形選框,并對(duì)圖形對(duì)象進(jìn)行操作
  private updateRect() {
    const { x, y } = this.lastDragPoint;
    const sceneGraph = this.editor.sceneGraph;
    const { x: startX, y: startY } = this.startPoint;

    const width = x - startX; // 這個(gè)可能是負(fù)數(shù),還沒(méi)做標(biāo)準(zhǔn)化
    const height = y - startY; // 同上

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

    // 按住shift鍵,通過(guò)算法把矩形變成方形。
    if (this.editor.hostEventManager.isShiftPressing) {
      this.adjustSizeWhenShiftPressing(rect);
    }

    if (this.drawingGraph) {
      // (1)更新圖形邏輯
      this.updateGraph(rect);
    } else {
      // (2)創(chuàng)建圖形邏輯
      const element = this.createGraph(rect)!;
      sceneGraph.addItems([element]);

      this.drawingGraph = element;
    }
    // 設(shè)置選中對(duì)象,并渲染
    this.editor.selectedElements.setItems([this.drawingGraph]);
    sceneGraph.render();
  }
}

創(chuàng)建圖形

創(chuàng)建圖形對(duì)象的方法是 createGraph(),要返回一個(gè)圖形對(duì)象,保存到 this.drawingGraph。

這個(gè)圖形對(duì)象需要子類來(lái)提供。所以寫成抽象方法:

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

我們的矩形繪制工具,實(shí)現(xiàn)如下。

export class DrawRectTool extends DrawGraphTool implements ITool {
 // ...
  
  // 這里提供實(shí)現(xiàn)創(chuàng)建圖形對(duì)象
  protected createGraph(rect: IRect) {
    rect = normalizeRect(rect);
    return new Rect({
      ...rect,
      fill: [cloneDeep(this.editor.setting.get('firstFill'))],
    });
  }
}

這里用 normalizeRect 對(duì) rect 對(duì)象做了標(biāo)準(zhǔn)化,原來(lái) width 和 height 可能為負(fù)數(shù),標(biāo)準(zhǔn)化就是改變 x、y,并讓 width 和 height 變回正數(shù),變成一個(gè)常規(guī)的 rect 對(duì)象。

這樣我們拿到了圖形對(duì)象通用屬性:x、y、width、height,然后這里再補(bǔ)上了一個(gè)默認(rèn)的填充色。

如果要實(shí)現(xiàn)繪制直線,就不要提供填充色,而是要補(bǔ)一個(gè)默認(rèn)描邊。

更新圖形

更新圖形通常就是更新一下圖形的 x、y、width、height 屬性,所以基類會(huì)提供一個(gè)默認(rèn)實(shí)現(xiàn)。

/**
 * 這個(gè)是通用邏輯,直接更新 x、y、width、height
 */
protected updateGraph(rect: IRect) {
  // 對(duì)矩形標(biāo)準(zhǔn)化
  rect = normalizeRect(rect);

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

當(dāng)然有些圖形并不是這樣的邏輯,那子類就需要重寫 updateGraph  方法。

比如繪制直線就比較特殊,它更新的是 width 和 rotation,height 則永遠(yuǎn)是 0,需要另寫一個(gè)算法去實(shí)現(xiàn)轉(zhuǎn)換。

Shift 模式

這里有個(gè)比較特別的效果,就是按住 Shift,會(huì)讓 圖形的寬高比保持一比一。

繪制正方形:

繪制圓形:

實(shí)現(xiàn)就是找 width 和 height 絕對(duì)值大的那一個(gè),然后符號(hào)保持不變,兩者的絕對(duì)值都變成這個(gè)最大值。

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() 方法可能會(huì)返回 0,所以要兜底為 1
  rect.height = (Math.sign(height) || 1) * size;
  rect.width = (Math.sign(width) || 1) * size;
}

子類如果比較特殊(沒(méi)錯(cuò)說(shuō)的就是你,直線工具),可重寫該方法。

順帶一提,還有一種 Alt 模式,會(huì)將起始點(diǎn)作為圖形的中心點(diǎn)進(jìn)行繪制,這個(gè)我還沒(méi)去實(shí)現(xiàn)。

鼠標(biāo)釋放

鼠標(biāo)釋放時(shí),主要邏輯是將新的狀態(tài)保持到歷史記錄中。

end(e: PointerEvent) {
  if (this.drawingGraph) {
    // 記錄新的狀態(tài)
    this.editor.commandManager.pushCommand(
      new AddShapeCommand(this.commandDesc, this.editor, [this.drawingGraph]),
    );
  }
}

結(jié)尾

模板模式的優(yōu)點(diǎn)是復(fù)用和擴(kuò)展。相同的主體框架邏輯不變,暴露幾個(gè)方法讓子類實(shí)現(xiàn),有些是必須實(shí)現(xiàn),有些是可實(shí)現(xiàn)可不實(shí)現(xiàn)(不實(shí)現(xiàn)用默認(rèn)算法),對(duì)我們實(shí)現(xiàn)一種通用的繪制圖形工具很有幫助。

實(shí)現(xiàn)了這個(gè)圖形繪制基類后,我們理論上就可以繪制任何圖形了,甚至用戶自定義的圖形,只要這些圖形對(duì)象使用 x、y、 width、height。

責(zé)任編輯:姜華 來(lái)源: 前端西瓜哥
相關(guān)推薦

2023-10-19 10:12:34

圖形編輯器開發(fā)縮放圖形

2009-10-23 16:43:01

VB.NET繪制圖形

2011-06-30 15:09:37

QT 繪制 圖形

2023-08-31 11:32:57

圖形編輯器contain

2013-12-27 13:00:30

Android開發(fā)Android應(yīng)用Context Men

2023-02-06 16:59:57

Canvas編輯器

2023-02-02 14:07:00

圖形編輯器Canvas

2023-09-26 07:39:21

2013-12-04 16:07:27

Android游戲引擎libgdx教程

2024-01-08 08:30:05

光標(biāo)圖形編輯器開發(fā)游標(biāo)

2023-09-11 09:02:31

圖形編輯器模塊間的通信

2023-06-12 08:22:56

圖形編輯器工具

2023-07-31 08:46:07

圖形編輯器圖形自動(dòng)對(duì)齊

2023-08-28 08:10:50

Hex圖形編輯器

2023-10-08 08:11:40

圖形編輯器快捷鍵操作

2023-10-10 16:04:30

圖形編輯器格式轉(zhuǎn)換

2023-02-09 07:02:30

圖形編輯器修改圖形

2023-04-07 08:02:30

圖形編輯器對(duì)齊功能

2023-01-04 11:18:21

Canvas 封裝pixi.js

2023-01-18 08:30:40

圖形編輯器元素
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)