目录

【Fabric.js】常用图形交互式即时绘制指南(上)

前言

本篇博客主要介绍使用 fabric.js 在画布中绘制常见的图形,主要包括有:

  • 绘制直线
  • 绘制折线
  • 绘制矩形
  • 绘制圆形
  • 绘制点
  • 绘制多边形
  • 放置可编辑文本对象

出于通用性考虑,本次封装将会通过面向对象的方式,所以限于篇幅,本次主题将会分成上下两篇。

上篇主要实现类的封装,初始化及通用函数的实现。

下篇则是具体的图形绘制实现。

效果展示

展示效果采用公司已经实现的业务系统(顺便装个B),不过本篇博客主要聚焦在图形的绘制上,且会在 demo 项目中对代码进行重新实现并且省略非关键代码。

工具栏上的其余功能及红外温度计算等代码由于涉及商业,也不会在本篇博客中涉及到。

https://wumanhoblogimg.obs.cn-south-1.myhuaweicloud.com/images/fabricjs/drawing.gif

 

基础对象

出于通用性考虑,本次封装将会通过面向对象的方式,所有绘制操作都在 IFabric 类的内部实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { fabric } from 'fabric'
import { Emitter } from './Emitter.js'

// 设置选中样式
fabric.Object.prototype.set({
  transparentCorners: false,
  borderColor: '#51B9F9',
  cornerColor: '#FFF',
  borderScaleFactor: 2.5,
  cornerStyle: 'circle',
  cornerStrokeColor: '#0E98FC',
  borderOpacityWhenMoving: 0.6
})

class IFabric extends Emitter{
  constructor(container) {
    super()
    // 创建实例
    this._canvas = new fabric.Canvas(container, {
      fireRightClick: true,  // 监听右键事件
      stopContextMenu: true  // 取消右键默认事件
    })
    // 设置选中时不会置顶图形
    this._canvas.set('preserveObjectStacking', true)
    // 禁止多选
    this._canvas.set('selection', false)  
  }
    
  /**
   * 绘制图形入口
   * @param {* String} shape 形状
   */
  handleDraw(shape) {
    // 指针样式
    this._canvas.defaultCursor = "crosshair";
    switch (shape) {
      case "rect":
        this._drawRect();
        break;
      case "circle":
        this._drawCircle();
        break;
      case "text":
        this._canvas.defaultCursor = "text";
        this._genText();
        break;
      case "polyLine":
        this._drawPolyline();
        break;      
      case "point":
        this._drawPoint();
        break;
      case "line":
        this._drawLine();
        break;
      case "polygon":
        this._drawPolygon();
    }
  }

  _drawPolygon() {}

  _drawLine() {}

  _drawRect() {}

  _genText() {}

  _drawPoint() {}

  _drawCircle() {}
    
  _drawPolyline() {}  
}

export default IFabric

Emitter 用于实现对象的事件派发和监听,基于开源库 tiny-emitter

右键事件可以用来做取消绘制之类的交互,或者用于实现自己的右键菜单,所以建议把默认事件取消了。

图形添加方法

为了方便使用以及在业务代码中加入自己的逻辑,我们可以将添加图形的动作抽取成一系列的方法。

举个例子,当我们添加一个矩形到画布中时,可以将当前添加的状态设置为矩形rect,用作业务逻辑判断,添加其他图形也是一样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class IFabric extends Emitter{
  constructor(container, url) {
      /** ... **/
    // 记录当前添加的图形类型  
    this._activeShape = ''
  }
    
  get activeShape() {
    return this._activeShape
  }
    
  // 添加一个矩形到画布中
  addRectToCanvas(options) {
    this._activeShape = 'rect'
    new fabric.Rect(options)
  }

  // 添加一个圆形到画布中
  addCircleToCanvas(options) {
    this._activeShape = 'circle'
    new fabric.Circle(options)
  }

  // 添加点到画布中
  addCrossToCanvas(path, options) {
    this._activeShape = "cross";
    new fabric.Path(path, options);
  }

  // 添加多边形到画布中
  addPolygonToCanvas(points, options) {
    this._activeShape = 'polygon'
    new fabric.Polygon(points, options)
  }

  // 添加直线到画布中
  addLineToCanvas(linePath, options) {
    this._activeShape = 'line'
    new fabric.Line(linePath, options)
  }
  
  // 添加折线到画布中
  addPolyLineToCanvas(points, options) {
    this._activeShape = "polyline";
    new fabric.Polyline(points, options);
  }  

  // 添加文本到画布中
  addTextToCanvas(text, options) {
    this._activeShape = 'text'
    new fabric.IText(text, options)
  }
}

