├── .browserslistrc
├── vue.config.js
├── demo.png
├── babel.config.js
├── public
├── favicon.ico
└── index.html
├── src
├── assets
│ └── logo.png
├── main.js
├── router
│ └── index.js
├── App.vue
├── views
│ └── Home.vue
└── libs
│ └── eraser_brush.mixin.js
├── .gitignore
├── .eslintrc.js
├── README.md
└── package.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | lintOnSave: false
3 | }
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CC4J/fabric-drawing-board/HEAD/demo.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@vue/cli-plugin-babel/preset"],
3 | };
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CC4J/fabric-drawing-board/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CC4J/fabric-drawing-board/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import ElementUI from 'element-ui';
3 | import 'element-ui/lib/theme-chalk/index.css';
4 | import App from "./App.vue";
5 | import router from "./router";
6 |
7 | Vue.use(ElementUI);
8 |
9 | Vue.config.productionTip = false;
10 |
11 | new Vue({
12 | router,
13 | render: (h) => h(App),
14 | }).$mount("#app");
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueRouter from "vue-router";
3 | import Home from "../views/Home.vue";
4 |
5 | Vue.use(VueRouter);
6 |
7 | const routes = [{
8 | path: "/",
9 | name: "Home",
10 | component: Home,
11 | }];
12 |
13 | const router = new VueRouter({
14 | routes,
15 | });
16 |
17 | export default router;
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | // extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
7 | parserOptions: {
8 | parser: "babel-eslint",
9 | },
10 | rules: {
11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
13 | },
14 | };
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
30 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
9 | # fabric-drawing-borard
10 |
11 | use fabric to achievement drawing board and support free drawing, line, rect,cirlce,eraser,text,pan,scale,undo,redo,clear, save.
12 |
13 | 使用fabric实现画板功能,支持画笔,绘制直线,矩形,圆形,文字,移动缩放画布,撤销重做,清屏与保存功能。
14 |
15 | 
16 |
17 | ## Project setup
18 | ```
19 | npm install
20 | ```
21 |
22 | ### Compiles and hot-reloads for development
23 | ```
24 | npm run serve
25 | ```
26 |
27 | ## Usage
28 |
29 | ### npm package
30 |
31 | ```
32 | npm install --save fabric-drawing-board
33 |
34 | ```
35 |
36 | ### Repo
37 |
38 | - https://github.com/CC4J/fabric-drawing-board-plugin
39 |
40 | ### npm package demo
41 |
42 | - https://github.com/CC4J/fabric-drawing-board-plugin-demo
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fabric-drawing-board",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "core-js": "^3.6.5",
12 | "element-ui": "^2.15.2",
13 | "fabric": "^4.5.0",
14 | "vue": "^2.6.11",
15 | "vue-color": "^2.8.1",
16 | "vue-router": "^3.2.0",
17 | "vuex": "^3.4.0"
18 | },
19 | "devDependencies": {
20 | "@vue/cli-plugin-babel": "~4.5.0",
21 | "@vue/cli-plugin-eslint": "~4.5.0",
22 | "@vue/cli-plugin-router": "~4.5.0",
23 | "@vue/cli-plugin-vuex": "~4.5.0",
24 | "@vue/cli-service": "~4.5.0",
25 | "@vue/eslint-config-prettier": "^6.0.0",
26 | "babel-eslint": "^10.1.0",
27 | "eslint": "^6.7.2",
28 | "eslint-plugin-prettier": "^3.3.1",
29 | "eslint-plugin-vue": "^6.2.2",
30 | "less": "^3.0.4",
31 | "less-loader": "^5.0.0",
32 | "prettier": "^2.2.1",
33 | "vue-template-compiler": "^2.6.11"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
16 |
strokeColor
17 |
18 |
23 |
29 |
30 |
fillColor
31 |
32 |
37 |
43 |
44 |
bgColor
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
61 |
68 |
75 |
82 |
89 |
96 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
597 |
641 |
--------------------------------------------------------------------------------
/src/libs/eraser_brush.mixin.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | /** ERASER_START */
3 | var __setBgOverlayColor = fabric.StaticCanvas.prototype.__setBgOverlayColor;
4 | var ___setBgOverlay = fabric.StaticCanvas.prototype.__setBgOverlay;
5 | var __setSVGBgOverlayColor = fabric.StaticCanvas.prototype._setSVGBgOverlayColor;
6 | fabric.util.object.extend(fabric.StaticCanvas.prototype, {
7 | backgroundColor: undefined,
8 | overlayColor: undefined,
9 | /**
10 | * Create Rect that holds the color to support erasing
11 | * patches {@link CommonMethods#_initGradient}
12 | * @private
13 | * @param {'bakground'|'overlay'} property
14 | * @param {(String|fabric.Pattern|fabric.Rect)} color Color or pattern or rect (in case of erasing)
15 | * @param {Function} callback Callback to invoke when color is set
16 | * @param {Object} options
17 | * @return {fabric.Canvas} instance
18 | * @chainable true
19 | */
20 | __setBgOverlayColor: function(property, color, callback, options) {
21 | if (color && color.isType && color.isType('rect')) {
22 | // color is already an object
23 | this[property] = color;
24 | color.set(options);
25 | callback && callback(this[property]);
26 | } else {
27 | var _this = this;
28 | var cb = function() {
29 | _this[property] = new fabric.Rect(fabric.util.object.extend({
30 | width: _this.width,
31 | height: _this.height,
32 | fill: _this[property],
33 | }, options));
34 | callback && callback(_this[property]);
35 | };
36 | __setBgOverlayColor.call(this, property, color, cb);
37 | // invoke cb in case of gradient
38 | // see {@link CommonMethods#_initGradient}
39 | if (color && color.colorStops && !(color instanceof fabric.Gradient)) {
40 | cb();
41 | }
42 | }
43 |
44 | return this;
45 | },
46 |
47 | setBackgroundColor: function(backgroundColor, callback, options) {
48 | return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback, options);
49 | },
50 |
51 | setOverlayColor: function(overlayColor, callback, options) {
52 | return this.__setBgOverlayColor('overlayColor', overlayColor, callback, options);
53 | },
54 |
55 | /**
56 | * patch serialization - from json
57 | * background/overlay properties could be objects if parsed by this mixin or could be legacy values
58 | * @private
59 | * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor)
60 | * @param {(Object|String)} value Value to set
61 | * @param {Object} loaded Set loaded property to true if property is set
62 | * @param {Object} callback Callback function to invoke after property is set
63 | */
64 | __setBgOverlay: function(property, value, loaded, callback) {
65 | var _this = this;
66 |
67 | if ((property === 'backgroundColor' || property === 'overlayColor') &&
68 | (value && typeof value === 'object' && value.type === 'rect')) {
69 | fabric.util.enlivenObjects([value], function(enlivedObject) {
70 | _this[property] = enlivedObject[0];
71 | loaded[property] = true;
72 | callback && callback();
73 | });
74 | } else {
75 | ___setBgOverlay.call(this, property, value, loaded, callback);
76 | }
77 | },
78 |
79 | /**
80 | * patch serialization - to svg
81 | * background/overlay properties could be objects if parsed by this mixin or could be legacy values
82 | * @private
83 | */
84 | _setSVGBgOverlayColor: function(markup, property, reviver) {
85 | var filler = this[property + 'Color'];
86 | if (filler && filler.isType && filler.isType('rect')) {
87 | var excludeFromExport = filler.excludeFromExport || (this[property] && this[property].excludeFromExport);
88 | if (filler && !excludeFromExport && filler.toSVG) {
89 | markup.push(filler.toSVG(reviver));
90 | }
91 | } else {
92 | __setSVGBgOverlayColor.call(this, markup, property, reviver);
93 | }
94 | },
95 |
96 | /**
97 | * @private
98 | * @param {CanvasRenderingContext2D} ctx Context to render on
99 | * @param {string} property 'background' or 'overlay'
100 | */
101 | _renderBackgroundOrOverlay: function(ctx, property) {
102 | var fill = this[property + 'Color'],
103 | object = this[property + 'Image'],
104 | v = this.viewportTransform,
105 | needsVpt = this[property + 'Vpt'];
106 | if (!fill && !object) {
107 | return;
108 | }
109 | if (fill || object) {
110 | ctx.save();
111 | if (needsVpt) {
112 | ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
113 | }
114 | fill && fill.render(ctx);
115 | object && object.render(ctx);
116 | ctx.restore();
117 | }
118 | },
119 | });
120 |
121 | var _toObject = fabric.Object.prototype.toObject;
122 | var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup;
123 | fabric.util.object.extend(fabric.Object.prototype, {
124 | /**
125 | * Indicates whether this object can be erased by {@link fabric.EraserBrush}
126 | * @type boolean
127 | * @default true
128 | */
129 | erasable: true,
130 |
131 | /**
132 | *
133 | * @returns {fabric.Group | null}
134 | */
135 | getEraser: function() {
136 | return this.clipPath && this.clipPath.eraser ? this.clipPath : null;
137 | },
138 |
139 | /**
140 | * Returns an object representation of an instance
141 | * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
142 | * @return {Object} Object representation of an instance
143 | */
144 | toObject: function(additionalProperties) {
145 | return _toObject.call(this, ['erasable'].concat(additionalProperties));
146 | },
147 |
148 | /**
149 | * use to achieve erasing for svg
150 | * credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
151 | * @param {Function} reviver
152 | * @returns {string} markup
153 | */
154 | eraserToSVG: function(options) {
155 | var eraser = this.getEraser();
156 | if (eraser) {
157 | var fill = eraser._objects[0].fill;
158 | eraser._objects[0].fill = 'white';
159 | eraser.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++;
160 | var commons = [
161 | 'id="' + eraser.clipPathId + '"',
162 | /*options.additionalTransform ? ' transform="' + options.additionalTransform + '" ' : ''*/
163 | ].join(' ');
164 | var objectMarkup = ['', '', eraser.toSVG(options.reviver), '', ''];
165 | eraser._objects[0].fill = fill;
166 | return objectMarkup.join('\n');
167 | }
168 | return '';
169 | },
170 |
171 | /**
172 | * use to achieve erasing for svg, override
173 | * @param {string[]} objectMarkup
174 | * @param {Object} options
175 | * @returns
176 | */
177 | _createBaseSVGMarkup: function(objectMarkup, options) {
178 | var eraser = this.getEraser();
179 | if (eraser) {
180 | var eraserMarkup = this.eraserToSVG(options);
181 | this.clipPath = null;
182 | var markup = __createBaseSVGMarkup.call(this, objectMarkup, options);
183 | this.clipPath = eraser;
184 | return [
185 | eraserMarkup,
186 | markup.replace('>', 'mask="url(#' + eraser.clipPathId + ')" >')
187 | ].join('\n');
188 | } else {
189 | return __createBaseSVGMarkup.call(this, objectMarkup, options);
190 | }
191 | }
192 | });
193 |
194 | var _groupToObject = fabric.Group.prototype.toObject;
195 | fabric.util.object.extend(fabric.Group.prototype, {
196 | /**
197 | * Returns an object representation of an instance
198 | * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
199 | * @return {Object} Object representation of an instance
200 | */
201 | toObject: function(additionalProperties) {
202 | return _groupToObject.call(this, ['eraser'].concat(additionalProperties));
203 | }
204 | });
205 |
206 | fabric.util.object.extend(fabric.Canvas.prototype, {
207 | /**
208 | * Used by {@link #renderAll}
209 | * @returns boolean
210 | */
211 | isErasing: function() {
212 | return (
213 | this.isDrawingMode &&
214 | this.freeDrawingBrush &&
215 | this.freeDrawingBrush.type === 'eraser' &&
216 | this.freeDrawingBrush._isErasing
217 | );
218 | },
219 |
220 | /**
221 | * While erasing, the brush is in charge of rendering the canvas
222 | * It uses both layers to achieve diserd erasing effect
223 | *
224 | * @returns fabric.Canvas
225 | */
226 | renderAll: function() {
227 | if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) {
228 | this.clearContext(this.contextTop);
229 | this.contextTopDirty = false;
230 | }
231 | // while erasing the brush is in charge of rendering the canvas so we return
232 | if (this.isErasing()) {
233 | this.freeDrawingBrush._render();
234 | return;
235 | }
236 | if (this.hasLostContext) {
237 | this.renderTopLayer(this.contextTop);
238 | }
239 | var canvasToDrawOn = this.contextContainer;
240 | this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender());
241 | return this;
242 | }
243 | });
244 |
245 |
246 | /**
247 | * EraserBrush class
248 | * Supports selective erasing meaning that only erasable objects are affected by the eraser brush.
249 | * In order to support selective erasing all non erasable objects are rendered on the main/bottom ctx
250 | * while the entire canvas is rendered on the top ctx.
251 | * Canvas bakground/overlay image/color are handled as well.
252 | * When erasing occurs, the path clips the top ctx and reveals the bottom ctx.
253 | * This achieves the desired effect of seeming to erase only erasable objects.
254 | * After erasing is done the created path is added to all intersected objects' `clipPath` property.
255 | *
256 | *
257 | * @class fabric.EraserBrush
258 | * @extends fabric.PencilBrush
259 | */
260 | fabric.EraserBrush = fabric.util.createClass(
261 | fabric.PencilBrush,
262 | /** @lends fabric.EraserBrush.prototype */
263 | {
264 | type: 'eraser',
265 |
266 | /**
267 | * Indicates that the ctx is ready and rendering can begin.
268 | * Used to prevent a race condition caused by {@link fabric.EraserBrush#onMouseMove} firing before {@link fabric.EraserBrush#onMouseDown} has completed
269 | *
270 | * @private
271 | */
272 | _ready: false,
273 |
274 | /**
275 | * @private
276 | */
277 | _drawOverlayOnTop: false,
278 |
279 | /**
280 | * @private
281 | */
282 | _isErasing: false,
283 |
284 | initialize: function(canvas) {
285 | this.callSuper('initialize', canvas);
286 | this._renderBound = this._render.bind(this);
287 | this.render = this.render.bind(this);
288 | },
289 |
290 | /**
291 | * Used to hide a drawable from the rendering process
292 | * @param {fabric.Object} object
293 | */
294 | hideObject: function(object) {
295 | if (object) {
296 | object._originalOpacity = object.opacity;
297 | object.set({ opacity: 0 });
298 | }
299 | },
300 |
301 | /**
302 | * Restores hiding an object
303 | * {@link fabric.EraserBrush#hideObject}
304 | * @param {fabric.Object} object
305 | */
306 | restoreObjectVisibility: function(object) {
307 | if (object && object._originalOpacity) {
308 | object.set({ opacity: object._originalOpacity });
309 | object._originalOpacity = undefined;
310 | }
311 | },
312 |
313 | /**
314 | * Drawing Logic For background drawables: (`backgroundImage`, `backgroundColor`)
315 | * 1. if erasable = true:
316 | * we need to hide the drawable on the bottom ctx so when the brush is erasing it will clip the top ctx and reveal white space underneath
317 | * 2. if erasable = false:
318 | * we need to draw the drawable only on the bottom ctx so the brush won't affect it
319 | * @param {'bottom' | 'top' | 'overlay'} layer
320 | */
321 | prepareCanvasBackgroundForLayer: function(layer) {
322 | if (layer === 'overlay') {
323 | return;
324 | }
325 | var canvas = this.canvas;
326 | var image = canvas.get('backgroundImage');
327 | var color = canvas.get('backgroundColor');
328 | var erasablesOnLayer = layer === 'top';
329 | if (image && image.erasable === !erasablesOnLayer) {
330 | this.hideObject(image);
331 | }
332 | if (color && color.erasable === !erasablesOnLayer) {
333 | this.hideObject(color);
334 | }
335 | },
336 |
337 | /**
338 | * Drawing Logic For overlay drawables (`overlayImage`, `overlayColor`)
339 | * We must draw on top ctx to be on top of visible canvas
340 | * 1. if erasable = true:
341 | * we need to draw the drawable on the top ctx as a normal object
342 | * 2. if erasable = false:
343 | * we need to draw the drawable on top of the brush,
344 | * this means we need to repaint for every stroke
345 | *
346 | * @param {'bottom' | 'top' | 'overlay'} layer
347 | * @returns boolean render overlay above brush
348 | */
349 | prepareCanvasOverlayForLayer: function(layer) {
350 | var canvas = this.canvas;
351 | var image = canvas.get('overlayImage');
352 | var color = canvas.get('overlayColor');
353 | if (layer === 'bottom') {
354 | this.hideObject(image);
355 | this.hideObject(color);
356 | return false;
357 | };
358 | var erasablesOnLayer = layer === 'top';
359 | var renderOverlayOnTop = (image && !image.erasable) || (color && !color.erasable);
360 | if (image && image.erasable === !erasablesOnLayer) {
361 | this.hideObject(image);
362 | }
363 | if (color && color.erasable === !erasablesOnLayer) {
364 | this.hideObject(color);
365 | }
366 | return renderOverlayOnTop;
367 | },
368 |
369 | /**
370 | * @private
371 | */
372 | restoreCanvasDrawables: function() {
373 | var canvas = this.canvas;
374 | this.restoreObjectVisibility(canvas.get('backgroundImage'));
375 | this.restoreObjectVisibility(canvas.get('backgroundColor'));
376 | this.restoreObjectVisibility(canvas.get('overlayImage'));
377 | this.restoreObjectVisibility(canvas.get('overlayColor'));
378 | },
379 |
380 | /**
381 | * @private
382 | * This is designed to support erasing a group with both erasable and non-erasable objects.
383 | * Iterates over collections to allow nested selective erasing.
384 | * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
385 | * to prepare the bottom layer by hiding erasable nested objects
386 | *
387 | * @param {fabric.Collection} collection
388 | */
389 | prepareCollectionTraversal: function(collection) {
390 | var _this = this;
391 | collection.forEachObject(function(obj) {
392 | if (obj.forEachObject) {
393 | _this.prepareCollectionTraversal(obj);
394 | } else {
395 | if (obj.erasable) {
396 | _this.hideObject(obj);
397 | }
398 | }
399 | });
400 | },
401 |
402 | /**
403 | * @private
404 | * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
405 | * to reverse the action of {@link fabric.EraserBrush#prepareCollectionTraversal}
406 | *
407 | * @param {fabric.Collection} collection
408 | */
409 | restoreCollectionTraversal: function(collection) {
410 | var _this = this;
411 | collection.forEachObject(function(obj) {
412 | if (obj.forEachObject) {
413 | _this.restoreCollectionTraversal(obj);
414 | } else {
415 | _this.restoreObjectVisibility(obj);
416 | }
417 | });
418 | },
419 |
420 | /**
421 | * @private
422 | * This is designed to support erasing a group with both erasable and non-erasable objects.
423 | *
424 | * @param {'bottom' | 'top' | 'overlay'} layer
425 | */
426 | prepareCanvasObjectsForLayer: function(layer) {
427 | if (layer !== 'bottom') { return; }
428 | this.prepareCollectionTraversal(this.canvas);
429 | },
430 |
431 | /**
432 | * @private
433 | * @param {'bottom' | 'top' | 'overlay'} layer
434 | */
435 | restoreCanvasObjectsFromLayer: function(layer) {
436 | if (layer !== 'bottom') { return; }
437 | this.restoreCollectionTraversal(this.canvas);
438 | },
439 |
440 | /**
441 | * @private
442 | * @param {'bottom' | 'top' | 'overlay'} layer
443 | * @returns boolean render overlay above brush
444 | */
445 | prepareCanvasForLayer: function(layer) {
446 | this.prepareCanvasBackgroundForLayer(layer);
447 | this.prepareCanvasObjectsForLayer(layer);
448 | return this.prepareCanvasOverlayForLayer(layer);
449 | },
450 |
451 | /**
452 | * @private
453 | * @param {'bottom' | 'top' | 'overlay'} layer
454 | */
455 | restoreCanvasFromLayer: function(layer) {
456 | this.restoreCanvasDrawables();
457 | this.restoreCanvasObjectsFromLayer(layer);
458 | },
459 |
460 | /**
461 | * Render all non-erasable objects on bottom layer with the exception of overlays to avoid being clipped by the brush.
462 | * Groups are rendered for nested selective erasing, non-erasable objects are visible while erasable objects are not.
463 | */
464 | renderBottomLayer: function() {
465 | var canvas = this.canvas;
466 | this.prepareCanvasForLayer('bottom');
467 | canvas.renderCanvas(
468 | canvas.getContext(),
469 | canvas.getObjects().filter(function(obj) {
470 | return !obj.erasable || obj.isType('group');
471 | })
472 | );
473 | this.restoreCanvasFromLayer('bottom');
474 | },
475 |
476 | /**
477 | * 1. Render all objects on top layer, erasable and non-erasable
478 | * This is important for cases such as overlapping objects, the background object erasable and the foreground object not erasable.
479 | * 2. Render the brush
480 | */
481 | renderTopLayer: function() {
482 | var canvas = this.canvas;
483 | this._drawOverlayOnTop = this.prepareCanvasForLayer('top');
484 | canvas.renderCanvas(
485 | canvas.contextTop,
486 | canvas.getObjects()
487 | );
488 | this.callSuper('_render');
489 | this.restoreCanvasFromLayer('top');
490 | },
491 |
492 | /**
493 | * Render all non-erasable overlays on top of the brush so that they won't get erased
494 | */
495 | renderOverlay: function() {
496 | this.prepareCanvasForLayer('overlay');
497 | var canvas = this.canvas;
498 | var ctx = canvas.contextTop;
499 | this._saveAndTransform(ctx);
500 | canvas._renderOverlay(ctx);
501 | ctx.restore();
502 | this.restoreCanvasFromLayer('overlay');
503 | },
504 |
505 | /**
506 | * @extends @class fabric.BaseBrush
507 | * @param {CanvasRenderingContext2D} ctx
508 | */
509 | _saveAndTransform: function(ctx) {
510 | this.callSuper('_saveAndTransform', ctx);
511 | ctx.globalCompositeOperation = 'destination-out';
512 | },
513 |
514 | /**
515 | * We indicate {@link fabric.PencilBrush} to repaint itself if necessary
516 | * @returns
517 | */
518 | needsFullRender: function() {
519 | return this.callSuper('needsFullRender') || this._drawOverlayOnTop;
520 | },
521 |
522 | /**
523 | *
524 | * @param {fabric.Point} pointer
525 | * @param {fabric.IEvent} options
526 | * @returns
527 | */
528 | onMouseDown: function(pointer, options) {
529 | if (!this.canvas._isMainEvent(options.e)) {
530 | return;
531 | }
532 | this._prepareForDrawing(pointer);
533 | // capture coordinates immediately
534 | // this allows to draw dots (when movement never occurs)
535 | this._captureDrawingPath(pointer);
536 |
537 | this._isErasing = true;
538 | this.canvas.fire('erasing:start');
539 | this._ready = true;
540 | this._render();
541 | },
542 |
543 | /**
544 | * Rendering is done in 4 steps:
545 | * 1. Draw all non-erasable objects on bottom ctx with the exception of overlays {@link fabric.EraserBrush#renderBottomLayer}
546 | * 2. Draw all objects on top ctx including erasable drawables {@link fabric.EraserBrush#renderTopLayer}
547 | * 3. Draw eraser {@link fabric.PencilBrush#_render} at {@link fabric.EraserBrush#renderTopLayer}
548 | * 4. Draw non-erasable overlays {@link fabric.EraserBrush#renderOverlay}
549 | *
550 | * @param {fabric.Canvas} canvas
551 | */
552 | _render: function() {
553 | if (!this._ready) {
554 | return;
555 | }
556 | this.isRendering = 1;
557 | this.renderBottomLayer();
558 | this.renderTopLayer();
559 | this.renderOverlay();
560 | this.isRendering = 0;
561 | },
562 |
563 | /**
564 | * @public
565 | */
566 | render: function() {
567 | if (this._isErasing) {
568 | if (this.isRendering) {
569 | this.isRendering = fabric.util.requestAnimFrame(this._renderBound);
570 | } else {
571 | this._render();
572 | }
573 | return true;
574 | }
575 | return false;
576 | },
577 |
578 | /**
579 | * Adds path to existing clipPath of object
580 | *
581 | * @param {fabric.Object} obj
582 | * @param {fabric.Path} path
583 | */
584 | _addPathToObjectEraser: function(obj, path) {
585 | var clipObject;
586 | var _this = this;
587 | // object is collection, i.e group
588 | if (obj.forEachObject) {
589 | obj.forEachObject(function(_obj) {
590 | if (_obj.erasable) {
591 | _this._addPathToObjectEraser(_obj, path);
592 | }
593 | });
594 | return;
595 | }
596 | if (!obj.getEraser()) {
597 | var size = obj._getNonTransformedDimensions();
598 | var rect = new fabric.Rect({
599 | width: size.x,
600 | height: size.y,
601 | clipPath: obj.clipPath,
602 | originX: 'center',
603 | originY: 'center'
604 | });
605 | clipObject = new fabric.Group([rect], {
606 | eraser: true
607 | });
608 | } else {
609 | clipObject = obj.clipPath;
610 | }
611 |
612 | path.clone(function(path) {
613 | path.globalCompositeOperation = 'destination-out';
614 | // http://fabricjs.com/using-transformations
615 | var desiredTransform = fabric.util.multiplyTransformMatrices(
616 | fabric.util.invertTransform(
617 | obj.calcTransformMatrix()
618 | ),
619 | path.calcTransformMatrix()
620 | );
621 | fabric.util.applyTransformToObject(path, desiredTransform);
622 | clipObject.addWithUpdate(path);
623 | obj.set({
624 | clipPath: clipObject,
625 | dirty: true
626 | });
627 | });
628 | },
629 |
630 | /**
631 | * Add the eraser path to canvas drawables' clip paths
632 | *
633 | * @param {fabric.Canvas} source
634 | * @param {fabric.Canvas} path
635 | * @returns {Object} canvas drawables that were erased by the path
636 | */
637 | applyEraserToCanvas: function(path) {
638 | var canvas = this.canvas;
639 | var drawables = {};
640 | [
641 | 'backgroundImage',
642 | 'backgroundColor',
643 | 'overlayImage',
644 | 'overlayColor',
645 | ].forEach(function(prop) {
646 | var drawable = canvas[prop];
647 | if (drawable && drawable.erasable) {
648 | this._addPathToObjectEraser(drawable, path);
649 | drawables[prop] = drawable;
650 | }
651 | }, this);
652 | return drawables;
653 | },
654 |
655 | /**
656 | * On mouseup after drawing the path on contextTop canvas
657 | * we use the points captured to create an new fabric path object
658 | * and add it to every intersected erasable object.
659 | */
660 | _finalizeAndAddPath: function() {
661 | var ctx = this.canvas.contextTop,
662 | canvas = this.canvas;
663 | ctx.closePath();
664 | if (this.decimate) {
665 | this._points = this.decimatePoints(this._points, this.decimate);
666 | }
667 |
668 | // clear
669 | canvas.clearContext(canvas.contextTop);
670 | this._isErasing = false;
671 |
672 | var pathData = this._points && this._points.length > 1 ?
673 | this.convertPointsToSVGPath(this._points).join('') :
674 | 'M 0 0 Q 0 0 0 0 L 0 0';
675 | if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') {
676 | canvas.fire('erasing:end');
677 | // do not create 0 width/height paths, as they are
678 | // rendered inconsistently across browsers
679 | // Firefox 4, for example, renders a dot,
680 | // whereas Chrome 10 renders nothing
681 | canvas.requestRenderAll();
682 | return;
683 | }
684 |
685 | var path = this.createPath(pathData);
686 | canvas.fire('before:path:created', { path: path });
687 |
688 | // finalize erasing
689 | var drawables = this.applyEraserToCanvas(path);
690 | var _this = this;
691 | var targets = [];
692 | canvas.forEachObject(function(obj) {
693 | if (obj.erasable && obj.intersectsWithObject(path, true)) {
694 | _this._addPathToObjectEraser(obj, path);
695 | targets.push(obj);
696 | }
697 | });
698 |
699 | canvas.fire('erasing:end', { path: path, targets: targets, drawables: drawables });
700 |
701 | canvas.requestRenderAll();
702 | path.setCoords();
703 | this._resetShadow();
704 |
705 | // fire event 'path' created
706 | canvas.fire('path:created', { path: path });
707 | }
708 | }
709 | );
710 |
711 | /** ERASER_END */
712 | })();
--------------------------------------------------------------------------------