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.
- package/CHANGELOG.md +42 -0
- package/HEADER.js +1 -1
- package/README.md +1 -6
- package/SECURITY.md +5 -0
- package/dist/fabric.js +652 -393
- package/dist/fabric.min.js +1 -1
- package/lib/event.js +7 -3
- package/package.json +87 -88
- package/src/brushes/base_brush.class.js +2 -2
- package/src/brushes/pattern_brush.class.js +7 -5
- package/src/brushes/pencil_brush.class.js +33 -4
- package/src/canvas.class.js +9 -17
- package/src/mixins/animation.mixin.js +16 -21
- package/src/mixins/canvas_events.mixin.js +27 -56
- package/src/mixins/eraser_brush.mixin.js +354 -420
- package/src/mixins/itext_behavior.mixin.js +7 -1
- package/src/mixins/object_geometry.mixin.js +3 -34
- package/src/mixins/object_straightening.mixin.js +4 -9
- package/src/parser.js +13 -9
- package/src/shapes/circle.class.js +20 -17
- package/src/shapes/group.class.js +13 -25
- package/src/shapes/image.class.js +3 -3
- package/src/shapes/object.class.js +45 -19
- package/src/shapes/path.class.js +13 -9
- package/src/shapes/polygon.class.js +9 -1
- package/src/shapes/polyline.class.js +33 -9
- package/src/shapes/text.class.js +38 -21
- package/src/static_canvas.class.js +15 -7
- package/src/util/animate.js +146 -22
- package/src/util/misc.js +193 -45
- package/src/util/path.js +2 -56
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
/** ERASER_START */
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
* @
|
|
32
|
+
* @tutorial {@link http://fabricjs.com/erasing#eraser}
|
|
33
|
+
* @type fabric.Eraser
|
|
26
34
|
*/
|
|
27
|
-
|
|
28
|
-
return this.clipPath && this.clipPath.eraser ? this.clipPath : undefined;
|
|
29
|
-
},
|
|
35
|
+
eraser: undefined,
|
|
30
36
|
|
|
31
37
|
/**
|
|
32
|
-
*
|
|
33
|
-
* @returns
|
|
38
|
+
* @override
|
|
39
|
+
* @returns Boolean
|
|
34
40
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
*
|
|
42
|
-
* @
|
|
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 {
|
|
55
|
-
* @param {
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
if (eraser) {
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
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 (
|
|
102
|
-
|
|
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
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* @
|
|
109
|
-
* @returns {string} markup
|
|
80
|
+
* Returns id attribute for svg output
|
|
81
|
+
* @override
|
|
82
|
+
* @return {String}
|
|
110
83
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
this.
|
|
164
|
-
this
|
|
165
|
-
|
|
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 (
|
|
150
|
+
var _this = this, eraser = this.eraser;
|
|
151
|
+
if (eraser) {
|
|
152
|
+
delete this.eraser;
|
|
191
153
|
var transform = _this.calcTransformMatrix();
|
|
192
|
-
|
|
193
|
-
var 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(
|
|
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
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* @
|
|
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
|
-
|
|
237
|
-
|
|
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
|
|
261
|
-
*
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
430
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
if (
|
|
482
|
-
|
|
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 {
|
|
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
|
-
|
|
541
|
-
this.
|
|
542
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
588
|
-
* 1.
|
|
589
|
-
* 2.
|
|
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
|
-
|
|
597
|
-
|
|
528
|
+
var ctx;
|
|
529
|
+
if (!this.inverted) {
|
|
530
|
+
// clip canvas
|
|
531
|
+
ctx = this.canvas.getContext();
|
|
532
|
+
this.callSuper('_render', ctx);
|
|
598
533
|
}
|
|
599
|
-
|
|
600
|
-
this.
|
|
601
|
-
this.
|
|
602
|
-
this.
|
|
603
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
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
|