个人技术站点JASONWU

Keep Coding


  • 主页

  • 文章

  • 搜索

HTML Canvas:FabricJS

新建: 2021-06-11 编辑: 2021-06-19   |   分类: JavaScript   | 字数: 1457 字

Fabric.js v4.4.0 实现画笔、文本、直线及椭圆等绘制的示例代码。

index.html

HTML
 1<!DOCTYPE html>
 2<html>
 3  <head>
 4    <meta charset="UTF-8">
 5    <title>FabricJS</title>
 6    <style>
 7      .fabric-toolbar {
 8        width: 720px;
 9        position: absolute;
10        left: 0;
11        right: 0;
12        margin: auto;
13        z-index: 1;
14
15        display: grid;
16        grid-template-rows: 1fr 1fr;
17        grid-template-columns: repeat(7, 1fr);
18        border: 1px solid black;
19        grid-gap: 10px;
20        background-color: black;
21      }
22
23      .fabric-toolbar > div {
24        background-color: white;
25        padding: 15px;
26        text-align: center;
27      }
28
29      .fabric-toolbar__tool {
30        cursor: pointer;
31      }
32
33    </style>
34  </head>
35  <body>
36    <div class="fabric-toolbar">
37      <div class="fabric-toolbar__tool js-mode" data-mode="draw">
38        画笔
39      </div>
40      <div class="fabric-toolbar__tool js-mode" data-mode="text">
41        文本
42      </div>
43      <div class="fabric-toolbar__tool js-mode" data-mode="line">
44        直线
45      </div>
46      <div class="fabric-toolbar__tool js-mode" data-mode="ellipse">
47        椭圆
48      </div>
49      <div class="fabric-toolbar__tool js-mode" data-mode="rect">
50        矩形
51      </div>
52      <div class="fabric-toolbar__tool js-mode" data-mode="eraser">
53        像皮擦
54      </div>
55      <div id="js-undo" class="fabric-toolbar__tool">上一步</div>
56
57      <div>
58        <input type="radio" name="width" data-value="6" checked>6px
59      </div>
60      <div><input type="radio" name="width" data-value="10">10px</div>
61      <div><input type="radio" name="width" data-value="14">14px</div>
62
63      <div style="background-color: #e74c3c;">
64        <input type="radio" name="color" data-value="#e74c3c" checked>
65      </div>
66      <div style="background-color: #2ecc71;">
67        <input type="radio" name="color" data-value="#2ecc71">
68      </div>
69      <div style="background-color: #3498db;">
70        <input type="radio" name="color" data-value="#3498db">
71      </div>
72      <div style="background-color: #e67e22;">
73        <input type="radio" name="color" data-value="#e67e22">
74      </div>
75    </div>
76
77    <canvas id="js-canvas"></canvas>
78
79    <script src="fabric.min.js"></script>
80    <script src="index.js" type="module"></script>
81  </body>
82</html>

index.js