统一添加方法

在创建对象之后,需要将图形添加到画布中,我们可以实现一个统一的添加方法,在添加到画布中的时候,加上业务逻辑。

例如:在添加之前,为每个对象创建一个 uuid,将 uuid 返回给调用者。添加标识对象类型的属性logType,并且维护一个对象数据,将所有对象保存起来供业务使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { v4 as uuid } from "uuid";

class IFabric extends Emitter {
  constructor(container) {
    /** ... **/
    this._elements = [];
  }

  get elements() {
    return this._elements;
  }
  // 添加一个矩形到画布中
  addRectToCanvas(options) {
    this._activeShape = "rect";
    const rect = new fabric.Rect(options);
    return this._addElementToCanvas(rect);
  }

  // 添加一个圆形到画布中
  addCircleToCanvas(options) {
    this._activeShape = "circle";
    const circle = new fabric.Circle(options);
    return this._addElementToCanvas(circle);
  }

  // 添加点到画布中
  addCrossToCanvas(path, options) {
    this._activeShape = "cross";
    const cross = new fabric.Path(path, options);
    return this._addElementToCanvas(cross);
  }

  // 添加多边形到画布中
  addPolygonToCanvas(points, options) {
    this._activeShape = "polygon";
    const polygon = new fabric.Polygon(points, options);
    return this._addElementToCanvas(polygon);
  }

  // 添加直线到画布中
  addLineToCanvas(linePath, options) {
    this._activeShape = "line";
    const line = new fabric.Line(linePath, options);
    return this._addElementToCanvas(line);
  }
    
  // 添加折线到画布中
  addPolyLineToCanvas(points, options) {
    this._activeShape = "polyline";
    const polyLine = new fabric.Polyline(points, options);
    return this._addElementToCanvas(polyLine);
  }  

  // 添加文本到画布中
  addTextToCanvas(text, options) {
    this._activeShape = "text";
    const content = new fabric.IText(text, options);
    return this._addElementToCanvas(content);
  }

  // 添加已创建的对象到画布中  
  _addElementToCanvas(el) {
    // 创建一个 uuid
    const uid = uuid();
    el.uid = uid;
    // 为每个对象添加一个标识类型的字段
    if (!el.logType) {
      el.logType = this._activeShape;
    }
    this._canvas.add(el);
    this._elements.push(el);
    return uid;  
  }
}

对象关联事件

最后,在添加对象时,可以顺便将对象关联上事件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { v4 as uuid } from "uuid";

class IFabric extends Emitter {  

  /** ... **/  

  // 添加已创建的对象到画布中  
  _addElementToCanvas(el) {
    // 创建一个 uuid
    const uid = uuid();
    el.uid = uid;
    if (!el.logType) {
      el.logType = this._activeShape;
    }
    this._canvas.add(el);
    this._elements.push(el);
    this.hookEvents(el);
    return uid;  
  }

  // 为添加到画布的对象关联选择、移动、缩放事件
  hookEvents(target) {
    target.on({
      moving: this.emitEvent.bind(this),
      scaling: this.emitEvent.bind(this),
      selected: () => {
        this.emit("object-selected", target.uid);
      },
    });
  }

  // 选择对象、移动和缩放都会进入此方法
  emitEvent() {
    const activeEl = this._canvas.getActiveObject();
    this.emit("shape-change", activeEl);
  }    
}

 

公共操作方法

有了以上的准备,我们就可以扩展出很多公共的操作方法,方便调用者进行操作了。

举个例子,当用户要将所有文字元素重新更换一个颜色,那么可以提供一个resetFontColor方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class IFabric extends Emitter {
  // 设置文字颜色
  setFontSize(color) {
    // 遍历所有元素
    this._elements.forEach((el) => {
      // 找到 type 为 text 的元素
      if (el.logType === 'text') {
        el.set({
          fill: color
        })
      }
    })
  }
}

根据 id 获取元素:

1
2
3
4
5
6
class IFabric extends Emitter {
  
  getCanvasElementById(uid) {
    return this._elements.find((el) => el.uid === uid);
  }
}

移除指定元素:

1
2
3
4
5
6
7
8
class IFabric extends Emitter {
  
  removeElementById(uid) {
    const el = this.getCanvasElementById(uid);
    this._canvas.remove(el);
    this._elements = this._elements.filter((el) => el.uid !== uid);
  }
}

 

总结

至此,我们便拥有了一个易于扩展的类,为后续的实现打好了基础。

在下一篇博客中,会对本次主题最关键的图形绘制功能进行一一实现。

 

(上篇完)