UNPKG

aico-image-editor

Version:

Combine multiple image into and create single combined image

763 lines (714 loc) 29 kB
(function () { /** ERASER_START */ /** * add `eraser` to enlivened props */ fabric.Object.ENLIVEN_PROPS.push('eraser'); var __drawClipPath = fabric.Object.prototype._drawClipPath; var _needsItsOwnCache = fabric.Object.prototype.needsItsOwnCache; var _toObject = fabric.Object.prototype.toObject; var _getSvgCommons = fabric.Object.prototype.getSvgCommons; var __createBaseClipPathSVGMarkup = fabric.Object.prototype._createBaseClipPathSVGMarkup; var __createBaseSVGMarkup = fabric.Object.prototype._createBaseSVGMarkup; fabric.Object.prototype.cacheProperties.push('eraser'); fabric.Object.prototype.stateProperties.push('eraser'); /** * @fires erasing:end */ fabric.util.object.extend(fabric.Object.prototype, { /** * Indicates whether this object can be erased by {@link fabric.EraserBrush} * The `deep` option introduces fine grained control over a group's `erasable` property. * When set to `deep` the eraser will erase nested objects if they are erasable, leaving the group and the other objects untouched. * 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. * When set to `false` the eraser will leave all objects including the group untouched. * @tutorial {@link http://fabricjs.com/erasing#erasable_property} * @type boolean | 'deep' * @default true */ erasable: true, /** * @tutorial {@link http://fabricjs.com/erasing#eraser} * @type fabric.Eraser */ eraser: undefined, /** * @override * @returns Boolean */ needsItsOwnCache: function () { return _needsItsOwnCache.call(this) || !!this.eraser; }, /** * draw eraser above clip path * @override * @private * @param {CanvasRenderingContext2D} ctx * @param {fabric.Object} clipPath */ _drawClipPath: function (ctx, clipPath) { __drawClipPath.call(this, ctx, clipPath); if (this.eraser) { // update eraser size to match instance var size = this._getNonTransformedDimensions(); this.eraser.isType('eraser') && this.eraser.set({ width: size.x, height: size.y }); __drawClipPath.call(this, ctx, this.eraser); } }, /** * Returns an object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject: function (propertiesToInclude) { var object = _toObject.call(this, ['erasable'].concat(propertiesToInclude)); if (this.eraser && !this.eraser.excludeFromExport) { object.eraser = this.eraser.toObject(propertiesToInclude); } return object; }, /* _TO_SVG_START_ */ /** * Returns id attribute for svg output * @override * @return {String} */ getSvgCommons: function () { return _getSvgCommons.call(this) + (this.eraser ? 'mask="url(#' + this.eraser.clipPathId + ')" ' : ''); }, /** * create svg markup for eraser * use <mask> to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649 * must be called before object markup creation as it relies on the `clipPathId` property of the mask * @param {Function} [reviver] * @returns */ _createEraserSVGMarkup: function (reviver) { if (this.eraser) { this.eraser.clipPathId = 'MASK_' + fabric.Object.__uid++; return [ '<mask id="', this.eraser.clipPathId, '" >', this.eraser.toSVG(reviver), '</mask>', '\n' ].join(''); } return ''; }, /** * @private */ _createBaseClipPathSVGMarkup: function (objectMarkup, options) { return [ this._createEraserSVGMarkup(options && options.reviver), __createBaseClipPathSVGMarkup.call(this, objectMarkup, options) ].join(''); }, /** * @private */ _createBaseSVGMarkup: function (objectMarkup, options) { return [ this._createEraserSVGMarkup(options && options.reviver), __createBaseSVGMarkup.call(this, objectMarkup, options) ].join(''); } /* _TO_SVG_END_ */ }); var __restoreObjectsState = fabric.Group.prototype._restoreObjectsState; fabric.util.object.extend(fabric.Group.prototype, { /** * @private * @param {fabric.Path} path */ _addEraserPathToObjects: function (path) { this._objects.forEach(function (object) { fabric.EraserBrush.prototype._addPathToObjectEraser.call( fabric.EraserBrush.prototype, object, path ); }); }, /** * Applies the group's eraser to its objects * @tutorial {@link http://fabricjs.com/erasing#erasable_property} */ applyEraserToObjects: function () { var _this = this, eraser = this.eraser; if (eraser) { delete this.eraser; var transform = _this.calcTransformMatrix(); eraser.clone(function (eraser) { var clipPath = _this.clipPath; eraser.getObjects('path') .forEach(function (path) { // first we transform the path from the group's coordinate system to the canvas' var originalTransform = fabric.util.multiplyTransformMatrices( transform, path.calcTransformMatrix() ); fabric.util.applyTransformToObject(path, originalTransform); if (clipPath) { clipPath.clone(function (_clipPath) { var eraserPath = fabric.EraserBrush.prototype.applyClipPathToPath.call( fabric.EraserBrush.prototype, path, _clipPath, transform ); _this._addEraserPathToObjects(eraserPath); }, ['absolutePositioned', 'inverted']); } else { _this._addEraserPathToObjects(path); } }); }); } }, /** * Propagate the group's eraser to its objects, crucial for proper functionality of the eraser within the group and nested objects. * @private */ _restoreObjectsState: function () { this.erasable === true && this.applyEraserToObjects(); return __restoreObjectsState.call(this); } }); /** * An object's Eraser * @private * @class fabric.Eraser * @extends fabric.Group * @memberof fabric */ fabric.Eraser = fabric.util.createClass(fabric.Group, { /** * @readonly * @static */ type: 'eraser', /** * @default */ originX: 'center', /** * @default */ originY: 'center', drawObject: function (ctx) { ctx.save(); ctx.fillStyle = 'black'; ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); ctx.restore(); this.callSuper('drawObject', ctx); }, /** * eraser should retain size * dimensions should not change when paths are added or removed * handled by {@link fabric.Object#_drawClipPath} * @override * @private */ _getBounds: function () { // noop }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * use <mask> to achieve erasing for svg, credit: https://travishorn.com/removing-parts-of-shapes-in-svg-b539a89e5649 * for masking we need to add a white rect before all paths * * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ _toSVG: function (reviver) { var svgString = ['<g ', 'COMMON_PARTS', ' >\n']; var x = -this.width / 2, y = -this.height / 2; var rectSvg = [ '<rect ', 'fill="white" ', 'x="', x, '" y="', y, '" width="', this.width, '" height="', this.height, '" />\n' ].join(''); svgString.push('\t\t', rectSvg); for (var i = 0, len = this._objects.length; i < len; i++) { svgString.push('\t\t', this._objects[i].toSVG(reviver)); } svgString.push('</g>\n'); return svgString; }, /* _TO_SVG_END_ */ }); /** * Returns {@link fabric.Eraser} instance from an object representation * @static * @memberOf fabric.Eraser * @param {Object} object Object to create an Eraser from * @param {Function} [callback] Callback to invoke when an eraser instance is created */ fabric.Eraser.fromObject = function (object, callback) { var objects = object.objects; fabric.util.enlivenObjects(objects, function (enlivenedObjects) { var options = fabric.util.object.clone(object, true); delete options.objects; fabric.util.enlivenObjectEnlivables(object, options, function () { callback && callback(new fabric.Eraser(enlivenedObjects, options, true)); }); }); }; var __renderOverlay = fabric.Canvas.prototype._renderOverlay; /** * @fires erasing:start * @fires erasing:end */ fabric.util.object.extend(fabric.Canvas.prototype, { /** * Used by {@link #renderAll} * @returns boolean */ isErasing: function () { return ( this.isDrawingMode && this.freeDrawingBrush && this.freeDrawingBrush.type === 'eraser' && this.freeDrawingBrush._isErasing ); }, /** * While erasing the brush clips out the erasing path from canvas * so we need to render it on top of canvas every render * @param {CanvasRenderingContext2D} ctx */ _renderOverlay: function (ctx) { __renderOverlay.call(this, ctx); if (this.isErasing() && !this.freeDrawingBrush.inverted) { this.freeDrawingBrush._render(); } } }); /** * EraserBrush class * Supports selective erasing meaning that only erasable objects are affected by the eraser brush. * Supports **inverted** erasing meaning that the brush can "undo" erasing. * * In order to support selective erasing, the brush clips the entire canvas * and then draws all non-erasable objects over the erased path using a pattern brush so to speak (masking). * If brush is **inverted** there is no need to clip canvas. The brush draws all erasable objects without their eraser. * This achieves the desired effect of seeming to erase or unerase only erasable objects. * After erasing is done the created path is added to all intersected objects' `eraser` property. * * In order to update the EraserBrush call `preparePattern`. * It may come in handy when canvas changes during erasing (i.e animations) and you want the eraser to reflect the changes. * * @tutorial {@link http://fabricjs.com/erasing} * @class fabric.EraserBrush * @extends fabric.PencilBrush * @memberof fabric */ fabric.EraserBrush = fabric.util.createClass( fabric.PencilBrush, /** @lends fabric.EraserBrush.prototype */ { type: 'eraser', /** * When set to `true` the brush will create a visual effect of undoing erasing */ inverted: false, /** * @private */ _isErasing: false, /** * * @private * @param {fabric.Object} object * @returns boolean */ _isErasable: function (object) { return object.erasable !== false; }, /** * @private * This is designed to support erasing a collection with both erasable and non-erasable objects. * Iterates over collections to allow nested selective erasing. * Prepares the pattern brush that will draw on the top context to achieve the desired visual effect. * If brush is **NOT** inverted render all non-erasable objects. * If brush is inverted render all erasable objects that have been erased with their clip path inverted. * This will render the erased parts as if they were not erased. * * @param {fabric.Collection} collection * @param {CanvasRenderingContext2D} ctx * @param {{ visibility: fabric.Object[], eraser: fabric.Object[], collection: fabric.Object[] }} restorationContext */ _prepareCollectionTraversal: function (collection, ctx, restorationContext) { collection.forEachObject(function (obj) { if (obj.forEachObject && obj.erasable === 'deep') { // traverse this._prepareCollectionTraversal(obj, ctx, restorationContext); } else if (!this.inverted && obj.erasable && obj.visible) { // render only non-erasable objects obj.visible = false; collection.dirty = true; restorationContext.visibility.push(obj); restorationContext.collection.push(collection); } else if (this.inverted && obj.visible) { // render only erasable objects that were erased if (obj.erasable && obj.eraser) { obj.eraser.inverted = true; obj.dirty = true; collection.dirty = true; restorationContext.eraser.push(obj); restorationContext.collection.push(collection); } else { obj.visible = false; collection.dirty = true; restorationContext.visibility.push(obj); restorationContext.collection.push(collection); } } }, this); }, /** * Prepare the pattern for the erasing brush * This pattern will be drawn on the top context, achieving a visual effect of erasing only erasable objects * @todo decide how overlay color should behave when `inverted === true`, currently draws over it which is undesirable * @private */ preparePattern: function () { if (!this._patternCanvas) { this._patternCanvas = fabric.util.createCanvasElement(); } var canvas = this._patternCanvas; canvas.width = this.canvas.width; canvas.height = this.canvas.height; var patternCtx = canvas.getContext('2d'); if (this.canvas._isRetinaScaling()) { var retinaScaling = this.canvas.getRetinaScaling(); this.canvas.__initRetinaScaling(retinaScaling, canvas, patternCtx); } var backgroundImage = this.canvas.backgroundImage, bgErasable = backgroundImage && this._isErasable(backgroundImage), overlayImage = this.canvas.overlayImage, overlayErasable = overlayImage && this._isErasable(overlayImage); if (!this.inverted && ((backgroundImage && !bgErasable) || !!this.canvas.backgroundColor)) { if (bgErasable) { this.canvas.backgroundImage = undefined; } this.canvas._renderBackground(patternCtx); if (bgErasable) { this.canvas.backgroundImage = backgroundImage; } } else if (this.inverted && (backgroundImage && bgErasable)) { var color = this.canvas.backgroundColor; this.canvas.backgroundColor = undefined; this.canvas._renderBackground(patternCtx); this.canvas.backgroundColor = color; } patternCtx.save(); patternCtx.transform.apply(patternCtx, this.canvas.viewportTransform); var restorationContext = { visibility: [], eraser: [], collection: [] }; this._prepareCollectionTraversal(this.canvas, patternCtx, restorationContext); this.canvas._renderObjects(patternCtx, this.canvas._objects); restorationContext.visibility.forEach(function (obj) { obj.visible = true; }); restorationContext.eraser.forEach(function (obj) { obj.eraser.inverted = false; obj.dirty = true; }); restorationContext.collection.forEach(function (obj) { obj.dirty = true; }); patternCtx.restore(); if (!this.inverted && ((overlayImage && !overlayErasable) || !!this.canvas.overlayColor)) { if (overlayErasable) { this.canvas.overlayImage = undefined; } __renderOverlay.call(this.canvas, patternCtx); if (overlayErasable) { this.canvas.overlayImage = overlayImage; } } else if (this.inverted && (overlayImage && overlayErasable)) { var color = this.canvas.overlayColor; this.canvas.overlayColor = undefined; __renderOverlay.call(this.canvas, patternCtx); this.canvas.overlayColor = color; } }, /** * Sets brush styles * @private * @param {CanvasRenderingContext2D} ctx */ _setBrushStyles: function (ctx) { this.callSuper('_setBrushStyles', ctx); ctx.strokeStyle = 'black'; }, /** * **Customiztion** * * 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): * @example * ``` * if(ctx === this.canvas.contextTop) { * this.preparePattern(); * } * ``` * * @override fabric.BaseBrush#_saveAndTransform * @param {CanvasRenderingContext2D} ctx */ _saveAndTransform: function (ctx) { this.callSuper('_saveAndTransform', ctx); this._setBrushStyles(ctx); ctx.globalCompositeOperation = ctx === this.canvas.getContext() ? 'destination-out' : 'source-over'; }, /** * We indicate {@link fabric.PencilBrush} to repaint itself if necessary * @returns */ needsFullRender: function () { return true; }, /** * * @param {fabric.Point} pointer * @param {fabric.IEvent} options * @returns */ onMouseDown: function (pointer, options) { if (!this.canvas._isMainEvent(options.e)) { return; } this._prepareForDrawing(pointer); // capture coordinates immediately // this allows to draw dots (when movement never occurs) this._captureDrawingPath(pointer); // prepare for erasing this.preparePattern(); this._isErasing = true; this.canvas.fire('erasing:start'); this._render(); }, /** * Rendering Logic: * 1. Use brush to clip canvas by rendering it on top of canvas (unnecessary if `inverted === true`) * 2. Render brush with canvas pattern on top context * */ _render: function () { var ctx; if (!this.inverted) { // clip canvas ctx = this.canvas.getContext(); this.callSuper('_render', ctx); } // render brush and mask it with image of non erasables ctx = this.canvas.contextTop; this.canvas.clearContext(ctx); this.callSuper('_render', ctx); ctx.save(); var t = this.canvas.getRetinaScaling(), s = 1 / t; ctx.scale(s, s); ctx.globalCompositeOperation = 'source-in'; ctx.drawImage(this._patternCanvas, 0, 0); ctx.restore(); }, /** * Creates fabric.Path object * @override * @private * @param {(string|number)[][]} pathData Path data * @return {fabric.Path} Path to add on canvas * @returns */ createPath: function (pathData) { var path = this.callSuper('createPath', pathData); path.globalCompositeOperation = this.inverted ? 'source-over' : 'destination-out'; path.stroke = this.inverted ? 'white' : 'black'; return path; }, /** * Utility to apply a clip path to a path. * Used to preserve clipping on eraser paths in nested objects. * Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects. * @param {fabric.Path} path The eraser path in canvas coordinate plane * @param {fabric.Object} clipPath The clipPath to apply to the path * @param {number[]} clipPathContainerTransformMatrix The transform matrix of the object that the clip path belongs to * @returns {fabric.Path} path with clip path */ applyClipPathToPath: function (path, clipPath, clipPathContainerTransformMatrix) { var pathInvTransform = fabric.util.invertTransform(path.calcTransformMatrix()), clipPathTransform = clipPath.calcTransformMatrix(), transform = clipPath.absolutePositioned ? pathInvTransform : fabric.util.multiplyTransformMatrices( pathInvTransform, clipPathContainerTransformMatrix ); // when passing down a clip path it becomes relative to the parent // so we transform it acoordingly and set `absolutePositioned` to false clipPath.absolutePositioned = false; fabric.util.applyTransformToObject( clipPath, fabric.util.multiplyTransformMatrices( transform, clipPathTransform ) ); // We need to clip `path` with both `clipPath` and it's own clip path if existing (`path.clipPath`) // so in turn `path` erases an object only where it overlaps with all it's clip paths, regardless of how many there are. // this is done because both clip paths may have nested clip paths of their own (this method walks down a collection => this may reccur), // so we can't assign one to the other's clip path property. path.clipPath = path.clipPath ? fabric.util.mergeClipPaths(clipPath, path.clipPath) : clipPath; return path; }, /** * Utility to apply a clip path to a path. * Used to preserve clipping on eraser paths in nested objects. * Called when a group has a clip path that should be applied to the path before applying erasing on the group's objects. * @param {fabric.Path} path The eraser path * @param {fabric.Object} object The clipPath to apply to path belongs to object * @param {Function} callback Callback to be invoked with the cloned path after applying the clip path */ clonePathWithClipPath: function (path, object, callback) { var objTransform = object.calcTransformMatrix(); var clipPath = object.clipPath; var _this = this; path.clone(function (_path) { clipPath.clone(function (_clipPath) { callback(_this.applyClipPathToPath(_path, _clipPath, objTransform)); }, ['absolutePositioned', 'inverted']); }); }, /** * Adds path to object's eraser, walks down object's descendants if necessary * * @fires erasing:end on object * @param {fabric.Object} obj * @param {fabric.Path} path */ _addPathToObjectEraser: function (obj, path) { var _this = this; // object is collection, i.e group if (obj.forEachObject && obj.erasable === 'deep') { var targets = obj._objects.filter(function (_obj) { return _obj.erasable; }); if (targets.length > 0 && obj.clipPath) { this.clonePathWithClipPath(path, obj, function (_path) { targets.forEach(function (_obj) { _this._addPathToObjectEraser(_obj, _path); }); }); } else if (targets.length > 0) { targets.forEach(function (_obj) { _this._addPathToObjectEraser(_obj, path); }); } return; } // prepare eraser var eraser = obj.eraser; if (!eraser) { eraser = new fabric.Eraser(); obj.eraser = eraser; } // clone and add path path.clone(function (path) { // http://fabricjs.com/using-transformations var desiredTransform = fabric.util.multiplyTransformMatrices( fabric.util.invertTransform( obj.calcTransformMatrix() ), path.calcTransformMatrix() ); fabric.util.applyTransformToObject(path, desiredTransform); eraser.addWithUpdate(path); obj.set('dirty', true); obj.fire('erasing:end', { path: path }); if (obj.group && Array.isArray(_this.__subTargets)) { _this.__subTargets.push(obj); } }); }, /** * Add the eraser path to canvas drawables' clip paths * * @param {fabric.Canvas} source * @param {fabric.Canvas} path * @returns {Object} canvas drawables that were erased by the path */ applyEraserToCanvas: function (path) { var canvas = this.canvas; var drawables = {}; [ 'backgroundImage', 'overlayImage', ].forEach(function (prop) { var drawable = canvas[prop]; if (drawable && drawable.erasable) { this._addPathToObjectEraser(drawable, path); drawables[prop] = drawable; } }, this); return drawables; }, /** * On mouseup after drawing the path on contextTop canvas * we use the points captured to create an new fabric path object * and add it to every intersected erasable object. */ _finalizeAndAddPath: function () { var ctx = this.canvas.contextTop, canvas = this.canvas; ctx.closePath(); if (this.decimate) { this._points = this.decimatePoints(this._points, this.decimate); } // clear canvas.clearContext(canvas.contextTop); this._isErasing = false; var pathData = this._points && this._points.length > 1 ? this.convertPointsToSVGPath(this._points) : null; if (!pathData || this._isEmptySVGPath(pathData)) { canvas.fire('erasing:end'); // do not create 0 width/height paths, as they are // rendered inconsistently across browsers // Firefox 4, for example, renders a dot, // whereas Chrome 10 renders nothing canvas.requestRenderAll(); return; } var path = this.createPath(pathData); // needed for `intersectsWithObject` path.setCoords(); // commense event sequence canvas.fire('before:path:created', { path: path }); // finalize erasing var drawables = this.applyEraserToCanvas(path); var _this = this; this.__subTargets = []; var targets = []; canvas.forEachObject(function (obj) { if (obj.erasable && obj.intersectsWithObject(path, true, true)) { _this._addPathToObjectEraser(obj, path); targets.push(obj); } }); // fire erasing:end canvas.fire('erasing:end', { path: path, targets: targets, subTargets: this.__subTargets, drawables: drawables }); delete this.__subTargets; canvas.requestRenderAll(); this._resetShadow(); // fire event 'path' created canvas.fire('path:created', { path: path }); } } ); /** ERASER_END */ })();