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();