JavaScript
  1class Main {
  2  modeEls = document.querySelectorAll('.js-mode');
  3  undoEls = document.getElementById('js-undo');
  4  widthEls = document.querySelectorAll('input[name="width"]');
  5  colorEls = document.querySelectorAll('input[name="color"]');
  6
  7  canvas;
  8
  9  _width = 6;
 10  _color = '#e74c3c';
 11  _histories = [];
 12  _modes = {
 13    draw: 'draw',
 14    text: 'text',
 15    line: 'line',
 16    ellipse: 'ellipse',
 17    rect: 'rect',
 18    eraser: 'eraser'
 19  };
 20  _curMode;
 21
 22  constructor() {
 23    this._init();
 24    this._toggleModeClick();
 25    this._handleUndoClick();
 26    this._handleRadioChange();
 27  }
 28
 29  _init() {
 30    this.canvas = new fabric.Canvas('js-canvas', {
 31      width: window.innerWidth,
 32      height: window.innerHeight,
 33      isDrawingMode: true,
 34      selection: false
 35    });
 36
 37    this.canvas.freeDrawingBrush.width = this._width;
 38    this.canvas.freeDrawingBrush.color = this._color;
 39
 40    // 记录操作历史
 41    this.canvas.on('object:added', () => {
 42      this._histories.push(JSON.stringify(this.canvas));
 43    });
 44
 45    this._registerCanvasEvent();
 46  }
 47
 48  _toggleModeClick() {
 49    this.modeEls.forEach(el => {
 50      el.addEventListener('click', e => {
 51        const tarEl = e.target.closest('.js-mode');
 52        const mode = tarEl.dataset.mode;
 53
 54        if (this._curMode === mode) return;
 55
 56        this._curMode = mode;
 57
 58        // 重置所有
 59        this._resetAllMode();
 60
 61        if (mode === this._modes.draw) {
 62          this.canvas.isDrawingMode = true;
 63
 64        } else if (mode === this._modes.eraser) {
 65          this.canvas.hoverCursor = 'not-allowed';
 66
 67        } else if (mode === this._modes.text) {
 68          this.canvas.defaultCursor = 'text';
 69          this.canvas.hoverCursor = 'text';
 70
 71        } else if (mode === this._modes.line) {
 72          this.canvas.defaultCursor = 'crosshair';
 73          this.canvas.hoverCursor = 'crosshair';
 74
 75        } else if (mode === this._modes.ellipse) {
 76          this.canvas.defaultCursor = 'crosshair';
 77          this.canvas.hoverCursor = 'crosshair';
 78
 79        } else if (mode === this._modes.rect) {
 80          this.canvas.defaultCursor = 'crosshair';
 81          this.canvas.hoverCursor = 'crosshair';
 82        }
 83      });
 84    });
 85  }
 86
 87  _handleUndoClick() {
 88    this.undoEls.addEventListener('click', () => {
 89      this._histories.pop(); // 排除当前状态
 90
 91      if (this._histories.length === 0) {
 92        this.canvas.clear().requestRenderAll();
 93        return;
 94      }
 95
 96      const state = this._histories.pop();
 97      this._histories = []; // 清空队列
 98      this.canvas.loadFromJSON(state, () => {
 99        this.canvas.requestRenderAll();
100      });
101    });
102  }
103
104  _handleRadioChange() {
105    const brush = this.canvas.freeDrawingBrush;
106
107    this.widthEls.forEach(el => {
108      el.addEventListener('change', e => {
109        this._width = +e.target.dataset.value;
110
111        this._preventAllControl();
112        brush.width = this._width;
113      });
114    });
115
116    this.colorEls.forEach(el => {
117      el.addEventListener('change', e => {
118        this._color = e.target.dataset.value;
119
120        this._preventAllControl();
121        brush.color = this._color;
122      });
123    });
124  }
125
126  _registerCanvasEvent() {
127    let mouseDown;
128    let line, ellipse, rect;
129    let origX, origY;
130
131    this.canvas.on('mouse:down', options => {
132      const e = options.e;
133      mouseDown = true;
134
135      if (options.target && this._curMode === this._modes.eraser) {
136        this.canvas.getActiveObjects().forEach(obj => {
137          this.canvas.remove(obj);
138        });
139
140      } else if (this._curMode === this._modes.text) {
141        const text = new fabric.IText('', {
142          // 减多少,我也是凑的,能用就行
143          left: e.clientX - 8,
144          top: e.clientY - 23,
145
146          fill: this._color,
147          fontSize: 24,
148          lockMovementX: true,
149          lockMovementY: true,
150          lockScalingX: true,
151          lockScalingY: true,
152          lockRotation: true,
153          hasControls: false,
154          hasBorders: false
155        });
156
157        this.canvas.add(text).setActiveObject(text);
158        text.enterEditing();
159
160      } else if (this._curMode === this._modes.line) {
161        const { x, y } = this.canvas.getPointer(e);
162        const points = [x, y, x, y];
163
164        line = new fabric.Line(points, {
165          strokeWidth: this._width,
166          fill: this._color,
167          stroke: this._color,
168          originX: 'center',
169          originY: 'center',
170          lockMovementX: true,
171          lockMovementY: true,
172          lockScalingX: true,
173          lockScalingY: true,
174          lockRotation: true,
175          hasControls: false,
176          hasBorders: false
177        });
178
179        this.canvas.add(line);
180
181      } else if (this._curMode === this._modes.ellipse) {
182        const { x, y } = this.canvas.getPointer(e);
183        origX = x;
184        origY = y;
185
186        ellipse = new fabric.Ellipse({
187          left: x,
188          top: y,
189          originX: 'left',
190          originY: 'top',
191          rx: 0,
192          ry: 0,
193          fill: '',
194          stroke: this._color,
195          strokeWidth: this._width,
196          lockMovementX: true,
197          lockMovementY: true,
198          lockScalingX: true,
199          lockScalingY: true,
200          lockRotation: true,
201          hasControls: false,
202          hasBorders: false
203        });
204
205        this.canvas.add(ellipse);
206
207      } else if (this._curMode === this._modes.rect) {
208        const { x, y } = this.canvas.getPointer(e);
209        origX = x;
210        origY = y;
211
212        rect = new fabric.Rect({
213          left: x,
214          top: y,
215          originX: 'left',
216          originY: 'top',
217          width: 0,
218          height: 0,
219          angle: 0,
220          fill: '',
221          stroke: this._color,
222          strokeWidth: this._width,
223          lockMovementX: true,
224          lockMovementY: true,
225          lockScalingX: true,
226          lockScalingY: true,
227          lockRotation: true,
228          hasControls: false,
229          hasBorders: false
230        });
231
232        this.canvas.add(rect);
233      }
234    });
235
236    this.canvas.on('mouse:move', options => {
237      if (!mouseDown) return;
238
239      const e = options.e;
240
241      if (this._curMode === this._modes.line) {
242        const { x, y } = this.canvas.getPointer(e);
243        line.set({ x2: x, y2: y });
244        this.canvas.requestRenderAll();
245
246      } else if (this._curMode === this._modes.ellipse) {
247        const { x, y } = this.canvas.getPointer(e);
248        let rx = Math.abs(origX - x) / 2;
249        let ry = Math.abs(origY - y) / 2;
250        if (rx > ellipse.strokeWidth) {
251          rx -= ellipse.strokeWidth / 2;
252        }
253        if (ry > ellipse.strokeWidth) {
254          ry -= ellipse.strokeWidth / 2;
255        }
256        ellipse.set({ rx: rx, ry: ry });
257
258        if (origX > x) {
259          ellipse.set({ originX: 'right' });
260        } else {
261          ellipse.set({ originX: 'left' });
262        }
263        if (origY > y) {
264          ellipse.set({ originY: 'bottom' });
265        } else {
266          ellipse.set({ originY: 'top' });
267        }
268
269        this.canvas.requestRenderAll();
270
271      } else if (this._curMode === this._modes.rect) {
272        const { x, y } = this.canvas.getPointer(e);
273
274        if (origX > x) {
275          rect.set({ left: Math.abs(x) });
276        }
277        if (origY > y) {
278          rect.set({ top: Math.abs(y) });
279        }
280
281        rect.set({ width: Math.abs(origX - x) });
282        rect.set({ height: Math.abs(origY - y) });
283
284        this.canvas.requestRenderAll();
285      }
286    });
287
288    this.canvas.on('mouse:up', () => {
289      mouseDown = false;
290
291      if (this._curMode === this._modes.line) {
292        line.setCoords();
293      } else if (this._curMode === this._modes.ellipse) {
294        ellipse.setCoords();
295      } else if (this._curMode === this._modes.rect) {
296        rect.setCoords();
297      }
298    });
299  }
300
301  _resetAllMode() {
302    // Fabric.js
303    this.canvas.isDrawingMode = false;
304    this.canvas.defaultCursor = 'default';
305    this.canvas.hoverCursor = 'default';
306
307    this._preventAllControl();
308  }
309
310  _preventAllControl() {
311    this.canvas.getObjects().forEach(obj => {
312      // 可交互文本对象
313      if (obj instanceof fabric.IText) {
314        obj.exitEditing();
315      }
316
317      // 固定位置与缩放
318      obj.lockMovementX = true;
319      obj.lockMovementY = true;
320      obj.lockScalingX = true;
321      obj.lockScalingY = true;
322      obj.lockRotation = true;
323      obj.hasControls = obj.hasBorders = false;
324    });
325  }
326}
327
328new Main();
#Fabric.js#

文章:HTML Canvas:FabricJS

链接:https://www.wuxianjie.net/posts/fabric-js/

作者:吴仙杰

文章: 本博客文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议,转载请注明出处!

CSS 布局:Grid
Electron 全屏遮罩
吴仙杰

吴仙杰

🔍 Ctrl+K / ⌘K

27 文章
9 分类
25 标签
邮箱 GitHub
© 2021-2025 吴仙杰 保留所有权利 All Rights Reserved
浙公网安备 33010302003726号 浙ICP备2021017187号-1
0%