fabric 4.5.1-browser → 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 +78 -0
- package/CONTRIBUTING.md +14 -0
- package/HEADER.js +1 -1
- package/README.md +3 -6
- package/SECURITY.md +5 -0
- package/build.js +1 -0
- package/dist/fabric.js +1160 -668
- package/dist/fabric.min.js +1 -1
- package/lib/event.js +7 -3
- package/package.json +9 -3
- package/publish-next.js +15 -0
- package/publish.js +3 -0
- package/src/brushes/base_brush.class.js +3 -5
- package/src/brushes/pattern_brush.class.js +7 -5
- package/src/brushes/pencil_brush.class.js +49 -37
- package/src/canvas.class.js +27 -57
- package/src/filters/saturate_filter.class.js +9 -1
- package/src/filters/vibrance_filter.class.js +122 -0
- package/src/mixins/animation.mixin.js +20 -25
- package/src/mixins/canvas_events.mixin.js +30 -59
- package/src/mixins/canvas_grouping.mixin.js +4 -4
- package/src/mixins/collection.mixin.js +11 -2
- package/src/mixins/eraser_brush.mixin.js +495 -452
- package/src/mixins/itext_behavior.mixin.js +7 -1
- package/src/mixins/itext_key_behavior.mixin.js +7 -1
- package/src/mixins/object_geometry.mixin.js +5 -35
- package/src/mixins/object_interactivity.mixin.js +18 -7
- package/src/mixins/object_straightening.mixin.js +4 -9
- package/src/mixins/observable.mixin.js +22 -0
- package/src/parser.js +13 -9
- package/src/shapes/circle.class.js +22 -19
- package/src/shapes/ellipse.class.js +2 -2
- package/src/shapes/group.class.js +24 -32
- package/src/shapes/image.class.js +3 -25
- package/src/shapes/itext.class.js +10 -0
- package/src/shapes/line.class.js +5 -21
- package/src/shapes/object.class.js +67 -32
- package/src/shapes/path.class.js +17 -18
- package/src/shapes/polygon.class.js +11 -10
- package/src/shapes/polyline.class.js +33 -24
- package/src/shapes/rect.class.js +0 -18
- package/src/shapes/text.class.js +120 -58
- package/src/shapes/triangle.class.js +0 -15
- package/src/static_canvas.class.js +43 -20
- package/src/util/animate.js +149 -16
- package/src/util/animate_color.js +2 -1
- package/src/util/lang_object.js +5 -1
- package/src/util/misc.js +193 -42
- package/src/util/path.js +89 -52
|
@@ -1,210 +1,289 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
/** ERASER_START */
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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;
|
|
11
|
+
var _toObject = fabric.Object.prototype.toObject;
|
|
12
|
+
var _getSvgCommons = fabric.Object.prototype.getSvgCommons;
|
|
13
|
+
var __createBaseClipPathSVGMarkup = fabric.Object.prototype._createBaseClipPathSVGMarkup;
|
|
14
|
+
var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup;
|
|
15
|
+
/**
|
|
16
|
+
* @fires erasing:end
|
|
17
|
+
*/
|
|
18
|
+
fabric.util.object.extend(fabric.Object.prototype, {
|
|
9
19
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @
|
|
16
|
-
* @
|
|
17
|
-
* @
|
|
18
|
-
* @chainable true
|
|
20
|
+
* Indicates whether this object can be erased by {@link fabric.EraserBrush}
|
|
21
|
+
* The `deep` option introduces fine grained control over a group's `erasable` property.
|
|
22
|
+
* When set to `deep` the eraser will erase nested objects if they are erasable, leaving the group and the other objects untouched.
|
|
23
|
+
* When set to `true` the eraser will erase the entire group. Once the group changes the eraser is propagated to its children for proper functionality.
|
|
24
|
+
* When set to `false` the eraser will leave all objects including the group untouched.
|
|
25
|
+
* @tutorial {@link http://fabricjs.com/erasing#erasable_property}
|
|
26
|
+
* @type boolean | 'deep'
|
|
27
|
+
* @default true
|
|
19
28
|
*/
|
|
20
|
-
|
|
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
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
var _this = this;
|
|
29
|
-
var cb = function () {
|
|
30
|
-
_this[property] = new fabric.Rect(fabric.util.object.extend({
|
|
31
|
-
width: _this.width,
|
|
32
|
-
height: _this.height,
|
|
33
|
-
fill: _this[property],
|
|
34
|
-
}, options));
|
|
35
|
-
callback && callback(_this[property]);
|
|
36
|
-
};
|
|
37
|
-
__setBgOverlayColor.call(this, property, color, cb);
|
|
38
|
-
// invoke cb in case of gradient
|
|
39
|
-
// see {@link CommonMethods#_initGradient}
|
|
40
|
-
if (color && color.colorStops && !(color instanceof fabric.Gradient)) {
|
|
41
|
-
cb();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return this;
|
|
46
|
-
},
|
|
29
|
+
erasable: true,
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
/**
|
|
32
|
+
* @tutorial {@link http://fabricjs.com/erasing#eraser}
|
|
33
|
+
* @type fabric.Eraser
|
|
34
|
+
*/
|
|
35
|
+
eraser: undefined,
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
/**
|
|
38
|
+
* @override
|
|
39
|
+
* @returns Boolean
|
|
40
|
+
*/
|
|
41
|
+
needsItsOwnCache: function () {
|
|
42
|
+
return _needsItsOwnCache.call(this) || !!this.eraser;
|
|
54
43
|
},
|
|
55
44
|
|
|
56
45
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
46
|
+
* draw eraser above clip path
|
|
47
|
+
* @override
|
|
59
48
|
* @private
|
|
60
|
-
* @param {
|
|
61
|
-
* @param {
|
|
62
|
-
* @param {Object} loaded Set loaded property to true if property is set
|
|
63
|
-
* @param {Object} callback Callback function to invoke after property is set
|
|
49
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
50
|
+
* @param {fabric.Object} clipPath
|
|
64
51
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
callback && callback();
|
|
52
|
+
_drawClipPath: function (ctx, clipPath) {
|
|
53
|
+
__drawClipPath.call(this, ctx, clipPath);
|
|
54
|
+
if (this.eraser) {
|
|
55
|
+
// update eraser size to match instance
|
|
56
|
+
var size = this._getNonTransformedDimensions();
|
|
57
|
+
this.eraser.isType('eraser') && this.eraser.set({
|
|
58
|
+
width: size.x,
|
|
59
|
+
height: size.y
|
|
74
60
|
});
|
|
75
|
-
|
|
76
|
-
else {
|
|
77
|
-
___setBgOverlay.call(this, property, value, loaded, callback);
|
|
61
|
+
__drawClipPath.call(this, ctx, this.eraser);
|
|
78
62
|
}
|
|
79
63
|
},
|
|
80
64
|
|
|
81
65
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @
|
|
66
|
+
* Returns an object representation of an instance
|
|
67
|
+
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
68
|
+
* @return {Object} Object representation of an instance
|
|
85
69
|
*/
|
|
86
|
-
|
|
87
|
-
var
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
if (filler && !excludeFromExport && filler.toSVG) {
|
|
91
|
-
markup.push(filler.toSVG(reviver));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
__setSVGBgOverlayColor.call(this, markup, property, reviver);
|
|
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);
|
|
96
74
|
}
|
|
75
|
+
return object;
|
|
97
76
|
},
|
|
98
77
|
|
|
78
|
+
/* _TO_SVG_START_ */
|
|
99
79
|
/**
|
|
100
|
-
*
|
|
101
|
-
* @
|
|
102
|
-
* @
|
|
80
|
+
* Returns id attribute for svg output
|
|
81
|
+
* @override
|
|
82
|
+
* @return {String}
|
|
103
83
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
v = this.viewportTransform, needsVpt = this[property + 'Vpt'];
|
|
107
|
-
if (!fill && !object) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (fill || object) {
|
|
111
|
-
ctx.save();
|
|
112
|
-
if (needsVpt) {
|
|
113
|
-
ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
|
|
114
|
-
}
|
|
115
|
-
fill && fill.render(ctx);
|
|
116
|
-
object && object.render(ctx);
|
|
117
|
-
ctx.restore();
|
|
118
|
-
}
|
|
84
|
+
getSvgCommons: function () {
|
|
85
|
+
return _getSvgCommons.call(this) + (this.eraser ? 'mask="url(#' + this.eraser.clipPathId + ')" ' : '');
|
|
119
86
|
},
|
|
120
|
-
});
|
|
121
87
|
|
|
122
|
-
var _toObject = fabric.Object.prototype.toObject;
|
|
123
|
-
var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup;
|
|
124
|
-
fabric.util.object.extend(fabric.Object.prototype, {
|
|
125
88
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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]
|
|
93
|
+
* @returns
|
|
129
94
|
*/
|
|
130
|
-
|
|
95
|
+
_createEraserSVGMarkup: function (reviver) {
|
|
96
|
+
if (this.eraser) {
|
|
97
|
+
this.eraser.clipPathId = 'MASK_' + fabric.Object.__uid++;
|
|
98
|
+
return [
|
|
99
|
+
'<mask id="', this.eraser.clipPathId, '" >',
|
|
100
|
+
this.eraser.toSVG(reviver),
|
|
101
|
+
'</mask>', '\n'
|
|
102
|
+
].join('');
|
|
103
|
+
}
|
|
104
|
+
return '';
|
|
105
|
+
},
|
|
131
106
|
|
|
132
107
|
/**
|
|
133
|
-
*
|
|
134
|
-
* @returns {fabric.Group | null}
|
|
108
|
+
* @private
|
|
135
109
|
*/
|
|
136
|
-
|
|
137
|
-
return
|
|
110
|
+
_createBaseClipPathSVGMarkup: function (objectMarkup, options) {
|
|
111
|
+
return [
|
|
112
|
+
this._createEraserSVGMarkup(options && options.reviver),
|
|
113
|
+
__createBaseClipPathSVGMarkup.call(this, objectMarkup, options)
|
|
114
|
+
].join('');
|
|
138
115
|
},
|
|
139
116
|
|
|
140
117
|
/**
|
|
141
|
-
*
|
|
142
|
-
|
|
143
|
-
|
|
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, {
|
|
131
|
+
/**
|
|
132
|
+
* @private
|
|
133
|
+
* @param {fabric.Path} path
|
|
144
134
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
135
|
+
_addEraserPathToObjects: function (path) {
|
|
136
|
+
this._objects.forEach(function (object) {
|
|
137
|
+
fabric.EraserBrush.prototype._addPathToObjectEraser.call(
|
|
138
|
+
fabric.EraserBrush.prototype,
|
|
139
|
+
object,
|
|
140
|
+
path
|
|
141
|
+
);
|
|
142
|
+
});
|
|
147
143
|
},
|
|
148
144
|
|
|
149
145
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* @param {Function} reviver
|
|
153
|
-
* @returns {string} markup
|
|
146
|
+
* Applies the group's eraser to its objects
|
|
147
|
+
* @tutorial {@link http://fabricjs.com/erasing#erasable_property}
|
|
154
148
|
*/
|
|
155
|
-
|
|
156
|
-
var eraser = this.
|
|
149
|
+
applyEraserToObjects: function () {
|
|
150
|
+
var _this = this, eraser = this.eraser;
|
|
157
151
|
if (eraser) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
eraser.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
152
|
+
delete this.eraser;
|
|
153
|
+
var transform = _this.calcTransformMatrix();
|
|
154
|
+
eraser.clone(function (eraser) {
|
|
155
|
+
var clipPath = _this.clipPath;
|
|
156
|
+
eraser.getObjects('path')
|
|
157
|
+
.forEach(function (path) {
|
|
158
|
+
// first we transform the path from the group's coordinate system to the canvas'
|
|
159
|
+
var originalTransform = fabric.util.multiplyTransformMatrices(
|
|
160
|
+
transform,
|
|
161
|
+
path.calcTransformMatrix()
|
|
162
|
+
);
|
|
163
|
+
fabric.util.applyTransformToObject(path, originalTransform);
|
|
164
|
+
if (clipPath) {
|
|
165
|
+
clipPath.clone(function (_clipPath) {
|
|
166
|
+
var eraserPath = fabric.EraserBrush.prototype.applyClipPathToPath.call(
|
|
167
|
+
fabric.EraserBrush.prototype,
|
|
168
|
+
path,
|
|
169
|
+
_clipPath,
|
|
170
|
+
transform
|
|
171
|
+
);
|
|
172
|
+
_this._addEraserPathToObjects(eraserPath);
|
|
173
|
+
}, ['absolutePositioned', 'inverted']);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
_this._addEraserPathToObjects(path);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
168
180
|
}
|
|
169
|
-
return '';
|
|
170
181
|
},
|
|
171
182
|
|
|
172
183
|
/**
|
|
173
|
-
*
|
|
174
|
-
* @
|
|
175
|
-
* @param {Object} options
|
|
176
|
-
* @returns
|
|
184
|
+
* Propagate the group's eraser to its objects, crucial for proper functionality of the eraser within the group and nested objects.
|
|
185
|
+
* @private
|
|
177
186
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
var eraserMarkup = this.eraserToSVG(options);
|
|
182
|
-
this.clipPath = null;
|
|
183
|
-
var markup = __createBaseSVGMarkup.call(this, objectMarkup, options);
|
|
184
|
-
this.clipPath = eraser;
|
|
185
|
-
return [
|
|
186
|
-
eraserMarkup,
|
|
187
|
-
markup.replace('>', 'mask="url(#' + eraser.clipPathId + ')" >')
|
|
188
|
-
].join('\n');
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
return __createBaseSVGMarkup.call(this, objectMarkup, options);
|
|
192
|
-
}
|
|
187
|
+
_restoreObjectsState: function () {
|
|
188
|
+
this.erasable === true && this.applyEraserToObjects();
|
|
189
|
+
return __restoreObjectsState.call(this);
|
|
193
190
|
}
|
|
194
191
|
});
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
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, {
|
|
198
201
|
/**
|
|
199
|
-
*
|
|
200
|
-
* @
|
|
201
|
-
* @return {Object} Object representation of an instance
|
|
202
|
+
* @readonly
|
|
203
|
+
* @static
|
|
202
204
|
*/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
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
|
|
231
|
+
*/
|
|
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_ */
|
|
206
262
|
});
|
|
207
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;
|
|
283
|
+
/**
|
|
284
|
+
* @fires erasing:start
|
|
285
|
+
* @fires erasing:end
|
|
286
|
+
*/
|
|
208
287
|
fabric.util.object.extend(fabric.Canvas.prototype, {
|
|
209
288
|
/**
|
|
210
289
|
* Used by {@link #renderAll}
|
|
@@ -220,44 +299,36 @@
|
|
|
220
299
|
},
|
|
221
300
|
|
|
222
301
|
/**
|
|
223
|
-
* While erasing
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* @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
|
|
227
305
|
*/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.contextTopDirty = false;
|
|
232
|
-
}
|
|
233
|
-
// while erasing the brush is in charge of rendering the canvas so we return
|
|
234
|
-
if (this.isErasing()) {
|
|
306
|
+
_renderOverlay: function (ctx) {
|
|
307
|
+
__renderOverlay.call(this, ctx);
|
|
308
|
+
if (this.isErasing() && !this.freeDrawingBrush.inverted) {
|
|
235
309
|
this.freeDrawingBrush._render();
|
|
236
|
-
return;
|
|
237
310
|
}
|
|
238
|
-
if (this.hasLostContext) {
|
|
239
|
-
this.renderTopLayer(this.contextTop);
|
|
240
|
-
}
|
|
241
|
-
var canvasToDrawOn = this.contextContainer;
|
|
242
|
-
this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender());
|
|
243
|
-
return this;
|
|
244
311
|
}
|
|
245
312
|
});
|
|
246
313
|
|
|
247
|
-
|
|
248
314
|
/**
|
|
249
315
|
* EraserBrush class
|
|
250
316
|
* Supports selective erasing meaning that only erasable objects are affected by the eraser brush.
|
|
251
|
-
*
|
|
252
|
-
* while the entire canvas is rendered on the top ctx.
|
|
253
|
-
* Canvas bakground/overlay image/color are handled as well.
|
|
254
|
-
* When erasing occurs, the path clips the top ctx and reveals the bottom ctx.
|
|
255
|
-
* This achieves the desired effect of seeming to erase only erasable objects.
|
|
256
|
-
* 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.
|
|
257
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.
|
|
258
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.
|
|
327
|
+
*
|
|
328
|
+
* @tutorial {@link http://fabricjs.com/erasing}
|
|
259
329
|
* @class fabric.EraserBrush
|
|
260
330
|
* @extends fabric.PencilBrush
|
|
331
|
+
* @memberof fabric
|
|
261
332
|
*/
|
|
262
333
|
fabric.EraserBrush = fabric.util.createClass(
|
|
263
334
|
fabric.PencilBrush,
|
|
@@ -265,253 +336,156 @@
|
|
|
265
336
|
type: 'eraser',
|
|
266
337
|
|
|
267
338
|
/**
|
|
268
|
-
*
|
|
269
|
-
* Used to prevent a race condition caused by {@link fabric.EraserBrush#onMouseMove} firing before {@link fabric.EraserBrush#onMouseDown} has completed
|
|
270
|
-
*
|
|
271
|
-
* @private
|
|
272
|
-
*/
|
|
273
|
-
_ready: false,
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* @private
|
|
339
|
+
* When set to `true` the brush will create a visual effect of undoing erasing
|
|
277
340
|
*/
|
|
278
|
-
|
|
341
|
+
inverted: false,
|
|
279
342
|
|
|
280
343
|
/**
|
|
281
344
|
* @private
|
|
282
345
|
*/
|
|
283
346
|
_isErasing: false,
|
|
284
347
|
|
|
285
|
-
initialize: function (canvas) {
|
|
286
|
-
this.callSuper('initialize', canvas);
|
|
287
|
-
this._renderBound = this._render.bind(this);
|
|
288
|
-
this.render = this.render.bind(this);
|
|
289
|
-
},
|
|
290
|
-
|
|
291
348
|
/**
|
|
292
|
-
* Used to hide a drawable from the rendering process
|
|
293
|
-
* @param {fabric.Object} object
|
|
294
|
-
*/
|
|
295
|
-
hideObject: function (object) {
|
|
296
|
-
if (object) {
|
|
297
|
-
object._originalOpacity = object.opacity;
|
|
298
|
-
object.set({ opacity: 0 });
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Restores hiding an object
|
|
304
|
-
* {@link fabric.EraserBrush#hideObject}
|
|
305
|
-
* @param {fabric.Object} object
|
|
306
|
-
*/
|
|
307
|
-
restoreObjectVisibility: function (object) {
|
|
308
|
-
if (object && object._originalOpacity) {
|
|
309
|
-
object.set({ opacity: object._originalOpacity });
|
|
310
|
-
object._originalOpacity = undefined;
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Drawing Logic For background drawables: (`backgroundImage`, `backgroundColor`)
|
|
316
|
-
* 1. if erasable = true:
|
|
317
|
-
* 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
|
|
318
|
-
* 2. if erasable = false:
|
|
319
|
-
* we need to draw the drawable only on the bottom ctx so the brush won't affect it
|
|
320
|
-
* @param {'bottom' | 'top' | 'overlay'} layer
|
|
321
|
-
*/
|
|
322
|
-
prepareCanvasBackgroundForLayer: function (layer) {
|
|
323
|
-
if (layer === 'overlay') {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
var canvas = this.canvas;
|
|
327
|
-
var image = canvas.get('backgroundImage');
|
|
328
|
-
var color = canvas.get('backgroundColor');
|
|
329
|
-
var erasablesOnLayer = layer === 'top';
|
|
330
|
-
if (image && image.erasable === !erasablesOnLayer) {
|
|
331
|
-
this.hideObject(image);
|
|
332
|
-
}
|
|
333
|
-
if (color && color.erasable === !erasablesOnLayer) {
|
|
334
|
-
this.hideObject(color);
|
|
335
|
-
}
|
|
336
|
-
},
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Drawing Logic For overlay drawables (`overlayImage`, `overlayColor`)
|
|
340
|
-
* We must draw on top ctx to be on top of visible canvas
|
|
341
|
-
* 1. if erasable = true:
|
|
342
|
-
* we need to draw the drawable on the top ctx as a normal object
|
|
343
|
-
* 2. if erasable = false:
|
|
344
|
-
* we need to draw the drawable on top of the brush,
|
|
345
|
-
* this means we need to repaint for every stroke
|
|
346
349
|
*
|
|
347
|
-
* @param {'bottom' | 'top' | 'overlay'} layer
|
|
348
|
-
* @returns boolean render overlay above brush
|
|
349
|
-
*/
|
|
350
|
-
prepareCanvasOverlayForLayer: function (layer) {
|
|
351
|
-
var canvas = this.canvas;
|
|
352
|
-
var image = canvas.get('overlayImage');
|
|
353
|
-
var color = canvas.get('overlayColor');
|
|
354
|
-
if (layer === 'bottom') {
|
|
355
|
-
this.hideObject(image);
|
|
356
|
-
this.hideObject(color);
|
|
357
|
-
return false;
|
|
358
|
-
};
|
|
359
|
-
var erasablesOnLayer = layer === 'top';
|
|
360
|
-
var renderOverlayOnTop = (image && !image.erasable) || (color && !color.erasable);
|
|
361
|
-
if (image && image.erasable === !erasablesOnLayer) {
|
|
362
|
-
this.hideObject(image);
|
|
363
|
-
}
|
|
364
|
-
if (color && color.erasable === !erasablesOnLayer) {
|
|
365
|
-
this.hideObject(color);
|
|
366
|
-
}
|
|
367
|
-
return renderOverlayOnTop;
|
|
368
|
-
},
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
350
|
* @private
|
|
351
|
+
* @param {fabric.Object} object
|
|
352
|
+
* @returns boolean
|
|
372
353
|
*/
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this.restoreObjectVisibility(canvas.get('backgroundImage'));
|
|
376
|
-
this.restoreObjectVisibility(canvas.get('backgroundColor'));
|
|
377
|
-
this.restoreObjectVisibility(canvas.get('overlayImage'));
|
|
378
|
-
this.restoreObjectVisibility(canvas.get('overlayColor'));
|
|
354
|
+
_isErasable: function (object) {
|
|
355
|
+
return object.erasable !== false;
|
|
379
356
|
},
|
|
380
357
|
|
|
381
358
|
/**
|
|
382
359
|
* @private
|
|
383
|
-
* This is designed to support erasing a
|
|
360
|
+
* This is designed to support erasing a collection with both erasable and non-erasable objects.
|
|
384
361
|
* Iterates over collections to allow nested selective erasing.
|
|
385
|
-
*
|
|
386
|
-
*
|
|
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.
|
|
387
366
|
*
|
|
388
367
|
* @param {fabric.Collection} collection
|
|
368
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
369
|
+
* @param {{ visibility: fabric.Object[], eraser: fabric.Object[], collection: fabric.Object[] }} restorationContext
|
|
389
370
|
*/
|
|
390
|
-
|
|
391
|
-
var _this = this;
|
|
371
|
+
_prepareCollectionTraversal: function (collection, ctx, restorationContext) {
|
|
392
372
|
collection.forEachObject(function (obj) {
|
|
393
|
-
if (obj.forEachObject) {
|
|
394
|
-
|
|
373
|
+
if (obj.forEachObject && obj.erasable === 'deep') {
|
|
374
|
+
// traverse
|
|
375
|
+
this._prepareCollectionTraversal(obj, ctx, restorationContext);
|
|
395
376
|
}
|
|
396
|
-
else {
|
|
397
|
-
|
|
398
|
-
|
|
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);
|
|
383
|
+
}
|
|
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);
|
|
399
398
|
}
|
|
400
399
|
}
|
|
401
|
-
});
|
|
400
|
+
}, this);
|
|
402
401
|
},
|
|
403
402
|
|
|
404
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
|
|
405
407
|
* @private
|
|
406
|
-
* Used by {@link fabric.EraserBrush#prepareCanvasObjectsForLayer}
|
|
407
|
-
* to reverse the action of {@link fabric.EraserBrush#prepareCollectionTraversal}
|
|
408
|
-
*
|
|
409
|
-
* @param {fabric.Collection} collection
|
|
410
408
|
*/
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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;
|
|
420
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
|
+
}
|
|
421
459
|
},
|
|
422
460
|
|
|
423
461
|
/**
|
|
462
|
+
* Sets brush styles
|
|
424
463
|
* @private
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* @param {'bottom' | 'top' | 'overlay'} layer
|
|
428
|
-
*/
|
|
429
|
-
prepareCanvasObjectsForLayer: function (layer) {
|
|
430
|
-
if (layer !== 'bottom') { return; }
|
|
431
|
-
this.prepareCollectionTraversal(this.canvas);
|
|
432
|
-
},
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* @private
|
|
436
|
-
* @param {'bottom' | 'top' | 'overlay'} layer
|
|
437
|
-
*/
|
|
438
|
-
restoreCanvasObjectsFromLayer: function (layer) {
|
|
439
|
-
if (layer !== 'bottom') { return; }
|
|
440
|
-
this.restoreCollectionTraversal(this.canvas);
|
|
441
|
-
},
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* @private
|
|
445
|
-
* @param {'bottom' | 'top' | 'overlay'} layer
|
|
446
|
-
* @returns boolean render overlay above brush
|
|
447
|
-
*/
|
|
448
|
-
prepareCanvasForLayer: function (layer) {
|
|
449
|
-
this.prepareCanvasBackgroundForLayer(layer);
|
|
450
|
-
this.prepareCanvasObjectsForLayer(layer);
|
|
451
|
-
return this.prepareCanvasOverlayForLayer(layer);
|
|
452
|
-
},
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* @private
|
|
456
|
-
* @param {'bottom' | 'top' | 'overlay'} layer
|
|
457
|
-
*/
|
|
458
|
-
restoreCanvasFromLayer: function (layer) {
|
|
459
|
-
this.restoreCanvasDrawables();
|
|
460
|
-
this.restoreCanvasObjectsFromLayer(layer);
|
|
461
|
-
},
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Render all non-erasable objects on bottom layer with the exception of overlays to avoid being clipped by the brush.
|
|
465
|
-
* Groups are rendered for nested selective erasing, non-erasable objects are visible while erasable objects are not.
|
|
466
|
-
*/
|
|
467
|
-
renderBottomLayer: function () {
|
|
468
|
-
var canvas = this.canvas;
|
|
469
|
-
this.prepareCanvasForLayer('bottom');
|
|
470
|
-
canvas.renderCanvas(
|
|
471
|
-
canvas.getContext(),
|
|
472
|
-
canvas.getObjects().filter(function (obj) {
|
|
473
|
-
return !obj.erasable || obj.isType('group');
|
|
474
|
-
})
|
|
475
|
-
);
|
|
476
|
-
this.restoreCanvasFromLayer('bottom');
|
|
477
|
-
},
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* 1. Render all objects on top layer, erasable and non-erasable
|
|
481
|
-
* This is important for cases such as overlapping objects, the background object erasable and the foreground object not erasable.
|
|
482
|
-
* 2. Render the brush
|
|
483
|
-
*/
|
|
484
|
-
renderTopLayer: function () {
|
|
485
|
-
var canvas = this.canvas;
|
|
486
|
-
this._drawOverlayOnTop = this.prepareCanvasForLayer('top');
|
|
487
|
-
canvas.renderCanvas(
|
|
488
|
-
canvas.contextTop,
|
|
489
|
-
canvas.getObjects()
|
|
490
|
-
);
|
|
491
|
-
this.callSuper('_render');
|
|
492
|
-
this.restoreCanvasFromLayer('top');
|
|
493
|
-
},
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Render all non-erasable overlays on top of the brush so that they won't get erased
|
|
464
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
497
465
|
*/
|
|
498
|
-
|
|
499
|
-
this.
|
|
500
|
-
|
|
501
|
-
var ctx = canvas.contextTop;
|
|
502
|
-
this._saveAndTransform(ctx);
|
|
503
|
-
canvas._renderOverlay(ctx);
|
|
504
|
-
ctx.restore();
|
|
505
|
-
this.restoreCanvasFromLayer('overlay');
|
|
466
|
+
_setBrushStyles: function (ctx) {
|
|
467
|
+
this.callSuper('_setBrushStyles', ctx);
|
|
468
|
+
ctx.strokeStyle = 'black';
|
|
506
469
|
},
|
|
507
470
|
|
|
508
471
|
/**
|
|
509
|
-
*
|
|
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
|
|
510
483
|
* @param {CanvasRenderingContext2D} ctx
|
|
511
484
|
*/
|
|
512
485
|
_saveAndTransform: function (ctx) {
|
|
513
486
|
this.callSuper('_saveAndTransform', ctx);
|
|
514
|
-
ctx
|
|
487
|
+
this._setBrushStyles(ctx);
|
|
488
|
+
ctx.globalCompositeOperation = ctx === this.canvas.getContext() ? 'destination-out' : 'source-over';
|
|
515
489
|
},
|
|
516
490
|
|
|
517
491
|
/**
|
|
@@ -519,7 +493,7 @@
|
|
|
519
493
|
* @returns
|
|
520
494
|
*/
|
|
521
495
|
needsFullRender: function () {
|
|
522
|
-
return
|
|
496
|
+
return true;
|
|
523
497
|
},
|
|
524
498
|
|
|
525
499
|
/**
|
|
@@ -537,85 +511,144 @@
|
|
|
537
511
|
// this allows to draw dots (when movement never occurs)
|
|
538
512
|
this._captureDrawingPath(pointer);
|
|
539
513
|
|
|
514
|
+
// prepare for erasing
|
|
515
|
+
this.preparePattern();
|
|
540
516
|
this._isErasing = true;
|
|
541
517
|
this.canvas.fire('erasing:start');
|
|
542
|
-
this._ready = true;
|
|
543
518
|
this._render();
|
|
544
519
|
},
|
|
545
520
|
|
|
546
521
|
/**
|
|
547
|
-
* Rendering
|
|
548
|
-
* 1.
|
|
549
|
-
* 2.
|
|
550
|
-
* 3. Draw eraser {@link fabric.PencilBrush#_render} at {@link fabric.EraserBrush#renderTopLayer}
|
|
551
|
-
* 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
|
|
552
525
|
*
|
|
553
|
-
* @param {fabric.Canvas} canvas
|
|
554
526
|
*/
|
|
555
527
|
_render: function () {
|
|
556
|
-
|
|
557
|
-
|
|
528
|
+
var ctx;
|
|
529
|
+
if (!this.inverted) {
|
|
530
|
+
// clip canvas
|
|
531
|
+
ctx = this.canvas.getContext();
|
|
532
|
+
this.callSuper('_render', ctx);
|
|
558
533
|
}
|
|
559
|
-
|
|
560
|
-
this.
|
|
561
|
-
this.
|
|
562
|
-
this.
|
|
563
|
-
|
|
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();
|
|
564
544
|
},
|
|
565
545
|
|
|
566
546
|
/**
|
|
567
|
-
*
|
|
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
|
|
568
553
|
*/
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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;
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Utility to apply a clip path to a path.
|
|
563
|
+
* Used to preserve clipping on eraser paths in nested objects.
|
|
564
|
+
* Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects.
|
|
565
|
+
* @param {fabric.Path} path The eraser path in canvas coordinate plane
|
|
566
|
+
* @param {fabric.Object} clipPath The clipPath to apply to the path
|
|
567
|
+
* @param {number[]} clipPathContainerTransformMatrix The transform matrix of the object that the clip path belongs to
|
|
568
|
+
* @returns {fabric.Path} path with clip path
|
|
569
|
+
*/
|
|
570
|
+
applyClipPathToPath: function (path, clipPath, clipPathContainerTransformMatrix) {
|
|
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;
|
|
582
|
+
fabric.util.applyTransformToObject(
|
|
583
|
+
clipPath,
|
|
584
|
+
fabric.util.multiplyTransformMatrices(
|
|
585
|
+
transform,
|
|
586
|
+
clipPathTransform
|
|
587
|
+
)
|
|
588
|
+
);
|
|
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;
|
|
594
|
+
return path;
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Utility to apply a clip path to a path.
|
|
599
|
+
* Used to preserve clipping on eraser paths in nested objects.
|
|
600
|
+
* Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects.
|
|
601
|
+
* @param {fabric.Path} path The eraser path
|
|
602
|
+
* @param {fabric.Object} object The clipPath to apply to path belongs to object
|
|
603
|
+
* @param {Function} callback Callback to be invoked with the cloned path after applying the clip path
|
|
604
|
+
*/
|
|
605
|
+
clonePathWithClipPath: function (path, object, callback) {
|
|
606
|
+
var objTransform = object.calcTransformMatrix();
|
|
607
|
+
var clipPath = object.clipPath;
|
|
608
|
+
var _this = this;
|
|
609
|
+
path.clone(function (_path) {
|
|
610
|
+
clipPath.clone(function (_clipPath) {
|
|
611
|
+
callback(_this.applyClipPathToPath(_path, _clipPath, objTransform));
|
|
612
|
+
}, ['absolutePositioned', 'inverted']);
|
|
613
|
+
});
|
|
580
614
|
},
|
|
581
615
|
|
|
582
616
|
/**
|
|
583
|
-
* Adds path to
|
|
617
|
+
* Adds path to object's eraser, walks down object's descendants if necessary
|
|
584
618
|
*
|
|
619
|
+
* @fires erasing:end on object
|
|
585
620
|
* @param {fabric.Object} obj
|
|
586
621
|
* @param {fabric.Path} path
|
|
587
622
|
*/
|
|
588
623
|
_addPathToObjectEraser: function (obj, path) {
|
|
589
|
-
var clipObject;
|
|
590
624
|
var _this = this;
|
|
591
625
|
// object is collection, i.e group
|
|
592
|
-
if (obj.forEachObject) {
|
|
593
|
-
obj.
|
|
594
|
-
|
|
595
|
-
_this._addPathToObjectEraser(_obj, path);
|
|
596
|
-
}
|
|
626
|
+
if (obj.forEachObject && obj.erasable === 'deep') {
|
|
627
|
+
var targets = obj._objects.filter(function (_obj) {
|
|
628
|
+
return _obj.erasable;
|
|
597
629
|
});
|
|
630
|
+
if (targets.length > 0 && obj.clipPath) {
|
|
631
|
+
this.clonePathWithClipPath(path, obj, function (_path) {
|
|
632
|
+
targets.forEach(function (_obj) {
|
|
633
|
+
_this._addPathToObjectEraser(_obj, _path);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
else if (targets.length > 0) {
|
|
638
|
+
targets.forEach(function (_obj) {
|
|
639
|
+
_this._addPathToObjectEraser(_obj, path);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
598
642
|
return;
|
|
599
643
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
clipPath: obj.clipPath,
|
|
606
|
-
originX: 'center',
|
|
607
|
-
originY: 'center'
|
|
608
|
-
});
|
|
609
|
-
clipObject = new fabric.Group([rect], {
|
|
610
|
-
eraser: true
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
clipObject = obj.clipPath;
|
|
644
|
+
// prepare eraser
|
|
645
|
+
var eraser = obj.eraser;
|
|
646
|
+
if (!eraser) {
|
|
647
|
+
eraser = new fabric.Eraser();
|
|
648
|
+
obj.eraser = eraser;
|
|
615
649
|
}
|
|
616
|
-
|
|
650
|
+
// clone and add path
|
|
617
651
|
path.clone(function (path) {
|
|
618
|
-
path.globalCompositeOperation = 'destination-out';
|
|
619
652
|
// http://fabricjs.com/using-transformations
|
|
620
653
|
var desiredTransform = fabric.util.multiplyTransformMatrices(
|
|
621
654
|
fabric.util.invertTransform(
|
|
@@ -624,11 +657,14 @@
|
|
|
624
657
|
path.calcTransformMatrix()
|
|
625
658
|
);
|
|
626
659
|
fabric.util.applyTransformToObject(path, desiredTransform);
|
|
627
|
-
|
|
628
|
-
obj.set(
|
|
629
|
-
|
|
630
|
-
|
|
660
|
+
eraser.addWithUpdate(path);
|
|
661
|
+
obj.set('dirty', true);
|
|
662
|
+
obj.fire('erasing:end', {
|
|
663
|
+
path: path
|
|
631
664
|
});
|
|
665
|
+
if (obj.group && Array.isArray(_this.__subTargets)) {
|
|
666
|
+
_this.__subTargets.push(obj);
|
|
667
|
+
}
|
|
632
668
|
});
|
|
633
669
|
},
|
|
634
670
|
|
|
@@ -644,9 +680,7 @@
|
|
|
644
680
|
var drawables = {};
|
|
645
681
|
[
|
|
646
682
|
'backgroundImage',
|
|
647
|
-
'backgroundColor',
|
|
648
683
|
'overlayImage',
|
|
649
|
-
'overlayColor',
|
|
650
684
|
].forEach(function (prop) {
|
|
651
685
|
var drawable = canvas[prop];
|
|
652
686
|
if (drawable && drawable.erasable) {
|
|
@@ -674,9 +708,9 @@
|
|
|
674
708
|
this._isErasing = false;
|
|
675
709
|
|
|
676
710
|
var pathData = this._points && this._points.length > 1 ?
|
|
677
|
-
this.convertPointsToSVGPath(this._points)
|
|
678
|
-
|
|
679
|
-
if (pathData
|
|
711
|
+
this.convertPointsToSVGPath(this._points) :
|
|
712
|
+
null;
|
|
713
|
+
if (!pathData || this._isEmptySVGPath(pathData)) {
|
|
680
714
|
canvas.fire('erasing:end');
|
|
681
715
|
// do not create 0 width/height paths, as they are
|
|
682
716
|
// rendered inconsistently across browsers
|
|
@@ -687,23 +721,32 @@
|
|
|
687
721
|
}
|
|
688
722
|
|
|
689
723
|
var path = this.createPath(pathData);
|
|
724
|
+
// needed for `intersectsWithObject`
|
|
725
|
+
path.setCoords();
|
|
726
|
+
// commense event sequence
|
|
690
727
|
canvas.fire('before:path:created', { path: path });
|
|
691
728
|
|
|
692
729
|
// finalize erasing
|
|
693
730
|
var drawables = this.applyEraserToCanvas(path);
|
|
694
731
|
var _this = this;
|
|
732
|
+
this.__subTargets = [];
|
|
695
733
|
var targets = [];
|
|
696
734
|
canvas.forEachObject(function (obj) {
|
|
697
|
-
if (obj.erasable && obj.intersectsWithObject(path)) {
|
|
735
|
+
if (obj.erasable && obj.intersectsWithObject(path, true, true)) {
|
|
698
736
|
_this._addPathToObjectEraser(obj, path);
|
|
699
737
|
targets.push(obj);
|
|
700
738
|
}
|
|
701
739
|
});
|
|
702
|
-
|
|
703
|
-
canvas.fire('erasing:end', {
|
|
740
|
+
// fire erasing:end
|
|
741
|
+
canvas.fire('erasing:end', {
|
|
742
|
+
path: path,
|
|
743
|
+
targets: targets,
|
|
744
|
+
subTargets: this.__subTargets,
|
|
745
|
+
drawables: drawables
|
|
746
|
+
});
|
|
747
|
+
delete this.__subTargets;
|
|
704
748
|
|
|
705
749
|
canvas.requestRenderAll();
|
|
706
|
-
path.setCoords();
|
|
707
750
|
this._resetShadow();
|
|
708
751
|
|
|
709
752
|
// fire event 'path' created
|