fabric 4.6.0 → 5.0.0-browser

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,16 @@
1
1
  (function () {
2
2
  /** ERASER_START */
3
- var __set = fabric.Object.prototype._set;
4
- var _render = fabric.Object.prototype.render;
3
+
4
+ /**
5
+ * add `eraser` to enlivened props
6
+ */
7
+ fabric.Object.ENLIVEN_PROPS.push('eraser');
8
+
9
+ var __drawClipPath = fabric.Object.prototype._drawClipPath;
10
+ var _needsItsOwnCache = fabric.Object.prototype.needsItsOwnCache;
5
11
  var _toObject = fabric.Object.prototype.toObject;
12
+ var _getSvgCommons = fabric.Object.prototype.getSvgCommons;
13
+ var __createBaseClipPathSVGMarkup = fabric.Object.prototype._createBaseClipPathSVGMarkup;
6
14
  var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup;
7
15
  /**
8
16
  * @fires erasing:end
@@ -21,76 +29,37 @@
21
29
  erasable: true,
22
30
 
23
31
  /**
24
- *
25
- * @returns {fabric.Group | undefined}
32
+ * @tutorial {@link http://fabricjs.com/erasing#eraser}
33
+ * @type fabric.Eraser
26
34
  */
27
- getEraser: function () {
28
- return this.clipPath && this.clipPath.eraser ? this.clipPath : undefined;
29
- },
35
+ eraser: undefined,
30
36
 
31
37
  /**
32
- * Get the object's actual clip path regardless of clipping done by erasing
33
- * @returns {fabric.Object | undefined}
38
+ * @override
39
+ * @returns Boolean
34
40
  */
35
- getClipPath: function () {
36
- var eraser = this.getEraser();
37
- return eraser ? eraser._objects[0].clipPath : this.clipPath;
41
+ needsItsOwnCache: function () {
42
+ return _needsItsOwnCache.call(this) || !!this.eraser;
38
43
  },
39
44
 
40
45
  /**
41
- * Set the object's actual clip path regardless of clipping done by erasing
42
- * @param {fabric.Object} [clipPath]
43
- */
44
- setClipPath: function (clipPath) {
45
- var eraser = this.getEraser();
46
- var target = eraser ? eraser._objects[0] : this;
47
- target.set('clipPath', clipPath);
48
- this.set('dirty', true);
49
- },
50
-
51
- /**
52
- * Updates eraser size and position to match object's size
46
+ * draw eraser above clip path
47
+ * @override
53
48
  * @private
54
- * @param {Object} [dimensions] uses object's dimensions if unspecified
55
- * @param {number} [dimensions.width]
56
- * @param {number} [dimensions.height]
57
- * @param {boolean} [center=false] postion the eraser relative to object's center or it's top left corner
49
+ * @param {CanvasRenderingContext2D} ctx
50
+ * @param {fabric.Object} clipPath
58
51
  */
59
- _updateEraserDimensions: function (dimensions, center) {
60
- var eraser = this.getEraser();
61
- if (eraser) {
62
- var rect = eraser._objects[0];
63
- var eraserSize = { width: rect.width, height: rect.height };
52
+ _drawClipPath: function (ctx, clipPath) {
53
+ __drawClipPath.call(this, ctx, clipPath);
54
+ if (this.eraser) {
55
+ // update eraser size to match instance
64
56
  var size = this._getNonTransformedDimensions();
65
- var newSize = fabric.util.object.extend({ width: size.x, height: size.y }, dimensions);
66
- if (eraserSize.width === newSize.width && eraserSize.height === newSize.height) {
67
- return;
68
- }
69
- var offset = new fabric.Point((eraserSize.width - newSize.width) / 2, (eraserSize.height - newSize.height) / 2);
70
- eraser.set(newSize);
71
- eraser.setPositionByOrigin(new fabric.Point(0, 0), 'center', 'center');
72
- rect.set(newSize);
73
- eraser.set('dirty', true);
74
- if (!center) {
75
- eraser.getObjects('path').forEach(function (path) {
76
- path.setPositionByOrigin(path.getCenterPoint().add(offset), 'center', 'center');
77
- });
78
- }
79
- this.setCoords();
80
- }
81
- },
82
-
83
- _set: function (key, value) {
84
- __set.call(this, key, value);
85
- if (key === 'width' || key === 'height') {
86
- this._updateEraserDimensions();
57
+ this.eraser.isType('eraser') && this.eraser.set({
58
+ width: size.x,
59
+ height: size.y
60
+ });
61
+ __drawClipPath.call(this, ctx, this.eraser);
87
62
  }
88
- return this;
89
- },
90
-
91
- render: function (ctx) {
92
- this._updateEraserDimensions();
93
- _render.call(this, ctx);
94
63
  },
95
64
 
96
65
  /**
@@ -98,75 +67,67 @@
98
67
  * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
99
68
  * @return {Object} Object representation of an instance
100
69
  */
101
- toObject: function (additionalProperties) {
102
- return _toObject.call(this, ['erasable'].concat(additionalProperties));
70
+ toObject: function (propertiesToInclude) {
71
+ var object = _toObject.call(this, ['erasable'].concat(propertiesToInclude));
72
+ if (this.eraser && !this.eraser.excludeFromExport) {
73
+ object.eraser = this.eraser.toObject(propertiesToInclude);
74
+ }
75
+ return object;
103
76
  },
104
77
 
78
+ /* _TO_SVG_START_ */
105
79
  /**
106
- * use <mask> to achieve erasing for svg
107
- * credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
108
- * @param {Function} reviver
109
- * @returns {string} markup
80
+ * Returns id attribute for svg output
81
+ * @override
82
+ * @return {String}
110
83
  */
111
- eraserToSVG: function (options) {
112
- var eraser = this.getEraser();
113
- if (eraser) {
114
- var fill = eraser._objects[0].fill;
115
- eraser._objects[0].fill = 'white';
116
- eraser.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++;
117
- var commons = [
118
- 'id="' + eraser.clipPathId + '"',
119
- /*options.additionalTransform ? ' transform="' + options.additionalTransform + '" ' : ''*/
120
- ].join(' ');
121
- var objectMarkup = ['<defs>', '<mask ' + commons + ' >', eraser.toSVG(options.reviver), '</mask>', '</defs>'];
122
- eraser._objects[0].fill = fill;
123
- return objectMarkup.join('\n');
124
- }
125
- return '';
84
+ getSvgCommons: function () {
85
+ return _getSvgCommons.call(this) + (this.eraser ? 'mask="url(#' + this.eraser.clipPathId + ')" ' : '');
126
86
  },
127
87
 
128
88
  /**
129
- * use <mask> to achieve erasing for svg, override <clipPath>
130
- * @param {string[]} objectMarkup
131
- * @param {Object} options
89
+ * create svg markup for eraser
90
+ * use <mask> to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
91
+ * must be called before object markup creation as it relies on the `clipPathId` property of the mask
92
+ * @param {Function} [reviver]
132
93
  * @returns
133
94
  */
134
- _createBaseSVGMarkup: function (objectMarkup, options) {
135
- var eraser = this.getEraser();
136
- if (eraser) {
137
- var eraserMarkup = this.eraserToSVG(options);
138
- this.clipPath = null;
139
- var markup = __createBaseSVGMarkup.call(this, objectMarkup, options);
140
- this.clipPath = eraser;
95
+ _createEraserSVGMarkup: function (reviver) {
96
+ if (this.eraser) {
97
+ this.eraser.clipPathId = 'MASK_' + fabric.Object.__uid++;
141
98
  return [
142
- eraserMarkup,
143
- markup.replace('>', 'mask="url(#' + eraser.clipPathId + ')" >')
144
- ].join('\n');
145
- }
146
- else {
147
- return __createBaseSVGMarkup.call(this, objectMarkup, options);
99
+ '<mask id="', this.eraser.clipPathId, '" >',
100
+ this.eraser.toSVG(reviver),
101
+ '</mask>', '\n'
102
+ ].join('');
148
103
  }
149
- }
150
- });
151
-
152
- var __restoreObjectsState = fabric.Group.prototype._restoreObjectsState;
153
- var _groupToObject = fabric.Group.prototype.toObject;
154
- var __getBounds = fabric.Group.prototype._getBounds;
155
- fabric.util.object.extend(fabric.Group.prototype, {
104
+ return '';
105
+ },
156
106
 
157
107
  /**
158
- * If group is an eraser then dimensions should not change when paths are added or removed and should remain the size of the base rect
159
108
  * @private
160
109
  */
161
- _getBounds: function (aX, aY, onlyWidthHeight) {
162
- if (this.eraser) {
163
- this.width = this._objects[0].width;
164
- this.height = this._objects[0].height;
165
- return;
166
- }
167
- __getBounds.call(this, aX, aY, onlyWidthHeight);
110
+ _createBaseClipPathSVGMarkup: function (objectMarkup, options) {
111
+ return [
112
+ this._createEraserSVGMarkup(options && options.reviver),
113
+ __createBaseClipPathSVGMarkup.call(this, objectMarkup, options)
114
+ ].join('');
168
115
  },
169
116
 
117
+ /**
118
+ * @private
119
+ */
120
+ _createBaseSVGMarkup: function (objectMarkup, options) {
121
+ return [
122
+ this._createEraserSVGMarkup(options && options.reviver),
123
+ __createBaseSVGMarkup.call(this, objectMarkup, options)
124
+ ].join('');
125
+ }
126
+ /* _TO_SVG_END_ */
127
+ });
128
+
129
+ var __restoreObjectsState = fabric.Group.prototype._restoreObjectsState;
130
+ fabric.util.object.extend(fabric.Group.prototype, {
170
131
  /**
171
132
  * @private
172
133
  * @param {fabric.Path} path
@@ -186,12 +147,12 @@
186
147
  * @tutorial {@link http://fabricjs.com/erasing#erasable_property}
187
148
  */
188
149
  applyEraserToObjects: function () {
189
- var _this = this;
190
- if (this.getEraser()) {
150
+ var _this = this, eraser = this.eraser;
151
+ if (eraser) {
152
+ delete this.eraser;
191
153
  var transform = _this.calcTransformMatrix();
192
- _this.getEraser().clone(function (eraser) {
193
- var clipPath = eraser._objects[0].clipPath;
194
- _this.clipPath = clipPath ? clipPath : undefined;
154
+ eraser.clone(function (eraser) {
155
+ var clipPath = _this.clipPath;
195
156
  eraser.getObjects('path')
196
157
  .forEach(function (path) {
197
158
  // first we transform the path from the group's coordinate system to the canvas'
@@ -202,14 +163,14 @@
202
163
  fabric.util.applyTransformToObject(path, originalTransform);
203
164
  if (clipPath) {
204
165
  clipPath.clone(function (_clipPath) {
205
- fabric.EraserBrush.prototype.applyClipPathToPath.call(
166
+ var eraserPath = fabric.EraserBrush.prototype.applyClipPathToPath.call(
206
167
  fabric.EraserBrush.prototype,
207
168
  path,
208
169
  _clipPath,
209
170
  transform
210
171
  );
211
- _this._addEraserPathToObjects(path);
212
- });
172
+ _this._addEraserPathToObjects(eraserPath);
173
+ }, ['absolutePositioned', 'inverted']);
213
174
  }
214
175
  else {
215
176
  _this._addEraserPathToObjects(path);
@@ -226,18 +187,99 @@
226
187
  _restoreObjectsState: function () {
227
188
  this.erasable === true && this.applyEraserToObjects();
228
189
  return __restoreObjectsState.call(this);
190
+ }
191
+ });
192
+
193
+ /**
194
+ * An object's Eraser
195
+ * @private
196
+ * @class fabric.Eraser
197
+ * @extends fabric.Group
198
+ * @memberof fabric
199
+ */
200
+ fabric.Eraser = fabric.util.createClass(fabric.Group, {
201
+ /**
202
+ * @readonly
203
+ * @static
204
+ */
205
+ type: 'eraser',
206
+
207
+ /**
208
+ * @default
209
+ */
210
+ originX: 'center',
211
+
212
+ /**
213
+ * @default
214
+ */
215
+ originY: 'center',
216
+
217
+ drawObject: function (ctx) {
218
+ ctx.save();
219
+ ctx.fillStyle = 'black';
220
+ ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
221
+ ctx.restore();
222
+ this.callSuper('drawObject', ctx);
229
223
  },
230
224
 
231
225
  /**
232
- * Returns an object representation of an instance
233
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
234
- * @return {Object} Object representation of an instance
226
+ * eraser should retain size
227
+ * dimensions should not change when paths are added or removed
228
+ * handled by {@link fabric.Object#_drawClipPath}
229
+ * @override
230
+ * @private
235
231
  */
236
- toObject: function (additionalProperties) {
237
- return _groupToObject.call(this, ['eraser'].concat(additionalProperties));
238
- }
232
+ _getBounds: function () {
233
+ // noop
234
+ },
235
+
236
+ /* _TO_SVG_START_ */
237
+ /**
238
+ * Returns svg representation of an instance
239
+ * use <mask> to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649
240
+ * for masking we need to add a white rect before all paths
241
+ *
242
+ * @param {Function} [reviver] Method for further parsing of svg representation.
243
+ * @return {String} svg representation of an instance
244
+ */
245
+ _toSVG: function (reviver) {
246
+ var svgString = ['<g ', 'COMMON_PARTS', ' >\n'];
247
+ var x = -this.width / 2, y = -this.height / 2;
248
+ var rectSvg = [
249
+ '<rect ', 'fill="white" ',
250
+ 'x="', x, '" y="', y,
251
+ '" width="', this.width, '" height="', this.height,
252
+ '" />\n'
253
+ ].join('');
254
+ svgString.push('\t\t', rectSvg);
255
+ for (var i = 0, len = this._objects.length; i < len; i++) {
256
+ svgString.push('\t\t', this._objects[i].toSVG(reviver));
257
+ }
258
+ svgString.push('</g>\n');
259
+ return svgString;
260
+ },
261
+ /* _TO_SVG_END_ */
239
262
  });
240
263
 
264
+ /**
265
+ * Returns {@link fabric.Eraser} instance from an object representation
266
+ * @static
267
+ * @memberOf fabric.Eraser
268
+ * @param {Object} object Object to create an Eraser from
269
+ * @param {Function} [callback] Callback to invoke when an eraser instance is created
270
+ */
271
+ fabric.Eraser.fromObject = function (object, callback) {
272
+ var objects = object.objects;
273
+ fabric.util.enlivenObjects(objects, function (enlivenedObjects) {
274
+ var options = fabric.util.object.clone(object, true);
275
+ delete options.objects;
276
+ fabric.util.enlivenObjectEnlivables(object, options, function () {
277
+ callback && callback(new fabric.Eraser(enlivenedObjects, options, true));
278
+ });
279
+ });
280
+ };
281
+
282
+ var __renderOverlay = fabric.Canvas.prototype._renderOverlay;
241
283
  /**
242
284
  * @fires erasing:start
243
285
  * @fires erasing:end
@@ -257,43 +299,36 @@
257
299
  },
258
300
 
259
301
  /**
260
- * While erasing, the brush is in charge of rendering the canvas
261
- * It uses both layers to achieve diserd erasing effect
262
- *
263
- * @returns fabric.Canvas
302
+ * While erasing the brush clips out the erasing path from canvas
303
+ * so we need to render it on top of canvas every render
304
+ * @param {CanvasRenderingContext2D} ctx
264
305
  */
265
- renderAll: function () {
266
- if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) {
267
- this.clearContext(this.contextTop);
268
- this.contextTopDirty = false;
269
- }
270
- // while erasing the brush is in charge of rendering the canvas so we return
271
- if (this.isErasing()) {
306
+ _renderOverlay: function (ctx) {
307
+ __renderOverlay.call(this, ctx);
308
+ if (this.isErasing() && !this.freeDrawingBrush.inverted) {
272
309
  this.freeDrawingBrush._render();
273
- return;
274
310
  }
275
- if (this.hasLostContext) {
276
- this.renderTopLayer(this.contextTop);
277
- }
278
- var canvasToDrawOn = this.contextContainer;
279
- this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender());
280
- return this;
281
311
  }
282
312
  });
283
313
 
284
314
  /**
285
315
  * EraserBrush class
286
316
  * Supports selective erasing meaning that only erasable objects are affected by the eraser brush.
287
- * In order to support selective erasing all non erasable objects are rendered on the main/bottom ctx
288
- * while the entire canvas is rendered on the top ctx.
289
- * Canvas background/overlay images are handled as well.
290
- * When erasing occurs, the path clips the top ctx and reveals the bottom ctx.
291
- * This achieves the desired effect of seeming to erase only erasable objects.
292
- * After erasing is done the created path is added to all intersected objects' `clipPath` property.
317
+ * Supports **inverted** erasing meaning that the brush can "undo" erasing.
318
+ *
319
+ * In order to support selective erasing, the brush clips the entire canvas
320
+ * and then draws all non-erasable objects over the erased path using a pattern brush so to speak (masking).
321
+ * If brush is **inverted** there is no need to clip canvas. The brush draws all erasable objects without their eraser.
322
+ * This achieves the desired effect of seeming to erase or unerase only erasable objects.
323
+ * After erasing is done the created path is added to all intersected objects' `eraser` property.
324
+ *
325
+ * In order to update the EraserBrush call `preparePattern`.
326
+ * It may come in handy when canvas changes during erasing (i.e animations) and you want the eraser to reflect the changes.
293
327
  *
294
328
  * @tutorial {@link http://fabricjs.com/erasing}
295
329
  * @class fabric.EraserBrush
296
330
  * @extends fabric.PencilBrush
331
+ * @memberof fabric
297
332
  */
298
333
  fabric.EraserBrush = fabric.util.createClass(
299
334
  fabric.PencilBrush,
@@ -301,52 +336,15 @@
301
336
  type: 'eraser',
302
337
 
303
338
  /**
304
- * Indicates that the ctx is ready and rendering can begin.
305
- * Used to prevent a race condition caused by {@link fabric.EraserBrush#onMouseMove} firing before {@link fabric.EraserBrush#onMouseDown} has completed
306
- *
307
- * @private
308
- */
309
- _ready: false,
310
-
311
- /**
312
- * @private
339
+ * When set to `true` the brush will create a visual effect of undoing erasing
313
340
  */
314
- _drawOverlayOnTop: false,
341
+ inverted: false,
315
342
 
316
343
  /**
317
344
  * @private
318
345
  */
319
346
  _isErasing: false,
320
347
 
321
- initialize: function (canvas) {
322
- this.callSuper('initialize', canvas);
323
- this._renderBound = this._render.bind(this);
324
- this.render = this.render.bind(this);
325
- },
326
-
327
- /**
328
- * Used to hide a drawable from the rendering process
329
- * @param {fabric.Object} object
330
- */
331
- hideObject: function (object) {
332
- if (object) {
333
- object._originalOpacity = object.opacity;
334
- object.set({ opacity: 0 });
335
- }
336
- },
337
-
338
- /**
339
- * Restores hiding an object
340
- * {@link fabric.EraserBrush#hideObject}
341
- * @param {fabric.Object} object
342
- */
343
- restoreObjectVisibility: function (object) {
344
- if (object && object._originalOpacity) {
345
- object.set({ opacity: object._originalOpacity });
346
- object._originalOpacity = undefined;
347
- }
348
- },
349
-
350
348
  /**
351
349
  *
352
350
  * @private
@@ -357,201 +355,137 @@
357
355
  return object.erasable !== false;
358
356
  },
359
357
 
360
- /**
361
- * Drawing Logic For background drawables: (`backgroundImage`)
362
- * 1. if erasable = true:
363
- * 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
364
- * 2. if erasable = false:
365
- * we need to draw the drawable only on the bottom ctx so the brush won't affect it
366
- * @param {'bottom' | 'top' | 'overlay'} layer
367
- */
368
- prepareCanvasBackgroundForLayer: function (layer) {
369
- if (layer === 'overlay') {
370
- return;
371
- }
372
- var canvas = this.canvas;
373
- var image = canvas.backgroundImage;
374
- var erasablesOnLayer = layer === 'top';
375
- if (image && this._isErasable(image) === !erasablesOnLayer) {
376
- this.hideObject(image);
377
- }
378
- },
379
-
380
- /**
381
- * Drawing Logic For overlay drawables (`overlayImage`)
382
- * We must draw on top ctx to be on top of visible canvas
383
- * 1. if erasable = true:
384
- * we need to draw the drawable on the top ctx as a normal object
385
- * 2. if erasable = false:
386
- * we need to draw the drawable on top of the brush,
387
- * this means we need to repaint for every stroke
388
- *
389
- * @param {'bottom' | 'top' | 'overlay'} layer
390
- * @returns boolean render overlay above brush
391
- */
392
- prepareCanvasOverlayForLayer: function (layer) {
393
- var canvas = this.canvas;
394
- var image = canvas.overlayImage;
395
- var hasOverlayColor = !!canvas.overlayColor;
396
- if (canvas.overlayColor && layer !== 'overlay') {
397
- this.__overlayColor = canvas.overlayColor;
398
- delete canvas.overlayColor;
399
- }
400
- if (layer === 'bottom') {
401
- this.hideObject(image);
402
- return false;
403
- };
404
- var erasablesOnLayer = layer === 'top';
405
- var renderOverlayOnTop = (image && !this._isErasable(image)) || hasOverlayColor;
406
- if (image && this._isErasable(image) === !erasablesOnLayer) {
407
- this.hideObject(image);
408
- }
409
- return renderOverlayOnTop;
410
- },
411
-
412
358
  /**
413
359
  * @private
414
- */
415
- restoreCanvasDrawables: function () {
416
- var canvas = this.canvas;
417
- if (this.__overlayColor) {
418
- canvas.overlayColor = this.__overlayColor;
419
- delete this.__overlayColor;
420
- }
421
- this.restoreObjectVisibility(canvas.backgroundImage);
422
- this.restoreObjectVisibility(canvas.overlayImage);
423
- },
424
-
425
- /**
426
- * @private
427
- * This is designed to support erasing a group with both erasable and non-erasable objects.
360
+ * This is designed to support erasing a collection with both erasable and non-erasable objects.
428
361
  * Iterates over collections to allow nested selective erasing.
429
- * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
430
- * to prepare the bottom layer by hiding erasable nested objects
362
+ * Prepares the pattern brush that will draw on the top context to achieve the desired visual effect.
363
+ * If brush is **NOT** inverted render all non-erasable objects.
364
+ * If brush is inverted render all erasable objects that have been erased with their clip path inverted.
365
+ * This will render the erased parts as if they were not erased.
431
366
  *
432
367
  * @param {fabric.Collection} collection
368
+ * @param {CanvasRenderingContext2D} ctx
369
+ * @param {{ visibility: fabric.Object[], eraser: fabric.Object[], collection: fabric.Object[] }} restorationContext
433
370
  */
434
- prepareCollectionTraversal: function (collection) {
435
- var _this = this;
371
+ _prepareCollectionTraversal: function (collection, ctx, restorationContext) {
436
372
  collection.forEachObject(function (obj) {
437
373
  if (obj.forEachObject && obj.erasable === 'deep') {
438
- _this.prepareCollectionTraversal(obj);
439
- }
440
- else if (obj.erasable) {
441
- _this.hideObject(obj);
374
+ // traverse
375
+ this._prepareCollectionTraversal(obj, ctx, restorationContext);
442
376
  }
443
- });
444
- },
445
-
446
- /**
447
- * @private
448
- * Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
449
- * to reverse the action of {@link fabric.EraserBrush#prepareCollectionTraversal}
450
- *
451
- * @param {fabric.Collection} collection
452
- */
453
- restoreCollectionTraversal: function (collection) {
454
- var _this = this;
455
- collection.forEachObject(function (obj) {
456
- if (obj.forEachObject && obj.erasable === 'deep') {
457
- _this.restoreCollectionTraversal(obj);
377
+ else if (!this.inverted && obj.erasable && obj.visible) {
378
+ // render only non-erasable objects
379
+ obj.visible = false;
380
+ collection.dirty = true;
381
+ restorationContext.visibility.push(obj);
382
+ restorationContext.collection.push(collection);
458
383
  }
459
- else {
460
- _this.restoreObjectVisibility(obj);
384
+ else if (this.inverted && obj.visible) {
385
+ // render only erasable objects that were erased
386
+ if (obj.erasable && obj.eraser) {
387
+ obj.eraser.inverted = true;
388
+ obj.dirty = true;
389
+ collection.dirty = true;
390
+ restorationContext.eraser.push(obj);
391
+ restorationContext.collection.push(collection);
392
+ }
393
+ else {
394
+ obj.visible = false;
395
+ collection.dirty = true;
396
+ restorationContext.visibility.push(obj);
397
+ restorationContext.collection.push(collection);
398
+ }
461
399
  }
462
- });
463
- },
464
-
465
- /**
466
- * @private
467
- * This is designed to support erasing a group with both erasable and non-erasable objects.
468
- *
469
- * @param {'bottom' | 'top' | 'overlay'} layer
470
- */
471
- prepareCanvasObjectsForLayer: function (layer) {
472
- if (layer !== 'bottom') { return; }
473
- this.prepareCollectionTraversal(this.canvas);
400
+ }, this);
474
401
  },
475
402
 
476
403
  /**
404
+ * Prepare the pattern for the erasing brush
405
+ * This pattern will be drawn on the top context, achieving a visual effect of erasing only erasable objects
406
+ * @todo decide how overlay color should behave when `inverted === true`, currently draws over it which is undesirable
477
407
  * @private
478
- * @param {'bottom' | 'top' | 'overlay'} layer
479
408
  */
480
- restoreCanvasObjectsFromLayer: function (layer) {
481
- if (layer !== 'bottom') { return; }
482
- this.restoreCollectionTraversal(this.canvas);
409
+ preparePattern: function () {
410
+ if (!this._patternCanvas) {
411
+ this._patternCanvas = fabric.util.createCanvasElement();
412
+ }
413
+ var canvas = this._patternCanvas;
414
+ canvas.width = this.canvas.width;
415
+ canvas.height = this.canvas.height;
416
+ var patternCtx = canvas.getContext('2d');
417
+ if (this.canvas._isRetinaScaling()) {
418
+ var retinaScaling = this.canvas.getRetinaScaling();
419
+ this.canvas.__initRetinaScaling(retinaScaling, canvas, patternCtx);
420
+ }
421
+ var backgroundImage = this.canvas.backgroundImage,
422
+ bgErasable = backgroundImage && this._isErasable(backgroundImage),
423
+ overlayImage = this.canvas.overlayImage,
424
+ overlayErasable = overlayImage && this._isErasable(overlayImage);
425
+ if (!this.inverted && ((backgroundImage && !bgErasable) || !!this.canvas.backgroundColor)) {
426
+ if (bgErasable) { this.canvas.backgroundImage = undefined; }
427
+ this.canvas._renderBackground(patternCtx);
428
+ if (bgErasable) { this.canvas.backgroundImage = backgroundImage; }
429
+ }
430
+ else if (this.inverted && (backgroundImage && bgErasable)) {
431
+ var color = this.canvas.backgroundColor;
432
+ this.canvas.backgroundColor = undefined;
433
+ this.canvas._renderBackground(patternCtx);
434
+ this.canvas.backgroundColor = color;
435
+ }
436
+ patternCtx.save();
437
+ patternCtx.transform.apply(patternCtx, this.canvas.viewportTransform);
438
+ var restorationContext = { visibility: [], eraser: [], collection: [] };
439
+ this._prepareCollectionTraversal(this.canvas, patternCtx, restorationContext);
440
+ this.canvas._renderObjects(patternCtx, this.canvas._objects);
441
+ restorationContext.visibility.forEach(function (obj) { obj.visible = true; });
442
+ restorationContext.eraser.forEach(function (obj) {
443
+ obj.eraser.inverted = false;
444
+ obj.dirty = true;
445
+ });
446
+ restorationContext.collection.forEach(function (obj) { obj.dirty = true; });
447
+ patternCtx.restore();
448
+ if (!this.inverted && ((overlayImage && !overlayErasable) || !!this.canvas.overlayColor)) {
449
+ if (overlayErasable) { this.canvas.overlayImage = undefined; }
450
+ __renderOverlay.call(this.canvas, patternCtx);
451
+ if (overlayErasable) { this.canvas.overlayImage = overlayImage; }
452
+ }
453
+ else if (this.inverted && (overlayImage && overlayErasable)) {
454
+ var color = this.canvas.overlayColor;
455
+ this.canvas.overlayColor = undefined;
456
+ __renderOverlay.call(this.canvas, patternCtx);
457
+ this.canvas.overlayColor = color;
458
+ }
483
459
  },
484
460
 
485
461
  /**
462
+ * Sets brush styles
486
463
  * @private
487
- * @param {'bottom' | 'top' | 'overlay'} layer
488
- * @returns boolean render overlay above brush
489
- */
490
- prepareCanvasForLayer: function (layer) {
491
- this.prepareCanvasBackgroundForLayer(layer);
492
- this.prepareCanvasObjectsForLayer(layer);
493
- return this.prepareCanvasOverlayForLayer(layer);
494
- },
495
-
496
- /**
497
- * @private
498
- * @param {'bottom' | 'top' | 'overlay'} layer
499
- */
500
- restoreCanvasFromLayer: function (layer) {
501
- this.restoreCanvasDrawables();
502
- this.restoreCanvasObjectsFromLayer(layer);
503
- },
504
-
505
- /**
506
- * Render all non-erasable objects on bottom layer with the exception of overlays to avoid being clipped by the brush.
507
- * Groups are rendered for nested selective erasing, non-erasable objects are visible while erasable objects are not.
508
- */
509
- renderBottomLayer: function () {
510
- var canvas = this.canvas;
511
- this.prepareCanvasForLayer('bottom');
512
- canvas.renderCanvas(
513
- canvas.getContext(),
514
- canvas.getObjects().filter(function (obj) {
515
- return !obj.erasable || obj.forEachObject;
516
- })
517
- );
518
- this.restoreCanvasFromLayer('bottom');
519
- },
520
-
521
- /**
522
- * 1. Render all objects on top layer, erasable and non-erasable
523
- * This is important for cases such as overlapping objects, the background object erasable and the foreground object not erasable.
524
- * 2. Render the brush
525
- */
526
- renderTopLayer: function () {
527
- var canvas = this.canvas;
528
- this._drawOverlayOnTop = this.prepareCanvasForLayer('top');
529
- canvas.renderCanvas(
530
- canvas.contextTop,
531
- canvas.getObjects()
532
- );
533
- this.callSuper('_render');
534
- this.restoreCanvasFromLayer('top');
535
- },
536
-
537
- /**
538
- * Render all non-erasable overlays on top of the brush so that they won't get erased
464
+ * @param {CanvasRenderingContext2D} ctx
539
465
  */
540
- renderOverlay: function () {
541
- this.prepareCanvasForLayer('overlay');
542
- var canvas = this.canvas;
543
- var ctx = canvas.contextTop;
544
- canvas._renderOverlay(ctx);
545
- this.restoreCanvasFromLayer('overlay');
466
+ _setBrushStyles: function (ctx) {
467
+ this.callSuper('_setBrushStyles', ctx);
468
+ ctx.strokeStyle = 'black';
546
469
  },
547
470
 
548
471
  /**
549
- * @extends @class fabric.BaseBrush
472
+ * **Customiztion**
473
+ *
474
+ * if you need the eraser to update on each render (i.e animating during erasing) override this method by **adding** the following (performance may suffer):
475
+ * @example
476
+ * ```
477
+ * if(ctx === this.canvas.contextTop) {
478
+ * this.preparePattern();
479
+ * }
480
+ * ```
481
+ *
482
+ * @override fabric.BaseBrush#_saveAndTransform
550
483
  * @param {CanvasRenderingContext2D} ctx
551
484
  */
552
485
  _saveAndTransform: function (ctx) {
553
486
  this.callSuper('_saveAndTransform', ctx);
554
- ctx.globalCompositeOperation = 'destination-out';
487
+ this._setBrushStyles(ctx);
488
+ ctx.globalCompositeOperation = ctx === this.canvas.getContext() ? 'destination-out' : 'source-over';
555
489
  },
556
490
 
557
491
  /**
@@ -559,7 +493,7 @@
559
493
  * @returns
560
494
  */
561
495
  needsFullRender: function () {
562
- return this.callSuper('needsFullRender') || this._drawOverlayOnTop;
496
+ return true;
563
497
  },
564
498
 
565
499
  /**
@@ -577,64 +511,74 @@
577
511
  // this allows to draw dots (when movement never occurs)
578
512
  this._captureDrawingPath(pointer);
579
513
 
514
+ // prepare for erasing
515
+ this.preparePattern();
580
516
  this._isErasing = true;
581
517
  this.canvas.fire('erasing:start');
582
- this._ready = true;
583
518
  this._render();
584
519
  },
585
520
 
586
521
  /**
587
- * Rendering is done in 4 steps:
588
- * 1. Draw all non-erasable objects on bottom ctx with the exception of overlays {@link fabric.EraserBrush#renderBottomLayer}
589
- * 2. Draw all objects on top ctx including erasable drawables {@link fabric.EraserBrush#renderTopLayer}
590
- * 3. Draw eraser {@link fabric.PencilBrush#_render} at {@link fabric.EraserBrush#renderTopLayer}
591
- * 4. Draw non-erasable overlays {@link fabric.EraserBrush#renderOverlay}
522
+ * Rendering Logic:
523
+ * 1. Use brush to clip canvas by rendering it on top of canvas (unnecessary if `inverted === true`)
524
+ * 2. Render brush with canvas pattern on top context
592
525
  *
593
- * @param {fabric.Canvas} canvas
594
526
  */
595
527
  _render: function () {
596
- if (!this._ready) {
597
- return;
528
+ var ctx;
529
+ if (!this.inverted) {
530
+ // clip canvas
531
+ ctx = this.canvas.getContext();
532
+ this.callSuper('_render', ctx);
598
533
  }
599
- this.isRendering = 1;
600
- this.renderBottomLayer();
601
- this.renderTopLayer();
602
- this.renderOverlay();
603
- this.isRendering = 0;
534
+ // render brush and mask it with image of non erasables
535
+ ctx = this.canvas.contextTop;
536
+ this.canvas.clearContext(ctx);
537
+ this.callSuper('_render', ctx);
538
+ ctx.save();
539
+ var t = this.canvas.getRetinaScaling(), s = 1 / t;
540
+ ctx.scale(s, s);
541
+ ctx.globalCompositeOperation = 'source-in';
542
+ ctx.drawImage(this._patternCanvas, 0, 0);
543
+ ctx.restore();
604
544
  },
605
545
 
606
546
  /**
607
- * @public
547
+ * Creates fabric.Path object
548
+ * @override
549
+ * @private
550
+ * @param {(string|number)[][]} pathData Path data
551
+ * @return {fabric.Path} Path to add on canvas
552
+ * @returns
608
553
  */
609
- render: function () {
610
- if (this._isErasing) {
611
- if (this.isRendering) {
612
- this.isRendering = fabric.util.requestAnimFrame(this._renderBound);
613
- }
614
- else {
615
- this._render();
616
- }
617
- return true;
618
- }
619
- return false;
554
+ createPath: function (pathData) {
555
+ var path = this.callSuper('createPath', pathData);
556
+ path.globalCompositeOperation = this.inverted ? 'source-over' : 'destination-out';
557
+ path.stroke = this.inverted ? 'white' : 'black';
558
+ return path;
620
559
  },
621
560
 
622
561
  /**
623
562
  * Utility to apply a clip path to a path.
624
563
  * Used to preserve clipping on eraser paths in nested objects.
625
564
  * Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects.
626
- * @param {fabric.Path} path The eraser path
565
+ * @param {fabric.Path} path The eraser path in canvas coordinate plane
627
566
  * @param {fabric.Object} clipPath The clipPath to apply to the path
628
567
  * @param {number[]} clipPathContainerTransformMatrix The transform matrix of the object that the clip path belongs to
629
568
  * @returns {fabric.Path} path with clip path
630
569
  */
631
570
  applyClipPathToPath: function (path, clipPath, clipPathContainerTransformMatrix) {
632
- var pathTransform = path.calcTransformMatrix();
633
- var clipPathTransform = clipPath.calcTransformMatrix();
634
- var transform = fabric.util.multiplyTransformMatrices(
635
- fabric.util.invertTransform(pathTransform),
636
- clipPathContainerTransformMatrix
637
- );
571
+ var pathInvTransform = fabric.util.invertTransform(path.calcTransformMatrix()),
572
+ clipPathTransform = clipPath.calcTransformMatrix(),
573
+ transform = clipPath.absolutePositioned ?
574
+ pathInvTransform :
575
+ fabric.util.multiplyTransformMatrices(
576
+ pathInvTransform,
577
+ clipPathContainerTransformMatrix
578
+ );
579
+ // when passing down a clip path it becomes relative to the parent
580
+ // so we transform it acoordingly and set `absolutePositioned` to false
581
+ clipPath.absolutePositioned = false;
638
582
  fabric.util.applyTransformToObject(
639
583
  clipPath,
640
584
  fabric.util.multiplyTransformMatrices(
@@ -642,7 +586,11 @@
642
586
  clipPathTransform
643
587
  )
644
588
  );
645
- path.clipPath = clipPath;
589
+ // We need to clip `path` with both `clipPath` and it's own clip path if existing (`path.clipPath`)
590
+ // so in turn `path` erases an object only where it overlaps with all it's clip paths, regardless of how many there are.
591
+ // this is done because both clip paths may have nested clip paths of their own (this method walks down a collection => this may reccur),
592
+ // so we can't assign one to the other's clip path property.
593
+ path.clipPath = path.clipPath ? fabric.util.mergeClipPaths(clipPath, path.clipPath) : clipPath;
646
594
  return path;
647
595
  },
648
596
 
@@ -656,23 +604,23 @@
656
604
  */
657
605
  clonePathWithClipPath: function (path, object, callback) {
658
606
  var objTransform = object.calcTransformMatrix();
659
- var clipPath = object.getClipPath();
607
+ var clipPath = object.clipPath;
660
608
  var _this = this;
661
609
  path.clone(function (_path) {
662
610
  clipPath.clone(function (_clipPath) {
663
611
  callback(_this.applyClipPathToPath(_path, _clipPath, objTransform));
664
- });
612
+ }, ['absolutePositioned', 'inverted']);
665
613
  });
666
614
  },
667
615
 
668
616
  /**
669
- * Adds path to existing clipPath of object
617
+ * Adds path to object's eraser, walks down object's descendants if necessary
670
618
  *
619
+ * @fires erasing:end on object
671
620
  * @param {fabric.Object} obj
672
621
  * @param {fabric.Path} path
673
622
  */
674
623
  _addPathToObjectEraser: function (obj, path) {
675
- var clipObject;
676
624
  var _this = this;
677
625
  // object is collection, i.e group
678
626
  if (obj.forEachObject && obj.erasable === 'deep') {
@@ -693,26 +641,14 @@
693
641
  }
694
642
  return;
695
643
  }
696
- if (!obj.getEraser()) {
697
- var size = obj._getNonTransformedDimensions();
698
- var rect = new fabric.Rect({
699
- fill: 'rgb(0,0,0)',
700
- width: size.x,
701
- height: size.y,
702
- clipPath: obj.clipPath,
703
- originX: 'center',
704
- originY: 'center'
705
- });
706
- clipObject = new fabric.Group([rect], {
707
- eraser: true
708
- });
709
- }
710
- else {
711
- clipObject = obj.clipPath;
644
+ // prepare eraser
645
+ var eraser = obj.eraser;
646
+ if (!eraser) {
647
+ eraser = new fabric.Eraser();
648
+ obj.eraser = eraser;
712
649
  }
713
-
650
+ // clone and add path
714
651
  path.clone(function (path) {
715
- path.globalCompositeOperation = 'destination-out';
716
652
  // http://fabricjs.com/using-transformations
717
653
  var desiredTransform = fabric.util.multiplyTransformMatrices(
718
654
  fabric.util.invertTransform(
@@ -721,11 +657,8 @@
721
657
  path.calcTransformMatrix()
722
658
  );
723
659
  fabric.util.applyTransformToObject(path, desiredTransform);
724
- clipObject.addWithUpdate(path);
725
- obj.set({
726
- clipPath: clipObject,
727
- dirty: true
728
- });
660
+ eraser.addWithUpdate(path);
661
+ obj.set('dirty', true);
729
662
  obj.fire('erasing:end', {
730
663
  path: path
731
664
  });
@@ -790,6 +723,7 @@
790
723
  var path = this.createPath(pathData);
791
724
  // needed for `intersectsWithObject`
792
725
  path.setCoords();
726
+ // commense event sequence
793
727
  canvas.fire('before:path:created', { path: path });
794
728
 
795
729
  // finalize erasing
@@ -803,6 +737,7 @@
803
737
  targets.push(obj);
804
738
  }
805
739
  });
740
+ // fire erasing:end
806
741
  canvas.fire('erasing:end', {
807
742
  path: path,
808
743
  targets: targets,
@@ -812,7 +747,6 @@
812
747
  delete this.__subTargets;
813
748
 
814
749
  canvas.requestRenderAll();
815
- path.setCoords();
816
750
  this._resetShadow();
817
751
 
818
752
  // fire event 'path' created