From 1a191a74bf0e45e095223e3501798c927e890fb3 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 3 Apr 2022 23:32:55 +0300 Subject: [PATCH 01/77] apply 7744 patch (no binaries) --- src/canvas.class.js | 2 +- src/mixins/canvas_grouping.mixin.js | 4 +- src/mixins/collection.mixin.js | 90 ++- src/shapes/active_selection.class.js | 72 +- src/shapes/group.class.js | 948 ++++++++++++++++++--------- src/shapes/object.class.js | 7 +- src/static_canvas.class.js | 41 +- src/util/misc.js | 23 +- test/lib/visualTestLoop.js | 6 +- test/unit/activeselection.js | 50 +- test/unit/canvas.js | 23 +- test/unit/canvas_static.js | 76 ++- test/unit/collection.js | 161 ++--- test/unit/group.js | 168 +++-- test/unit/object.js | 2 +- test/visual/group_layout.js | 338 ++++++++++ test/visual/svg_import.js | 2 +- 17 files changed, 1338 insertions(+), 675 deletions(-) create mode 100644 test/visual/group_layout.js diff --git a/src/canvas.class.js b/src/canvas.class.js index 0f5566d082f..e95b2bbcfa2 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -873,7 +873,7 @@ this._normalizePointer(objToCheck.group, pointer) : pointer; if (this._checkTarget(pointerToUse, objToCheck, pointer)) { target = objects[i]; - if (target.subTargetCheck && target instanceof fabric.Group) { + if (target.subTargetCheck && Array.isArray(target._objects)) { subTarget = this._searchPossibleTargets(target._objects, pointer); subTarget && this.targets.push(subTarget); } diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js index b3775557277..b59661a38ab 100644 --- a/src/mixins/canvas_grouping.mixin.js +++ b/src/mixins/canvas_grouping.mixin.js @@ -51,7 +51,7 @@ var activeSelection = this._activeObject, currentActiveObjects = activeSelection._objects.slice(0); if (activeSelection.contains(target)) { - activeSelection.removeWithUpdate(target); + activeSelection.remove(target); this._hoveredTarget = target; this._hoveredTargets = this.targets.concat(); if (activeSelection.size() === 1) { @@ -60,7 +60,7 @@ } } else { - activeSelection.addWithUpdate(target); + activeSelection.add(target); this._hoveredTarget = activeSelection; this._hoveredTargets = this.targets.concat(); } diff --git a/src/mixins/collection.mixin.js b/src/mixins/collection.mixin.js index 49af03f1960..9966d8986c2 100644 --- a/src/mixins/collection.mixin.js +++ b/src/mixins/collection.mixin.js @@ -3,79 +3,69 @@ */ fabric.Collection = { + /** + * @type {fabric.Object[]} + */ _objects: [], /** * Adds objects to collection, Canvas or Group, then renders canvas * (if `renderOnAddRemove` is not `false`). - * in case of Group no changes to bounding box are made. * Objects should be instances of (or inherit from) fabric.Object - * Use of this function is highly discouraged for groups. - * you can add a bunch of objects with the add method but then you NEED - * to run a addWithUpdate call for the Group class or position/bbox will be wrong. - * @param {...fabric.Object} object Zero or more fabric instances - * @return {Self} thisArg - * @chainable + * @private + * @param {fabric.Object[]} objects to add + * @param {(object:fabric.Object) => any} [callback] + * @returns {number} new array length */ - add: function () { - this._objects.push.apply(this._objects, arguments); - if (this._onObjectAdded) { - for (var i = 0, length = arguments.length; i < length; i++) { - this._onObjectAdded(arguments[i]); + add: function (objects, callback) { + var size = this._objects.push.apply(this._objects, objects); + if (callback) { + for (var i = 0, length = objects.length; i < length; i++) { + callback.call(this, objects[i]); } } - this.renderOnAddRemove && this.requestRenderAll(); - return this; + return size; }, /** * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) * An object should be an instance of (or inherit from) fabric.Object - * Use of this function is highly discouraged for groups. - * you can add a bunch of objects with the insertAt method but then you NEED - * to run a addWithUpdate call for the Group class or position/bbox will be wrong. - * @param {Object} object Object to insert + * @private + * @param {fabric.Object|fabric.Object[]} objects Object(s) to insert * @param {Number} index Index to insert object at - * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs - * @return {Self} thisArg - * @chainable + * @param {(object:fabric.Object) => any} [callback] */ - insertAt: function (object, index, nonSplicing) { - var objects = this._objects; - if (nonSplicing) { - objects[index] = object; - } - else { - objects.splice(index, 0, object); + insertAt: function (objects, index, callback) { + var args = [index, 0].concat(objects); + this._objects.splice.apply(this._objects, args); + if (callback) { + for (var i = 2, length = args.length; i < length; i++) { + callback.call(this, args[i]); + } } - this._onObjectAdded && this._onObjectAdded(object); - this.renderOnAddRemove && this.requestRenderAll(); - return this; }, /** * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) - * @param {...fabric.Object} object Zero or more fabric instances - * @return {Self} thisArg - * @chainable + * @private + * @param {fabric.Object[]} objectsToRemove objects to remove + * @param {(object:fabric.Object) => any} [callback] function to call for each object removed + * @returns {boolean} true if objects were removed */ - remove: function() { + remove: function(objectsToRemove, callback) { var objects = this._objects, index, somethingRemoved = false; - for (var i = 0, length = arguments.length; i < length; i++) { - index = objects.indexOf(arguments[i]); - + for (var i = 0, length = objectsToRemove.length; i < length; i++) { + index = objects.indexOf(objectsToRemove[i]); // only call onObjectRemoved if an object was actually removed if (index !== -1) { somethingRemoved = true; objects.splice(index, 1); - this._onObjectRemoved && this._onObjectRemoved(arguments[i]); + callback && callback.call(this, objectsToRemove[i]); } } - - this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); - return this; + return somethingRemoved; }, /** @@ -100,17 +90,16 @@ fabric.Collection = { /** * Returns an array of children objects of this instance - * Type parameter introduced in 1.3.10 - * since 2.3.5 this method return always a COPY of the array; - * @param {String} [type] When specified, only objects of this type are returned + * @param {...String} [types] When specified, only objects of these types are returned * @return {Array} */ - getObjects: function(type) { - if (typeof type === 'undefined') { + getObjects: function() { + if (arguments.length === 0) { return this._objects.concat(); } - return this._objects.filter(function(o) { - return o.type === type; + var types = Array.from(arguments); + return this._objects.filter(function (o) { + return types.indexOf(o.type) > -1; }); }, @@ -140,7 +129,8 @@ fabric.Collection = { }, /** - * Returns true if collection contains an object + * Returns true if collection contains an object.\ + * **Prefer using {@link `fabric.Object#isDescendantOf`} for performance reasons** * @param {Object} object Object to check against * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` * @return {Boolean} `true` if collection contains an object diff --git a/src/shapes/active_selection.class.js b/src/shapes/active_selection.class.js index 36b17c6b280..a2b648752d0 100644 --- a/src/shapes/active_selection.class.js +++ b/src/shapes/active_selection.class.js @@ -24,58 +24,42 @@ */ type: 'activeSelection', + /** + * @override + */ + layout: 'fit-content', + + /** + * @override + */ + subTargetCheck: false, + + /** + * @override + */ + interactive: false, + /** * Constructor - * @param {Object} objects ActiveSelection objects + * + * @param {fabric.Object[]} [objects] instance objects * @param {Object} [options] Options object - * @return {Object} thisArg + * @param {boolean} [objectsRelativeToGroup] true if objects exist in group coordinate plane + * @return {fabric.ActiveSelection} thisArg */ - initialize: function(objects, options) { - options = options || {}; - this._objects = objects || []; - for (var i = this._objects.length; i--; ) { - this._objects[i].group = this; - } - - if (options.originX) { - this.originX = options.originX; - } - if (options.originY) { - this.originY = options.originY; - } - this._calcBounds(); - this._updateObjectsCoords(); - fabric.Object.prototype.initialize.call(this, options); + initialize: function (objects, options, objectsRelativeToGroup) { + this.callSuper('initialize', objects, options, objectsRelativeToGroup); this.setCoords(); }, /** - * Change te activeSelection to a normal group, - * High level function that automatically adds it to canvas as - * active object. no events fired. - * @since 2.0.0 - * @return {fabric.Group} + * we want objects to retain their canvas ref when exiting instance + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it */ - toGroup: function() { - var objects = this._objects.concat(); - this._objects = []; - var options = fabric.Object.prototype.toObject.call(this); - var newGroup = new fabric.Group([]); - delete options.type; - newGroup.set(options); - objects.forEach(function(object) { - object.canvas.remove(object); - object.group = newGroup; - }); - newGroup._objects = objects; - if (!this.canvas) { - return newGroup; - } - var canvas = this.canvas; - canvas.add(newGroup); - canvas._activeObject = newGroup; - newGroup.setCoords(); - return newGroup; + exitGroup: function (object, removeParentTransform) { + this._exitGroup(object, removeParentTransform); }, /** @@ -84,7 +68,7 @@ * @return {Boolean} [cancel] */ onDeselect: function() { - this.destroy(); + this.removeAll(); return false; }, diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index da0d25e1664..af090dad895 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -1,12 +1,18 @@ -(function(global) { +(function (global) { 'use strict'; - var fabric = global.fabric || (global.fabric = { }), - min = fabric.util.array.min, - max = fabric.util.array.max; + var fabric = global.fabric || (global.fabric = {}), + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, + invertTransform = fabric.util.invertTransform, + transformPoint = fabric.util.transformPoint, + applyTransformToObject = fabric.util.applyTransformToObject, + degreesToRadians = fabric.util.degreesToRadians, + clone = fabric.util.object.clone, + extend = fabric.util.object.extend; if (fabric.Group) { + fabric.warn('fabric.Group is already defined'); return; } @@ -15,280 +21,319 @@ * @class fabric.Group * @extends fabric.Object * @mixes fabric.Collection - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @fires layout once layout completes * @see {@link fabric.Group#initialize} for constructor definition */ fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { /** * Type of an object - * @type String + * @type string * @default */ type: 'group', + /** + * Specifies the **layout strategy** for instance + * Used by `getLayoutStrategyResult` to calculate layout + * `fit-content`, `fit-content-lazy`, `fixed`, `clip-path` are supported out of the box + * @type string + * @default + */ + layout: 'fit-content', + /** * Width of stroke * @type Number - * @default */ strokeWidth: 0, /** - * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets - * @type Boolean + * List of properties to consider when checking if state + * of an object is changed (fabric.Object#hasStateChanged) + * as well as for history (undo/redo) purposes + * @type string[] + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('layout'), + + /** + * Used to optimize performance + * set to `false` if you don't need caontained objects to be target of events * @default + * @type boolean */ subTargetCheck: false, /** - * Groups are container, do not render anything on theyr own, ence no cache properties - * @type Array + * Used to allow targeting of object inside groups. + * set to true if you want to select an object inside a group. + * REQUIRES subTargetCheck set to true * @default + * @type boolean */ - cacheProperties: [], + interactive: false, /** - * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still - * available setting this boolean to true. - * @type Boolean - * @since 2.0.0 - * @default + * Used internally to optimize performance + * Once an object is selected, instance is rendered without the selected object. + * This way instance is cached only once for the entire interaction with the selected object. + * @private */ - useSetOnGroup: false, + _activeObjects: undefined, /** * Constructor - * @param {Object} objects Group objects + * + * @param {fabric.Object[]} [objects] instance objects * @param {Object} [options] Options object - * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. - * @return {Object} thisArg - */ - initialize: function(objects, options, isAlreadyGrouped) { - options = options || {}; - this._objects = []; - // if objects enclosed in a group have been grouped already, - // we cannot change properties of objects. - // Thus we need to set options to group without objects, - isAlreadyGrouped && this.callSuper('initialize', options); + * @param {boolean} [objectsRelativeToGroup] true if objects exist in group coordinate plane + * @return {fabric.Group} thisArg + */ + initialize: function (objects, options, objectsRelativeToGroup) { this._objects = objects || []; - for (var i = this._objects.length; i--; ) { - this._objects[i].group = this; - } - - if (!isAlreadyGrouped) { - var center = options && options.centerPoint; - // we want to set origins before calculating the bounding box. - // so that the topleft can be set with that in mind. - // if specific top and left are passed, are overwritten later - // with the callSuper('initialize', options) - if (options.originX !== undefined) { - this.originX = options.originX; - } - if (options.originY !== undefined) { - this.originY = options.originY; - } - // if coming from svg i do not want to calc bounds. - // i assume width and height are passed along options - center || this._calcBounds(); - this._updateObjectsCoords(center); - delete options.centerPoint; - this.callSuper('initialize', options); + this._activeObjects = []; + this.__objectMonitor = this.__objectMonitor.bind(this); + this.__objectSelectionTracker = this.__objectSelectionMonitor.bind(this, true); + this.__objectSelectionDisposer = this.__objectSelectionMonitor.bind(this, false); + this._firstLayoutDone = false; + this.callSuper('initialize', options); + if (objectsRelativeToGroup) { + this.forEachObject(function (object) { + this.enterGroup(object, false); + }, this); } else { - this._updateObjectsACoords(); + // we need to preserve object's center point in relation to canvas and apply group's transform to it + var inv = invertTransform(this.calcTransformMatrix()); + this.forEachObject(function (object) { + this.enterGroup(object, false); + var center = transformPoint(object.getCenterPoint(), inv); + object.setPositionByOrigin(center, 'center', 'center'); + }, this); } - - this.setCoords(); + this._applyLayoutStrategy({ + type: 'initialization', + options: options, + objectsRelativeToGroup: objectsRelativeToGroup + }); }, /** * @private - */ - _updateObjectsACoords: function() { - var skipControls = true; - for (var i = this._objects.length; i--; ){ - this._objects[i].setCoords(skipControls); + * @param {string} key + * @param {*} value + */ + _set: function (key, value) { + var prev = this[key]; + this.callSuper('_set', key, value); + if (key === 'canvas' && prev !== value) { + this.forEachObject(function (object) { + object._set(key, value); + }); + } + if (key === 'layout' && prev !== value) { + this._applyLayoutStrategy({ type: 'layout_change', layout: value, prevLayout: prev }); } + if (key === 'interactive') { + this.forEachObject(this._watchObject.bind(this, value)); + } + return this; }, /** - * @private - * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + * Add objects + * @param {...fabric.Object} objects */ - _updateObjectsCoords: function(center) { - var center = center || this.getRelativeCenterPoint(); - for (var i = this._objects.length; i--; ){ - this._updateObjectCoords(this._objects[i], center); - } + add: function () { + fabric.Collection.add.call(this, arguments, this._onObjectAdded); + this._onAfterObjectsChange('added', Array.from(arguments)); }, /** - * @private - * @param {Object} object - * @param {fabric.Point} center, current center of group. + * Inserts an object into collection at specified index + * @param {fabric.Object} objects Object to insert + * @param {Number} index Index to insert object at */ - _updateObjectCoords: function(object, center) { - var objectLeft = object.left, - objectTop = object.top, - skipControls = true; + insertAt: function (objects, index) { + fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); + this._onAfterObjectsChange('added', Array.isArray(objects) ? objects : [objects]); + }, - object.set({ - left: objectLeft - center.x, - top: objectTop - center.y - }); - object.group = this; - object.setCoords(skipControls); + /** + * Remove objects + * @param {...fabric.Object} objects + */ + remove: function () { + fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); + this._onAfterObjectsChange('removed', Array.from(arguments)); }, /** - * Returns string represenation of a group - * @return {String} + * Remove all objects + * @returns {fabric.Object[]} removed objects */ - toString: function() { - return '#'; + removeAll: function () { + this._activeObjects = []; + var remove = this._objects.slice(); + this.remove.apply(this, remove); + return remove; }, /** - * Adds an object to a group; Then recalculates group's dimension, position. - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - addWithUpdate: function(object) { - var nested = !!this.group; - this._restoreObjectsState(); - fabric.util.resetObjectTransform(this); - if (object) { - if (nested) { - // if this group is inside another group, we need to pre transform the object - fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); - } - this._objects.push(object); - object.group = this; - object._set('canvas', this.canvas); - } - this._calcBounds(); - this._updateObjectsCoords(); - this.dirty = true; - if (nested) { - this.group.addWithUpdate(); - } - else { - this.setCoords(); - } - return this; + * invalidates layout on object modified + * @private + */ + __objectMonitor: function (opt) { + this._applyLayoutStrategy(extend(clone(opt), { + type: 'object_modified' + })); + this._set('dirty', true); }, /** - * Removes an object from a group; Then recalculates group's dimension, position. - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable + * keeps track of the selected objects + * @private */ - removeWithUpdate: function(object) { - this._restoreObjectsState(); - fabric.util.resetObjectTransform(this); + __objectSelectionMonitor: function (selected, opt) { + var object = opt.target; + if (selected) { + this._activeObjects.push(object); + this._set('dirty', true); + } + else if (this._activeObjects.length > 0) { + var index = this._activeObjects.indexOf(object); + if (index > -1) { + this._activeObjects.splice(index, 1); + this._set('dirty', true); + } + } + }, - this.remove(object); - this._calcBounds(); - this._updateObjectsCoords(); - this.setCoords(); - this.dirty = true; - return this; + /** + * @private + * @param {boolean} watch + * @param {fabric.Object} object + */ + _watchObject: function (watch, object) { + var directive = watch ? 'on' : 'off'; + // make sure we listen only once + watch && this._watchObject(false, object); + // TODO check what changed ischeck + object[directive]('changed', this.__objectMonitor); + object[directive]('modified', this.__objectMonitor); + object[directive]('selected', this.__objectSelectionTracker); + object[directive]('deselected', this.__objectSelectionDisposer); }, /** * @private - */ - _onObjectAdded: function(object) { - this.dirty = true; - object.group = this; + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + */ + enterGroup: function (object, removeParentTransform) { + if (object.group) { + if (object.group === this) { + /* _DEV_MODE_START_ */ + console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); + /* _DEV_MODE_END_ */ + return; + } + object.group.remove(object); + } + // can be this converted to utils? + if (removeParentTransform) { + applyTransformToObject( + object, + multiplyTransformMatrices( + invertTransform(this.calcTransformMatrix()), + object.calcTransformMatrix() + ) + ); + } + object.setCoords(); + object._set('group', this); object._set('canvas', this.canvas); + this._watchObject(true, object); + var activeObject = this.canvas && this.canvas.getActiveObject && this.canvas.getActiveObject(); + // if we are adding the activeObject in a group + if (activeObject && (activeObject === object || (object.contains && object.contains(activeObject)))) { + this._activeObjects.push(object); + } }, /** * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it */ - _onObjectRemoved: function(object) { - this.dirty = true; - delete object.group; + exitGroup: function (object, removeParentTransform) { + this._exitGroup(object, removeParentTransform); + object._set('canvas', undefined); }, /** * @private - */ - _set: function(key, value) { - var i = this._objects.length; - if (this.useSetOnGroup) { - while (i--) { - this._objects[i].setOnGroup(key, value); - } + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it + */ + _exitGroup: function (object, removeParentTransform) { + object._set('group', undefined); + if (!removeParentTransform) { + applyTransformToObject( + object, + multiplyTransformMatrices( + this.calcTransformMatrix(), + object.calcTransformMatrix() + ) + ); + object.setCoords(); } - if (key === 'canvas') { - while (i--) { - this._objects[i]._set(key, value); - } + this._watchObject(false, object); + var index = this._activeObjects.length > 0 ? this._activeObjects.indexOf(object) : -1; + if (index > -1) { + this._activeObjects.splice(index, 1); } - fabric.Object.prototype._set.call(this, key, value); }, /** - * Returns 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 + * @private + * @param {'added'|'removed'} type + * @param {fabric.Object[]} targets */ - toObject: function(propertiesToInclude) { - var _includeDefaultValues = this.includeDefaultValues; - var objsToObject = this._objects - .filter(function (obj) { - return !obj.excludeFromExport; - }) - .map(function (obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = _includeDefaultValues; - var _obj = obj.toObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); - obj.objects = objsToObject; - return obj; + _onAfterObjectsChange: function (type, targets) { + this._applyLayoutStrategy({ + type: type, + targets: targets + }); + this._set('dirty', true); }, /** - * Returns object representation of an instance, in dataless mode. - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance + * @private + * @param {fabric.Object} object */ - toDatalessObject: function(propertiesToInclude) { - var objsToObject, sourcePath = this.sourcePath; - if (sourcePath) { - objsToObject = sourcePath; - } - else { - var _includeDefaultValues = this.includeDefaultValues; - objsToObject = this._objects.map(function(obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = _includeDefaultValues; - var _obj = obj.toDatalessObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - } - var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); - obj.objects = objsToObject; - return obj; + _onObjectAdded: function (object) { + this.enterGroup(object, true); + object.fire('added', { target: this }); }, /** - * Renders instance on a given context - * @param {CanvasRenderingContext2D} ctx context to render instance on + * @private + * @param {fabric.Object} object */ - render: function(ctx) { - this._transformDone = true; - this.callSuper('render', ctx); - this._transformDone = false; + _onRelativeObjectAdded: function (object) { + this.enterGroup(object, false); + object.fire('added', { target: this }); + }, + + /** + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it + */ + _onObjectRemoved: function (object, removeParentTransform) { + this.exitGroup(object, removeParentTransform); + object.fire('removed', { target: this }); }, /** @@ -328,11 +373,11 @@ }, /** - * Check if this group or its parent group are caching, recursively up + * Check if instance or its group are caching, recursively up * @return {Boolean} */ - isOnACache: function() { - return this.ownCaching || (this.group && this.group.isOnACache()); + isOnACache: function () { + return this.ownCaching || (!!this.group && this.group.isOnACache()); }, /** @@ -370,151 +415,448 @@ }, /** - * Restores original state of each of group objects (original state is that which was before group was created). - * if the nested boolean is true, the original state will be restored just for the - * first group and not for all the group chain - * @private - * @param {Boolean} nested tell the function to restore object state up to the parent group and not more - * @return {fabric.Group} thisArg - * @chainable - */ - _restoreObjectsState: function() { - var groupMatrix = this.calcOwnMatrix(); - this._objects.forEach(function(object) { - // instead of using _this = this; - fabric.util.addTransformToObject(object, groupMatrix); - delete object.group; + * @override + * @return {Boolean} + */ + setCoords: function () { + this.callSuper('setCoords'); + (this.subTargetCheck || this.type === 'activeSelection') && this.forEachObject(function (object) { object.setCoords(); }); - return this; }, /** - * Destroys a group (restoring state of its objects) - * @return {fabric.Group} thisArg - * @chainable + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on */ - destroy: function() { - // when group is destroyed objects needs to get a repaint to be eventually - // displayed on canvas. - this._objects.forEach(function(object) { - object.set('dirty', true); - }); - return this._restoreObjectsState(); + render: function (ctx) { + // used to inform objects not to double opacity + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; }, - dispose: function () { - this.callSuper('dispose'); - this.forEachObject(function (object) { - object.dispose && object.dispose(); + /** + * @public + * @param {Partial & { layout?: string }} [context] pass values to use for layout calculations + */ + triggerLayout: function (context) { + if (context && context.layout) { + context.prevLayout = this.layout; + this.layout = context.layout; + } + this._applyLayoutStrategy({ type: 'imperative', context: context }); + }, + + /** + * @private + * @param {fabric.Object} object + * @param {fabric.Point} diff + */ + _adjustObjectPosition: function (object, diff) { + object.set({ + left: object.left + diff.x, + top: object.top + diff.y, }); - this._objects = []; }, /** - * make a group an active selection, remove the group from canvas - * the group has to be on canvas for this to work. - * @return {fabric.ActiveSelection} thisArg - * @chainable + * initial layout logic: + * calculate bbox of objects (if necessary) and translate it according to options recieved from the constructor (left, top, width, height) + * so it is placed in the center of the bbox received from the constructor + * + * @private + * @param {LayoutContext} context */ - toActiveSelection: function() { - if (!this.canvas) { + _applyLayoutStrategy: function (context) { + var isFirstLayout = context.type === 'initialization'; + if (!isFirstLayout && !this._firstLayoutDone) { + // reject layout requests before initialization layout + return; + } + var center = this.getRelativeCenterPoint(); + var result = this.getLayoutStrategyResult(this.layout, this._objects.concat(), context); + if (result) { + // handle positioning + var newCenter = new fabric.Point(result.centerX, result.centerY); + var vector = center.subtract(newCenter).add(new fabric.Point(result.correctionX || 0, result.correctionY || 0)); + var diff = transformPoint(vector, invertTransform(this.calcOwnMatrix()), true); + // set dimensions + this.set({ width: result.width, height: result.height }); + // adjust objects to account for new center + !context.objectsRelativeToGroup && this.forEachObject(function (object) { + this._adjustObjectPosition(object, diff); + }, this); + // clip path as well + !isFirstLayout && this.layout !== 'clip-path' && this.clipPath && !this.clipPath.absolutePositioned + && this._adjustObjectPosition(this.clipPath, diff); + if (!newCenter.eq(center)) { + // set position + this.setPositionByOrigin(newCenter, 'center', 'center'); + this.setCoords(); + } + } + else if (isFirstLayout) { + // fill `result` with initial values for the layout hook + result = { + centerX: center.x, + centerY: center.y, + width: this.width, + height: this.height, + }; + } + else { + // no `result` so we return return; } - var objects = this._objects, canvas = this.canvas; - this._objects = []; - var options = this.toObject(); - delete options.objects; - var activeSelection = new fabric.ActiveSelection([]); - activeSelection.set(options); - activeSelection.type = 'activeSelection'; - canvas.remove(this); - objects.forEach(function(object) { - object.group = activeSelection; - object.dirty = true; - canvas.add(object); + // flag for next layouts + this._firstLayoutDone = true; + // fire layout hook and event (event will fire only for layouts after initialization layout) + this.onLayout(context, result); + this.fire('layout', { + context: context, + result: result, + diff: diff }); - activeSelection.canvas = canvas; - activeSelection._objects = objects; - canvas._activeObject = activeSelection; - activeSelection.setCoords(); - return activeSelection; + // recursive up + if (this.group && this.group._applyLayoutStrategy) { + // append the path recursion to context + if (!context.path) { + context.path = []; + } + context.path.push(this); + // all parents should invalidate their layout + this.group._applyLayoutStrategy(context); + } }, + /** - * Destroys a group (restoring state of its objects) - * @return {fabric.Group} thisArg - * @chainable - */ - ungroupOnCanvas: function() { - return this._restoreObjectsState(); + * Override this method to customize layout. + * If you need to run logic once layout completes use `onLayout` + * @public + * + * @typedef {'initialization'|'object_modified'|'added'|'removed'|'layout_change'|'imperative'} LayoutContextType + * + * @typedef LayoutContext context object with data regarding what triggered the call + * @property {LayoutContextType} type + * @property {fabric.Object[]} [path] array of objects starting from the object that triggered the call to the current one + * + * @typedef LayoutResult positioning and layout data **relative** to instance's parent + * @property {number} centerX new centerX as measured by the containing plane (same as `left` with `originX` set to `center`) + * @property {number} centerY new centerY as measured by the containing plane (same as `top` with `originY` set to `center`) + * @property {number} [correctionX] correctionX to translate objects by, measured as `centerX` + * @property {number} [correctionY] correctionY to translate objects by, measured as `centerY` + * @property {number} width + * @property {number} height + * + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {LayoutContext} context + * @returns {LayoutResult | undefined} + */ + getLayoutStrategyResult: function (layoutDirective, objects, context) { // eslint-disable-line no-unused-vars + // `fit-content-lazy` performance enhancement + // skip if instance had no objects before the `added` event because it may have kept layout after removing all previous objects + if (layoutDirective === 'fit-content-lazy' + && context.type === 'added' && objects.length > context.targets.length) { + // calculate added objects' bbox with existing bbox + var addedObjects = context.targets.concat(this); + return this.prepareBoundingBox(layoutDirective, addedObjects, context); + } + else if (layoutDirective === 'fit-content' || layoutDirective === 'fit-content-lazy' + || (layoutDirective === 'fixed' && context.type === 'initialization')) { + return this.prepareBoundingBox(layoutDirective, objects, context); + } + else if (layoutDirective === 'clip-path' && this.clipPath) { + var clipPath = this.clipPath; + var clipPathSizeAfter = clipPath._getTransformedDimensions(); + if (clipPath.absolutePositioned && (context.type === 'initialization' || context.type === 'layout_change')) { + // we want the center point to exist in group's containing plane + var clipPathCenter = clipPath.getCenterPoint(); + if (this.group) { + // send point from canvas plane to group's containing plane + var inv = invertTransform(this.group.calcTransformMatrix()); + clipPathCenter = transformPoint(clipPathCenter, inv); + } + return { + centerX: clipPathCenter.x, + centerY: clipPathCenter.y, + width: clipPathSizeAfter.x, + height: clipPathSizeAfter.y, + }; + } + else if (!clipPath.absolutePositioned) { + var center; + var clipPathRelativeCenter = clipPath.getRelativeCenterPoint(), + // we want the center point to exist in group's containing plane, so we send it upwards + clipPathCenter = transformPoint(clipPathRelativeCenter, this.calcOwnMatrix(), true); + if (context.type === 'initialization' || context.type === 'layout_change') { + var bbox = this.prepareBoundingBox(layoutDirective, objects, context) || {}; + center = new fabric.Point(bbox.centerX || 0, bbox.centerY || 0); + return { + centerX: center.x + clipPathCenter.x, + centerY: center.y + clipPathCenter.y, + correctionX: bbox.correctionX - clipPathCenter.x, + correctionY: bbox.correctionY - clipPathCenter.y, + width: clipPath.width, + height: clipPath.height, + }; + } + else { + center = this.getRelativeCenterPoint(); + return { + centerX: center.x + clipPathCenter.x, + centerY: center.y + clipPathCenter.y, + width: clipPathSizeAfter.x, + height: clipPathSizeAfter.y, + }; + } + } + } + else if (layoutDirective === 'svg' && context.type === 'initialization') { + var bbox = this.getObjectsBoundingBox(objects, true) || {}; + return Object.assign(bbox, { + correctionX: -bbox.offsetX || 0, + correctionY: -bbox.offsetY || 0, + }); + } }, /** - * Sets coordinates of all objects inside group - * @return {fabric.Group} thisArg - * @chainable + * Override this method to customize layout. + * A wrapper around {@link fabric.Group#getObjectsBoundingBox} + * @public + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {LayoutContext} context + * @returns {LayoutResult | undefined} */ - setObjectsCoords: function() { - var skipControls = true; - this.forEachObject(function(object) { - object.setCoords(skipControls); - }); - return this; + prepareBoundingBox: function (layoutDirective, objects, context) { + if (context.type === 'initialization') { + return this.prepareInitialBoundingBox(layoutDirective, objects, context); + } + else if (context.type === 'imperative' && context.context) { + return Object.assign( + this.getObjectsBoundingBox(objects) || {}, + context.context + ); + } + else { + return this.getObjectsBoundingBox(objects); + } }, /** - * @private + * Calculates center taking into account originX, originY while not being sure that width/height are initialized + * @public + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {LayoutContext} context + * @returns {LayoutResult | undefined} + */ + prepareInitialBoundingBox: function (layoutDirective, objects, context) { + var options = context.options || {}, + hasX = typeof options.left === 'number', + hasY = typeof options.top === 'number', + hasWidth = typeof options.width === 'number', + hasHeight = typeof options.height === 'number'; + + // performance enhancement + // skip layout calculation if bbox is defined + if ((hasX && hasY && hasWidth && hasHeight && context.objectsRelativeToGroup) || objects.length === 0) { + // return nothing to skip layout + return; + } + + var bbox = this.getObjectsBoundingBox(objects) || {}; + var width = hasWidth ? this.width : (bbox.width || 0), + height = hasHeight ? this.height : (bbox.height || 0), + calculatedCenter = new fabric.Point(bbox.centerX || 0, bbox.centerY || 0), + origin = new fabric.Point(this.resolveOriginX(this.originX), this.resolveOriginY(this.originY)), + size = new fabric.Point(width, height), + strokeWidthVector = this._getTransformedDimensions({ width: 0, height: 0 }), + sizeAfter = this._getTransformedDimensions({ + width: width, + height: height, + strokeWidth: 0 + }), + bboxSizeAfter = this._getTransformedDimensions({ + width: bbox.width, + height: bbox.height, + strokeWidth: 0 + }), + rotationCorrection = new fabric.Point(0, 0); + + if (this.angle) { + var rad = degreesToRadians(this.angle), + sin = Math.abs(fabric.util.sin(rad)), + cos = Math.abs(fabric.util.cos(rad)); + sizeAfter.setXY( + sizeAfter.x * cos + sizeAfter.y * sin, + sizeAfter.x * sin + sizeAfter.y * cos + ); + bboxSizeAfter.setXY( + bboxSizeAfter.x * cos + bboxSizeAfter.y * sin, + bboxSizeAfter.x * sin + bboxSizeAfter.y * cos + ); + strokeWidthVector = fabric.util.rotateVector(strokeWidthVector, rad); + // correct center after rotating + var strokeCorrection = strokeWidthVector.multiply(origin.scalarAdd(-0.5).scalarDivide(-2)); + rotationCorrection = sizeAfter.subtract(size).scalarDivide(2).add(strokeCorrection); + calculatedCenter.addEquals(rotationCorrection); + } + // calculate center and correction + var originT = origin.scalarAdd(0.5); + var originCorrection = sizeAfter.multiply(originT); + var centerCorrection = new fabric.Point( + hasWidth ? bboxSizeAfter.x / 2 : originCorrection.x, + hasHeight ? bboxSizeAfter.y / 2 : originCorrection.y + ); + var center = new fabric.Point( + hasX ? this.left - (sizeAfter.x + strokeWidthVector.x) * origin.x : calculatedCenter.x - centerCorrection.x, + hasY ? this.top - (sizeAfter.y + strokeWidthVector.y) * origin.y : calculatedCenter.y - centerCorrection.y + ); + var offsetCorrection = new fabric.Point( + hasX ? + center.x - calculatedCenter.x + bboxSizeAfter.x * (hasWidth ? 0.5 : 0) : + -(hasWidth ? (sizeAfter.x - strokeWidthVector.x) * 0.5 : sizeAfter.x * originT.x), + hasY ? + center.y - calculatedCenter.y + bboxSizeAfter.y * (hasHeight ? 0.5 : 0) : + -(hasHeight ? (sizeAfter.y - strokeWidthVector.y) * 0.5 : sizeAfter.y * originT.y) + ).add(rotationCorrection); + var correction = new fabric.Point( + hasWidth ? -sizeAfter.x / 2 : 0, + hasHeight ? -sizeAfter.y / 2 : 0 + ).add(offsetCorrection); + + return { + centerX: center.x, + centerY: center.y, + correctionX: correction.x, + correctionY: correction.y, + width: size.x, + height: size.y, + }; + }, + + /** + * Calculate the bbox of objects relative to instance's containing plane + * @public + * @param {fabric.Object[]} objects + * @returns {LayoutResult | null} bounding box */ - _calcBounds: function(onlyWidthHeight) { - var aX = [], - aY = [], - o, prop, coords, - props = ['tr', 'br', 'bl', 'tl'], - i = 0, iLen = this._objects.length, - j, jLen = props.length; - - for ( ; i < iLen; ++i) { - o = this._objects[i]; - coords = o.calcACoords(); - for (j = 0; j < jLen; j++) { - prop = props[j]; - aX.push(coords[prop].x); - aY.push(coords[prop].y); - } - o.aCoords = coords; + getObjectsBoundingBox: function (objects, ignoreOffset) { + if (objects.length === 0) { + return null; } + var objCenter, sizeVector, min, max, a, b; + objects.forEach(function (object, i) { + objCenter = object.getRelativeCenterPoint(); + sizeVector = object._getTransformedDimensions().scalarDivideEquals(2); + if (object.angle) { + var rad = degreesToRadians(object.angle), + sin = Math.abs(fabric.util.sin(rad)), + cos = Math.abs(fabric.util.cos(rad)), + rx = sizeVector.x * cos + sizeVector.y * sin, + ry = sizeVector.x * sin + sizeVector.y * cos; + sizeVector = new fabric.Point(rx, ry); + } + a = objCenter.subtract(sizeVector); + b = objCenter.add(sizeVector); + if (i === 0) { + min = new fabric.Point(Math.min(a.x, b.x), Math.min(a.y, b.y)); + max = new fabric.Point(Math.max(a.x, b.x), Math.max(a.y, b.y)); + } + else { + min.setXY(Math.min(min.x, a.x, b.x), Math.min(min.y, a.y, b.y)); + max.setXY(Math.max(max.x, a.x, b.x), Math.max(max.y, a.y, b.y)); + } + }); + + var size = max.subtract(min), + relativeCenter = ignoreOffset ? size.scalarDivide(2) : min.midPointFrom(max), + // we send `relativeCenter` up to group's containing plane + offset = transformPoint(min, this.calcOwnMatrix()), + center = transformPoint(relativeCenter, this.calcOwnMatrix()); + + return { + offsetX: offset.x, + offsetY: offset.y, + centerX: center.x, + centerY: center.y, + width: size.x, + height: size.y, + }; + }, - this._getBounds(aX, aY, onlyWidthHeight); + /** + * Hook that is called once layout has completed. + * Provided for layout customization, override if necessary. + * Complements `getLayoutStrategyResult`, which is called at the beginning of layout. + * @public + * @param {LayoutContext} context layout context + * @param {LayoutResult} result layout result + */ + onLayout: function (/* context, result */) { + // override by subclass }, /** + * * @private + * @param {'toObject'|'toDatalessObject'} [method] + * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @returns {fabric.Object[]} serialized objects */ - _getBounds: function(aX, aY, onlyWidthHeight) { - var minXY = new fabric.Point(min(aX), min(aY)), - maxXY = new fabric.Point(max(aX), max(aY)), - top = minXY.y || 0, left = minXY.x || 0, - width = (maxXY.x - minXY.x) || 0, - height = (maxXY.y - minXY.y) || 0; - this.width = width; - this.height = height; - if (!onlyWidthHeight) { - // the bounding box always finds the topleft most corner. - // whatever is the group origin, we set up here the left/top position. - this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); - } + __serializeObjects: function (method, propertiesToInclude) { + var _includeDefaultValues = this.includeDefaultValues; + return this._objects + .filter(function (obj) { + return !obj.excludeFromExport; + }) + .map(function (obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var data = obj[method || 'toObject'](propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + //delete data.version; + return data; + }); + }, + + /** + * Returns object representation of an instance + * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function (propertiesToInclude) { + var obj = this.callSuper('toObject', ['layout', 'subTargetCheck', 'interactive'].concat(propertiesToInclude)); + obj.objects = this.__serializeObjects('toObject', propertiesToInclude); + return obj; + }, + + toString: function () { + return '#'; + }, + + dispose: function () { + this._activeObjects = []; + this.forEachObject(function (object) { + this._watchObject(false, object); + object.dispose && object.dispose(); + }, this); }, /* _TO_SVG_START_ */ + /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ - _toSVG: function(reviver) { + _toSVG: function (reviver) { var svgString = ['\n']; - for (var i = 0, len = this._objects.length; i < len; i++) { svgString.push('\t\t', this._objects[i].toSVG(reviver)); } @@ -542,20 +884,19 @@ * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ - toClipPathSVG: function(reviver) { + toClipPathSVG: function (reviver) { var svgString = []; - for (var i = 0, len = this._objects.length; i < len; i++) { svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); } - return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); }, /* _TO_SVG_END_ */ }); /** - * Returns {@link fabric.Group} instance from an object representation + * @todo support loading from svg + * @private * @static * @memberOf fabric.Group * @param {Object} object Object to create a group from @@ -563,12 +904,13 @@ */ fabric.Group.fromObject = function(object) { var objects = object.objects || [], - options = fabric.util.object.clone(object, true); + options = clone(object, true); delete options.objects; - return fabric.util.enlivenObjects(objects).then(function (enlivenedObjects) { - return fabric.util.enlivenObjectEnlivables(options).then(function(enlivedProps) { - return new fabric.Group(enlivenedObjects, Object.assign(options, enlivedProps), true); - }); + return Promise.all([ + fabric.util.enlivenObjects(objects), + fabric.util.enlivenObjectEnlivables(options) + ]).then(function (enlivened) { + return new fabric.Group(enlivened[0], Object.assign(options, enlivened[1]), true); }); }; diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 327fa7fedd2..c91e9adc063 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -899,10 +899,9 @@ * @param {Object} object */ _removeDefaultValues: function(object) { - var prototype = fabric.util.getKlass(object.type).prototype, - stateProperties = prototype.stateProperties; - stateProperties.forEach(function(prop) { - if (prop === 'left' || prop === 'top') { + var prototype = fabric.util.getKlass(object.type).prototype; + Object.keys(object).forEach(function(prop) { + if (prop === 'left' || prop === 'top' || prop === 'type') { return; } if (object[prop] === prototype[prop]) { diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 204fcd14892..393ec40a257 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -32,7 +32,8 @@ * @fires object:added * @fires object:removed */ - fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { + // eslint-disable-next-line max-len + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, fabric.Collection, /** @lends fabric.StaticCanvas.prototype */ { /** * Constructor @@ -563,6 +564,43 @@ return this.lowerCanvasEl; }, + /** + * @param {...fabric.Object} objects to add + * @return {Self} thisArg + * @chainable + */ + add: function () { + fabric.Collection.add.call(this, arguments, this._onObjectAdded); + arguments.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) + * An object should be an instance of (or inherit from) fabric.Object + * @param {fabric.Object|fabric.Object[]} objects Object(s) to insert + * @param {Number} index Index to insert object at + * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs + * @return {Self} thisArg + * @chainable + */ + insertAt: function (objects, index) { + fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @param {...fabric.Object} objects to remove + * @return {Self} thisArg + * @chainable + */ + remove: function () { + var didRemove = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); + didRemove && this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + /** * @private * @param {fabric.Object} obj Object that was added @@ -1600,7 +1638,6 @@ }); extend(fabric.StaticCanvas.prototype, fabric.Observable); - extend(fabric.StaticCanvas.prototype, fabric.Collection); extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { diff --git a/src/util/misc.js b/src/util/misc.js index 24f9b6ad243..6c9c122d0ef 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -606,32 +606,13 @@ * @static * @memberOf fabric.util * @param {Array} elements SVG elements to group - * @param {Object} [options] Options object - * @param {String} path Value to set sourcePath to * @return {fabric.Object|fabric.Group} */ - groupSVGElements: function(elements, options, path) { - var object; + groupSVGElements: function(elements) { if (elements && elements.length === 1) { return elements[0]; } - if (options) { - if (options.width && options.height) { - options.centerPoint = { - x: options.width / 2, - y: options.height / 2 - }; - } - else { - delete options.width; - delete options.height; - } - } - object = new fabric.Group(elements, options); - if (typeof path !== 'undefined') { - object.sourcePath = path; - } - return object; + return new fabric.Group(elements); }, /** diff --git a/test/lib/visualTestLoop.js b/test/lib/visualTestLoop.js index b185658abc7..582e4ef0197 100644 --- a/test/lib/visualTestLoop.js +++ b/test/lib/visualTestLoop.js @@ -136,16 +136,20 @@ var totalPixels = width * height; var imageDataCanvas = renderedCanvas.getContext('2d').getImageData(0, 0, width, height).data; var canvas = fabric.document.createElement('canvas'); + var canvas2 = fabric.document.createElement('canvas'); canvas.width = width; canvas.height = height; + canvas2.width = width; + canvas2.height = height; var ctx = canvas.getContext('2d'); var output = ctx.getImageData(0, 0, width, height); + canvas2.getContext('2d').drawImage(renderedCanvas, 0, 0, width, height); getImage(getGoldeName(golden), renderedCanvas, function(goldenImage) { ctx.drawImage(goldenImage, 0, 0); visualCallback.addArguments({ enabled: true, golden: canvas, - fabric: renderedCanvas, + fabric: canvas2, diff: output }); var imageDataGolden = ctx.getImageData(0, 0, width, height).data; diff --git a/test/unit/activeselection.js b/test/unit/activeselection.js index b2f52f175a6..534b47a36d0 100644 --- a/test/unit/activeselection.js +++ b/test/unit/activeselection.js @@ -62,6 +62,7 @@ width: 80, height: 60, fill: 'rgb(0,0,0)', + layout: 'fit-content', stroke: null, strokeWidth: 0, strokeDashArray: null, @@ -72,6 +73,8 @@ scaleX: 1, scaleY: 1, shadow: null, + subTargetCheck: false, + interactive: false, visible: true, backgroundColor: '', angle: 0, @@ -99,7 +102,7 @@ group.includeDefaultValues = false; var clone = group.toObject(); var objects = [{ - version: fabric.version, + version: fabric.version, type: 'rect', left: 10, top: -30, @@ -107,7 +110,7 @@ height: 10, strokeWidth: 0 }, { - version: fabric.version, + version: fabric.version, type: 'rect', left: -40, top: -10, @@ -122,7 +125,7 @@ top: 100, width: 80, height: 60, - objects: objects + objects: objects, }; assert.deepEqual(clone, expectedObject); }); @@ -181,42 +184,11 @@ // assert.equal(group.get('lockRotation'), true); }); - QUnit.test('insertAt', function(assert) { - var rect1 = new fabric.Rect(), - rect2 = new fabric.Rect(), - group = new fabric.Group(); - - group.add(rect1, rect2); - - assert.ok(typeof group.insertAt === 'function', 'should respond to `insertAt` method'); - - group.insertAt(rect1, 1); - assert.equal(group.item(1), rect1); - group.insertAt(rect2, 2); - assert.equal(group.item(2), rect2); - assert.equal(group.insertAt(rect1, 2), group, 'should be chainable'); - }); - - QUnit.test('group addWithUpdate', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - group = new fabric.Group([rect1]); - - var coords = group.oCoords; - group.addWithUpdate(rect2); - var newCoords = group.oCoords; - assert.notEqual(coords, newCoords, 'object coords have been recalculated - add'); - }); - - QUnit.test('group removeWithUpdate', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - group = new fabric.Group([rect1, rect2]); - - var coords = group.oCoords; - group.removeWithUpdate(rect2); - var newCoords = group.oCoords; - assert.notEqual(coords, newCoords, 'object coords have been recalculated - remove'); + QUnit.test('inherited methods', function (assert) { + var methods = ['add', 'insertAt', 'remove', 'removeAll']; + methods.forEach(method => { + assert.strictEqual(fabric.ActiveSelection.prototype[method], fabric.Group.prototype[method]); + }); }); QUnit.test('ActiveSelection shouldCache', function(assert) { diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 07fc28c4c92..2c7d6f0ef50 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -748,8 +748,8 @@ rect3Selected = true; }); var currentObjects = canvas.getActiveObjects(); - activeSelection.removeWithUpdate(rect1); - activeSelection.addWithUpdate(rect3); + activeSelection.remove(rect1); + activeSelection.add(rect3); canvas._fireSelectionEvents(currentObjects, {}); assert.ok(rect3Selected, 'rect 3 selected'); assert.ok(rect1Deselected, 'rect 1 deselected'); @@ -838,26 +838,31 @@ group = new fabric.Group([rect, rect2]); canvas.add(group); + target = canvas.findTarget({ clientX: 5, clientY: 5 }, true); assert.equal(target, group, 'Should return the group'); assert.equal(canvas.targets[0], undefined, 'no subtarget should return'); + target = canvas.findTarget({ clientX: 30, clientY: 30 }); assert.equal(target, group, 'Should return the group'); group.subTargetCheck = true; + group.setCoords(); target = canvas.findTarget({ clientX: 5, clientY: 5 }); assert.equal(target, group, 'Should return the group'); assert.equal(canvas.targets[0], rect, 'should return the rect'); + target = canvas.findTarget({ clientX: 15, clientY: 15 }); assert.equal(target, group, 'Should return the group'); assert.equal(canvas.targets[0], undefined, 'no subtarget should return'); + target = canvas.findTarget({ clientX: 32, clientY: 32 }); @@ -883,7 +888,7 @@ [rect3, rect4], { scaleX: 0.5, scaleY: 0.5, top: 100, left: 0 }); group3.subTargetCheck = true; - + group3.setCoords(); var rect1 = new fabric.Rect({ width: 100, height: 100, @@ -942,7 +947,6 @@ assert.equal(target, g, 'Should return the group 106'); assert.equal(canvas.targets[0], rect2, 'should find the target rect2 106'); canvas.targets = []; - }); QUnit.test('findTarget with subTargetCheck on activeObject', function(assert) { @@ -950,9 +954,10 @@ rect2 = makeRect({ left: 30, top: 30}), target, group = new fabric.Group([rect, rect2]); + + group.subTargetCheck = true; canvas.add(group); canvas.setActiveObject(group); - group.subTargetCheck = true; target = canvas.findTarget({ clientX: 9, clientY: 9 }); @@ -981,9 +986,9 @@ rect2 = makeRect({ left: 30, top: 30}), target, group = new fabric.Group([rect, rect2]); canvas.preserveObjectStacking = true; + group.subTargetCheck = true; canvas.add(group); canvas.setActiveObject(group); - group.subTargetCheck = true; target = canvas.findTarget({ clientX: 9, clientY: 9 }); @@ -1119,7 +1124,7 @@ canvas.add(rect1); canvas.add(rect2); canvas.add(rect3); - var group = new fabric.ActiveSelection([rect1, rect2]); + var group = new fabric.ActiveSelection([rect1, rect2], { subTargetCheck: true }); canvas.setActiveObject(group); target = canvas.findTarget({ clientX: 5, clientY: 5 @@ -1301,7 +1306,7 @@ assert.equal(center.left, upperCanvasEl.width / 2); assert.equal(center.top, upperCanvasEl.height / 2); }); - + QUnit.test('getCenterPoint', function(assert) { assert.ok(typeof canvas.getCenterPoint === 'function'); var center = canvas.getCenterPoint(); @@ -2593,7 +2598,7 @@ assert.ok(typeof InheritedCanvasClass === 'function'); }); - + QUnit.test('canvas getTopContext', function(assert) { assert.ok(typeof canvas.getTopContext === 'function'); assert.equal(canvas.getTopContext(), canvas.contextTop, 'it jsut returns contextTop'); diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 35890f3dd47..3b6366b3d02 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -188,8 +188,11 @@ canvas.overlayColor = fabric.StaticCanvas.prototype.overlayColor; canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; canvas.calcOffset(); + canvas.requestRenderAll = fabric.StaticCanvas.prototype.requestRenderAll; canvas.cancelRequestedRender(); canvas2.cancelRequestedRender(); + canvas.renderOnAddRemove = false; + canvas2.renderOnAddRemove = false; }, afterEach: function() { canvas.cancelRequestedRender(); @@ -247,6 +250,7 @@ assert.deepEqual(canvas.getObjects('rect'), [rect], 'should return rect only'); assert.deepEqual(canvas.getObjects('circle'), [circle], 'should return circle only'); + assert.deepEqual(canvas.getObjects('circle', 'rect'), [rect, circle], 'should return rect and circle'); }); QUnit.test('getElement', function(assert) { @@ -271,14 +275,25 @@ var rect1 = makeRect(), rect2 = makeRect(), rect3 = makeRect(), - rect4 = makeRect(); + rect4 = makeRect(), + renderAllCount = 0; + function countRenderAll() { + renderAllCount++; + } + canvas.renderOnAddRemove = true; + canvas.requestRenderAll = countRenderAll; assert.ok(typeof canvas.add === 'function'); assert.equal(canvas.add(rect1), canvas, 'should be chainable'); assert.strictEqual(canvas.item(0), rect1); + assert.equal(renderAllCount, 1); canvas.add(rect2, rect3, rect4); assert.equal(canvas.getObjects().length, 4, 'should support multiple arguments'); + assert.equal(renderAllCount, 2); + + canvas.add(); + assert.equal(renderAllCount, 2); assert.strictEqual(canvas.item(1), rect2); assert.strictEqual(canvas.item(2), rect3); @@ -287,17 +302,14 @@ QUnit.test('add renderOnAddRemove disabled', function(assert) { var rect = makeRect(), - originalRenderOnAddition, renderAllCount = 0; function countRenderAll() { renderAllCount++; } - originalRenderOnAddition = canvas.renderOnAddRemove; canvas.renderOnAddRemove = false; - - canvas.on('after:render', countRenderAll); + canvas.requestRenderAll = countRenderAll; assert.equal(canvas.add(rect), canvas, 'should be chainable'); assert.equal(renderAllCount, 0); @@ -307,12 +319,6 @@ canvas.add(makeRect(), makeRect(), makeRect()); assert.equal(canvas.getObjects().length, 4, 'should support multiple arguments'); assert.equal(renderAllCount, 0); - - canvas.renderAll(); - assert.equal(renderAllCount, 1); - - canvas.off('after:render', countRenderAll); - canvas.renderOnAddRemove = originalRenderOnAddition; }); QUnit.test('object:added', function(assert) { @@ -343,34 +349,41 @@ QUnit.test('insertAt', function(assert) { var rect1 = makeRect(), - rect2 = makeRect(); + rect2 = makeRect(), + renderAllCount = 0; canvas.add(rect1, rect2); assert.ok(typeof canvas.insertAt === 'function', 'should respond to `insertAt` method'); + function countRenderAll() { + renderAllCount++; + } + canvas.requestRenderAll = countRenderAll; + canvas.renderOnAddRemove = true; + assert.equal(renderAllCount, 0); var rect = makeRect(); canvas.insertAt(rect, 1); + assert.equal(renderAllCount, 1); assert.strictEqual(canvas.item(1), rect); canvas.insertAt(rect, 2); + assert.equal(renderAllCount, 2); assert.strictEqual(canvas.item(2), rect); assert.equal(canvas.insertAt(rect, 2), canvas, 'should be chainable'); + assert.equal(renderAllCount, 3); }); QUnit.test('insertAt renderOnAddRemove disabled', function(assert) { var rect1 = makeRect(), rect2 = makeRect(), - originalRenderOnAddition, renderAllCount = 0; function countRenderAll() { renderAllCount++; } - originalRenderOnAddition = canvas.renderOnAddRemove; canvas.renderOnAddRemove = false; - - canvas.on('after:render', countRenderAll); + canvas.requestRenderAll = countRenderAll; canvas.add(rect1, rect2); assert.equal(renderAllCount, 0); @@ -383,60 +396,53 @@ assert.strictEqual(canvas.item(1), rect); canvas.insertAt(rect, 2); assert.equal(renderAllCount, 0); - - canvas.renderAll(); - assert.equal(renderAllCount, 1); - - canvas.off('after:render', countRenderAll); - canvas.renderOnAddRemove = originalRenderOnAddition; }); QUnit.test('remove', function(assert) { var rect1 = makeRect(), rect2 = makeRect(), rect3 = makeRect(), - rect4 = makeRect(); + rect4 = makeRect(), + renderAllCount = 0; - canvas.add(rect1, rect2, rect3, rect4); + function countRenderAll() { + renderAllCount++; + } + canvas.add(rect1, rect2, rect3, rect4); + canvas.requestRenderAll = countRenderAll; + canvas.renderOnAddRemove = true; assert.ok(typeof canvas.remove === 'function'); + assert.equal(renderAllCount, 0); assert.equal(canvas.remove(rect1), canvas, 'should be chainable'); assert.strictEqual(canvas.item(0), rect2, 'should be second object'); canvas.remove(rect2, rect3); + assert.equal(renderAllCount, 2); assert.strictEqual(canvas.item(0), rect4); canvas.remove(rect4); + assert.equal(renderAllCount, 3); assert.equal(canvas.isEmpty(), true, 'canvas should be empty'); }); QUnit.test('remove renderOnAddRemove disabled', function(assert) { var rect1 = makeRect(), rect2 = makeRect(), - originalRenderOnAddition, renderAllCount = 0; function countRenderAll() { renderAllCount++; } - - originalRenderOnAddition = canvas.renderOnAddRemove; + canvas.requestRenderAll = countRenderAll; canvas.renderOnAddRemove = false; - canvas.on('after:render', countRenderAll); - canvas.add(rect1, rect2); assert.equal(renderAllCount, 0); assert.equal(canvas.remove(rect1), canvas, 'should be chainable'); assert.equal(renderAllCount, 0); assert.strictEqual(canvas.item(0), rect2, 'only second object should be left'); - - canvas.renderAll(); - assert.equal(renderAllCount, 1); - - canvas.off('after:render', countRenderAll); - canvas.renderOnAddRemove = originalRenderOnAddition; }); QUnit.test('object:removed', function(assert) { diff --git a/test/unit/collection.js b/test/unit/collection.js index 6a92b83b6be..064fae5d4d7 100644 --- a/test/unit/collection.js +++ b/test/unit/collection.js @@ -5,121 +5,127 @@ QUnit.module('fabric.Collection', { beforeEach: function() { - collection.rendered = 0; collection._objects = []; - delete collection.renderOnAddRemove; - delete collection._onObjectAdded; - delete collection._onObjectRemoved; collection2._objects = []; } }); - collection.requestRenderAll = function() { - this.rendered++; - }; - QUnit.test('add', function(assert) { var obj = { prop: 4 }, fired = 0; assert.ok(typeof collection.add === 'function', 'has add method'); assert.deepEqual(collection._objects, [], 'start with empty array of items'); - var returned = collection.add(obj); - assert.equal(returned, collection, 'is chainable'); + var returned = collection.add([obj], cb); assert.equal(collection._objects[0], obj, 'add object in the array'); assert.equal(fired, 0, 'fired is 0'); - - collection._onObjectAdded = function() { + var cb = function () { fired++; }; - collection.add(obj); + collection.add([obj], cb); assert.equal(collection._objects[1], obj, 'add object in the array'); - assert.equal(fired, 1, 'fired is incremented if there is a _onObjectAdded'); - collection.renderOnAddRemove = true; - assert.equal(collection.rendered, 0, 'this.renderAll has not been called'); - collection.add(obj); - assert.equal(collection.rendered, 1, 'this.renderAll has been called'); + assert.equal(fired, 1, 'fired is incremented due to callback'); + collection.add([obj], cb); assert.equal(collection._objects.length, 3, 'we have 3 objects in collection'); fired = 0; - collection.add(obj, obj, obj, obj); + collection.add([obj, obj, obj, obj], cb); assert.equal(fired, 4, 'fired is incremented for every object added'); assert.equal(collection._objects.length, 7, 'all objects have been added'); - assert.equal(collection.rendered, 2, 'this.renderAll has been called just once more'); }); - QUnit.test('insertAt', function(assert) { - var obj = { prop: 4 }, fired = 0, index = 1, nonSplicing = false; - collection._objects = [{ prop: 0 }, {prop: 1}]; - assert.ok(typeof collection.insertAt === 'function', 'has insertAdd method'); - var previousObject = collection._objects[index]; - var previousLength = collection._objects.length; - collection.insertAt(obj, index, nonSplicing); - assert.equal(collection._objects[index], obj, 'add object in the array at specified index'); - assert.equal(collection._objects[index + 1], previousObject, 'add old object in the array at next index'); - assert.equal(collection._objects.length, previousLength + 1, 'length is incremented'); + QUnit.test('insertAt', function (assert) { + var rect1 = new fabric.Rect({ id: 1 }), + rect2 = new fabric.Rect({ id: 2 }), + rect3 = new fabric.Rect({ id: 3 }), + rect4 = new fabric.Rect({ id: 4 }), + rect5 = new fabric.Rect({ id: 5 }), + rect6 = new fabric.Rect({ id: 6 }), + rect7 = new fabric.Rect({ id: 7 }), + rect8 = new fabric.Rect({ id: 8 }), + control = [], + fired = [], + firingControl = []; + + collection.add([rect1, rect2]); + control.push(rect1, rect2); + + assert.ok(typeof collection.insertAt === 'function', 'should respond to `insertAt` method'); + + const equalsControl = () => { + assert.deepEqual(collection.getObjects().map(o => o.id), control.map(o => o.id), 'should equal control array'); + assert.deepEqual(collection.getObjects(), control, 'should equal control array'); + assert.deepEqual(fired.map(o => o.id), firingControl.map(o => o.id), 'fired events should equal control array'); + assert.deepEqual(fired, firingControl, 'fired events should equal control array'); + } - nonSplicing = true; - previousLength = collection._objects.length; - var newObject = { prop: 5 }; - previousObject = collection._objects[index]; - var returned = collection.insertAt(newObject, index, nonSplicing); - assert.equal(returned, collection, 'is chainable'); - assert.equal(collection._objects[index], newObject, 'add newobject in the array at specified index'); - assert.notEqual(collection._objects[index + 1], previousObject, 'old object is not in the array at next index'); - assert.equal(collection._objects.indexOf(previousObject), -1, 'old object is no more in array'); - assert.equal(collection._objects.length, previousLength, 'length is not incremented'); assert.ok(typeof collection._onObjectAdded === 'undefined', 'do not have a standard _onObjectAdded method'); - assert.equal(fired, 0, 'fired is 0'); - collection._onObjectAdded = function() { - fired++; + var cb = function (object) { + fired.push(object); }; - collection.insertAt(obj, 1); - assert.equal(fired, 1, 'fired is incremented if there is a _onObjectAdded'); - collection.renderOnAddRemove = true; - collection.insertAt(obj, 1); - assert.equal(collection.rendered, 1, 'this.renderAll has been called'); + + collection.insertAt(rect3, 1, cb); + control.splice(1, 0, rect3); + firingControl.push(rect3); + equalsControl(); + collection.insertAt(rect4, 0, cb); + control.splice(0, 0, rect4); + firingControl.push(rect4); + equalsControl(); + collection.insertAt(rect5, 2, cb); + control.splice(2, 0, rect5); + firingControl.push(rect5); + equalsControl(); + collection.insertAt([rect6], 2, cb); + control.splice(2, 0, rect6); + firingControl.push(rect6); + equalsControl(); + collection.insertAt([rect7, rect8], 3, cb); + control.splice(3, 0, rect7, rect8); + firingControl.push(rect7, rect8); + equalsControl(); + // insert duplicates + collection.insertAt([rect1, rect2], 2, cb); + control.splice(2, 0, rect1, rect2); + firingControl.push(rect1, rect2); + equalsControl(); }); QUnit.test('remove', function(assert) { var obj = { prop: 4 }, obj2 = { prop: 2 }, obj3 = { prop: 3 }, fired = 0; - collection.add({ prop: 0 }, {prop: 1}, obj2, obj, obj3); + collection.add([{ prop: 0 }, {prop: 1}, obj2, obj, obj3]); var previousLength = collection._objects.length; assert.ok(typeof collection.remove === 'function', 'has remove method'); - var returned = collection.remove(obj); - assert.equal(returned, collection, 'is chainable'); + var returned = collection.remove([obj]); + assert.ok(returned, 'removed obj'); + assert.ok(!collection.remove([{ prop: 'foo' }]), 'nothing removed'); assert.equal(collection._objects.indexOf(obj), -1, 'obj is no more in array'); assert.equal(collection._objects.length, previousLength - 1, 'length has changed'); assert.equal(fired, 0, 'fired is 0'); - collection._onObjectRemoved = function() { + var callback = function() { fired++; }; - collection.remove(obj2); - assert.equal(fired, 1, 'fired is incremented if there is a _onObjectAdded'); - collection.remove(obj2); + collection.remove([obj2], callback); + assert.equal(fired, 1, 'fired is incremented if there is a callback'); + collection.remove([obj2], callback); assert.equal(fired, 1, 'fired is not incremented again if there is no object to remove'); - collection.add(obj2); - collection.add(obj); - collection.renderOnAddRemove = true; - assert.equal(collection.rendered, 0, 'this.renderAll has not been called'); - collection.remove(obj2); - assert.equal(collection.rendered, 1, 'this.renderAll has been called'); + collection.add([obj2]); + collection.add([obj]); + collection.remove([obj2], callback); previousLength = collection._objects.length; fired = 0; - collection.remove(obj, obj3); + collection.remove([obj, obj3], callback); assert.equal(collection._objects.length, previousLength - 2, 'we have 2 objects less'); assert.equal(fired, 2, 'fired is incremented for every object removed'); - assert.equal(collection.rendered, 2, 'this.renderAll has been called just once more'); }); QUnit.test('forEachObject', function(assert) { var obj = { prop: false }, obj2 = { prop: false }, obj3 = { prop: false }, fired = 0; - collection.add(obj2, obj, obj3); + collection.add([obj2, obj, obj3]); assert.ok(typeof collection.forEachObject === 'function', 'has forEachObject method'); var callback = function(_obj) { _obj.prop = true; fired++; }; var returned = collection.forEachObject(callback); - assert.equal(returned, collection, 'is chainable'); assert.equal(fired, collection._objects.length, 'fired once for every object'); assert.equal(obj.prop, true, 'fired for obj'); assert.equal(obj2.prop, true, 'fired for obj2'); @@ -127,8 +133,8 @@ }); QUnit.test('getObjects', function(assert) { - var obj = { type: 'a' }, obj2 = { type: 'b' }; - collection.add(obj2, obj); + var obj = { type: 'a' }, obj2 = { type: 'b' }, obj3 = { type: 'c' }; + collection.add([obj2, obj, obj3]); assert.ok(typeof collection.getObjects === 'function', 'has getObjects method'); var returned = collection.getObjects(); assert.notEqual(returned, collection._objects, 'does not return a reference to _objects'); @@ -136,11 +142,16 @@ assert.notEqual(returned, collection._objects, 'return a new array'); assert.equal(returned.indexOf(obj2), -1, 'object of type B is not included'); assert.equal(returned.indexOf(obj), 0, 'object of type A is included'); + returned = collection.getObjects('a', 'b'); + assert.ok(returned.indexOf(obj2) > -1, 'object of type B is not included'); + assert.ok(returned.indexOf(obj) > -1, 'object of type A is included'); + assert.ok(returned.indexOf(obj3) === -1, 'object of type c is included'); + assert.equal(returned.length, 2, 'returned only a, b types'); }); QUnit.test('item', function(assert) { var obj = { type: 'a' }, obj2 = { type: 'b' }, index = 1; - collection.add(obj2, obj); + collection.add([obj2, obj]); assert.ok(typeof collection.item === 'function', 'has item method'); var returned = collection.item(index); assert.equal(returned, collection._objects[index], 'return the object at index'); @@ -151,7 +162,7 @@ assert.ok(typeof collection.isEmpty === 'function', 'has isEmpty method'); var returned = collection.isEmpty(); assert.equal(returned, true, 'collection is empty'); - collection.add(obj2, obj); + collection.add([obj2, obj]); returned = collection.isEmpty(); assert.equal(returned, false, 'collection is not empty'); }); @@ -162,7 +173,7 @@ var returned = collection.size(); assert.ok(typeof returned === 'number', 'returns a number'); assert.equal(returned, 0, 'collection is empty'); - collection.add(obj2, obj); + collection.add([obj2, obj]); returned = collection.size(); assert.equal(returned, 2, 'collection has 2 objects'); }); @@ -173,12 +184,12 @@ var returned = collection.contains(obj); assert.ok(typeof returned === 'boolean', 'returns a boolean'); assert.equal(returned, false, 'collection is empty so does not contains obj'); - collection.add(obj); + collection.add([obj]); returned = collection.contains(obj); assert.equal(returned, true, 'collection contains obj'); var obj2 = { type: 'b' }; - collection2.add(obj2); - collection.add(collection2); + collection2.add([obj2]); + collection.add([collection2]); returned = collection.contains(obj2); assert.equal(returned, false, 'collection deeply contains obj, this check is shallow'); returned = collection.contains(obj2, false); @@ -193,12 +204,12 @@ var returned = collection.complexity(); assert.ok(typeof returned === 'number', 'returns a number'); assert.equal(returned, 0, 'collection has complexity 0'); - collection.add(obj2, obj); + collection.add([obj2, obj]); returned = collection.complexity(); assert.equal(returned, 0, 'collection has complexity 0 if objects have no complexity themselves'); var complexObject = { complexity: function() { return 9; }}; var complexObject2 = { complexity: function() { return 10; }}; - collection.add(complexObject, complexObject2); + collection.add([complexObject, complexObject2]); returned = collection.complexity(); assert.equal(returned, 19, 'collection has complexity 9 + 10'); }); diff --git a/test/unit/group.js b/test/unit/group.js index 1c9dc40e977..88aa19625f9 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -5,7 +5,7 @@ var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0 }); - return new fabric.Group([rect1, rect2], {strokeWidth: 0}); + return new fabric.Group([rect1, rect2], { strokeWidth: 0 }); } function makeGroupWith2ObjectsWithOpacity() { @@ -74,6 +74,7 @@ assert.deepEqual(group.getObjects('rect'), [rect], 'should return rect only'); assert.deepEqual(group.getObjects('circle'), [circle], 'should return circle only'); + assert.deepEqual(group.getObjects('circle', 'rect'), [rect, circle], 'should return circle and rect, in the same order they are'); }); QUnit.test('add', function(assert) { @@ -83,7 +84,7 @@ rect3 = new fabric.Rect(); assert.ok(typeof group.add === 'function'); - assert.equal(group.add(rect1), group, 'should be chainable'); + group.add(rect1); assert.strictEqual(group.item(group.size() - 1), rect1, 'last object should be newly added one'); assert.equal(group.getObjects().length, 3, 'there should be 3 objects'); @@ -99,7 +100,7 @@ group = new fabric.Group([rect1, rect2, rect3]); assert.ok(typeof group.remove === 'function'); - assert.equal(group.remove(rect2), group, 'should be chainable'); + group.remove(rect2); assert.deepEqual(group.getObjects(), [rect1, rect3], 'should remove object properly'); group.remove(rect1, rect3); @@ -124,7 +125,7 @@ assert.ok(typeof group.set === 'function'); - assert.equal(group.set('opacity', 0.12345), group, 'should be chainable'); + group.set('opacity', 0.12345); assert.equal(group.get('opacity'), 0.12345, 'group\'s "own" property should be set properly'); assert.equal(firstObject.get('opacity'), 1, 'objects\' value of non delegated property should stay same'); @@ -168,6 +169,7 @@ width: 80, height: 60, fill: 'rgb(0,0,0)', + layout: 'fit-content', stroke: null, strokeWidth: 0, strokeDashArray: null, @@ -190,7 +192,9 @@ skewX: 0, skewY: 0, objects: clone.objects, - strokeUniform: false + strokeUniform: false, + subTargetCheck: false, + interactive: false, }; assert.deepEqual(clone, expectedObject); @@ -211,7 +215,7 @@ top: -30, width: 30, height: 10, - strokeWidth: 0 + strokeWidth: 0, }, { version: fabric.version, type: 'rect', @@ -219,7 +223,7 @@ top: -10, width: 10, height: 40, - strokeWidth: 0 + strokeWidth: 0, }]; var expectedObject = { version: fabric.version, @@ -228,7 +232,7 @@ top: 100, width: 80, height: 60, - objects: objects + objects: objects, }; assert.deepEqual(clone, expectedObject); }); @@ -301,38 +305,22 @@ assert.equal(group.complexity(), 2); }); - QUnit.test('destroy', function(assert) { + QUnit.test('removeAll', function(assert) { var group = makeGroupWith2Objects(), firstObject = group.item(0), initialLeftValue = 100, initialTopValue = 100; - assert.ok(typeof group.destroy === 'function'); + assert.ok(typeof group.removeAll === 'function'); assert.ok(initialLeftValue !== firstObject.get('left')); assert.ok(initialTopValue !== firstObject.get('top')); - group.destroy(); + group.removeAll(); assert.equal(firstObject.get('left'), initialLeftValue, 'should restore initial left value'); assert.equal(firstObject.get('top'), initialTopValue, 'should restore initial top value'); }); - QUnit.test('setObjectCoords', function(assert) { - var group = makeGroupWith2Objects(); - - assert.ok(typeof group.setObjectsCoords === 'function'); - - var invokedObjects = []; - group.forEachObject(function(groupObject){ - groupObject.setCoords = function() { - invokedObjects.push(this); - }; - }, this); - - assert.equal(group.setObjectsCoords(), group, 'should be chainable'); - // this.assertEnumEqualUnordered(invokedObjects, group.getObjects(), 'setObjectsCoords should call setCoords on all objects'); - }); - QUnit.test('containsPoint', function(assert) { var group = makeGroupWith2Objects(); @@ -362,7 +350,6 @@ var group = makeGroupWith2Objects(); assert.ok(typeof group.forEachObject === 'function'); - assert.equal(group.forEachObject(function(){}), group, 'should be chainable'); var iteratedObjects = []; group.forEachObject(function(groupObject) { @@ -558,20 +545,60 @@ assert.ok(typeof firstObjInGroup.group === 'undefined'); }); - QUnit.test('insertAt', function(assert) { - var rect1 = new fabric.Rect(), - rect2 = new fabric.Rect(), - group = new fabric.Group(); + QUnit.test('insertAt', function (assert) { + var rect1 = new fabric.Rect({ id: 1 }), + rect2 = new fabric.Rect({ id: 2 }), + rect3 = new fabric.Rect({ id: 3 }), + rect4 = new fabric.Rect({ id: 4 }), + rect5 = new fabric.Rect({ id: 5 }), + rect6 = new fabric.Rect({ id: 6 }), + rect7 = new fabric.Rect({ id: 7 }), + rect8 = new fabric.Rect({ id: 8 }), + group = new fabric.Group(), + control = [], + fired = [], + firingControl = []; group.add(rect1, rect2); + control.push(rect1, rect2); assert.ok(typeof group.insertAt === 'function', 'should respond to `insertAt` method'); - group.insertAt(rect1, 1); - assert.equal(group.item(1), rect1); - group.insertAt(rect2, 2); - assert.equal(group.item(2), rect2); - assert.equal(group.insertAt(rect1, 2), group, 'should be chainable'); + const equalsControl = (description) => { + assert.deepEqual(group.getObjects().map(o => o.id), control.map(o => o.id), 'should equal control array ' + description); + assert.deepEqual(group.getObjects(), control, 'should equal control array ' + description); + assert.deepEqual(fired.map(o => o.id), firingControl.map(o => o.id), 'fired events should equal control array ' + description); + assert.deepEqual(fired, firingControl, 'fired events should equal control array ' + description); + } + + assert.ok(typeof group._onObjectAdded === 'function', 'has a standard _onObjectAdded method'); + [rect1, rect2, rect3, rect4, rect5, rect6, rect7, rect8].forEach(obj => { + obj.on('added', e => { + assert.equal(e.target, group); + fired.push(obj); + }); + }); + + group.insertAt(rect3, 1); + control.splice(1, 0, rect3); + firingControl.push(rect3); + equalsControl('rect3'); + group.insertAt(rect4, 0); + control.splice(0, 0, rect4); + firingControl.push(rect4); + equalsControl('rect4'); + group.insertAt(rect5, 2); + control.splice(2, 0, rect5); + firingControl.push(rect5); + equalsControl('rect5'); + group.insertAt([rect6], 2); + control.splice(2, 0, rect6); + firingControl.push(rect6); + equalsControl('rect6'); + group.insertAt([rect7, rect8], 3); + control.splice(3, 0, rect7, rect8); + firingControl.push(rect7, rect8); + equalsControl('rect7'); }); QUnit.test('dirty flag propagation from children up', function(assert) { @@ -612,7 +639,7 @@ assert.equal(g1.dirty, false, 'Group has no dirty flag set'); }); - QUnit.test('dirty flag propagation from children up with', function(assert) { + QUnit.test('dirty flag propagation from children up with', function (assert) { var g1 = makeGroupWith4Objects(); var obj = g1.item(0); g1.dirty = false; @@ -638,7 +665,7 @@ QUnit.test('test group - pixels.', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - group = new fabric.Group([rect1, rect2], {opacity: 1, fill: 'blue', strokeWidth: 0, objectCaching: false}), + group = new fabric.Group([rect1, rect2], {opacity: 1, fill: '', strokeWidth: 0, objectCaching: false}), isTransparent = fabric.util.isTransparent, ctx = canvas.contextContainer; canvas.add(group); @@ -654,34 +681,24 @@ assert.equal(isTransparent(ctx, 7, 7, 0), true, '7,7 is transparent'); }); - QUnit.test('group toDatalessObject', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - pathGroup = new fabric.Group([rect1, rect2], { sourcePath: 'sourcePath'}), - group = new fabric.Group([pathGroup]), - dataless = group.toDatalessObject(); - - assert.equal(dataless.objects[0].objects, 'sourcePath', 'the paths have been changed with the sourcePath'); - }); - - QUnit.test('group addWithUpdate', function(assert) { + QUnit.test('group add', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), group = new fabric.Group([rect1]); var coords = group.oCoords; - group.addWithUpdate(rect2); + group.add(rect2); var newCoords = group.oCoords; assert.notEqual(coords, newCoords, 'object coords have been recalculated - add'); }); - QUnit.test('group removeWithUpdate', function(assert) { + QUnit.test('group remove', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), group = new fabric.Group([rect1, rect2]); var coords = group.oCoords; - group.removeWithUpdate(rect2); + group.remove(rect2); var newCoords = group.oCoords; assert.notEqual(coords, newCoords, 'object coords have been recalculated - remove'); }); @@ -759,31 +776,6 @@ }); - QUnit.test('useSetOnGroup', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - group = new fabric.Group([rect1, rect2]); - - var count = 0; - var inspectKey = ''; - var inspectValue = ''; - rect1.setOnGroup = function(key, value) { - count++; - inspectKey = key; - inspectValue = value; - }; - - group.set('fill', 'red'); - assert.equal(count, 0, 'setOnGroup has not been called'); - assert.equal(inspectKey, '', 'setOnGroup has not been called'); - assert.equal(inspectValue, '', 'setOnGroup has not been called'); - group.useSetOnGroup = true; - group.set('fill', 'red'); - assert.equal(count, 1, 'setOnGroup has been called'); - assert.equal(inspectKey, 'fill', 'setOnGroup has been called'); - assert.equal(inspectValue, 'red', 'setOnGroup has been called'); - }); - QUnit.test('canvas prop propagation with set', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), @@ -815,21 +807,21 @@ assert.equal(group.canvas, canvas, 'canvas has been set'); group.add(rect1); assert.equal(group._objects[0].canvas, canvas, 'canvas has been set on object 0'); - group.addWithUpdate(rect2); + group.add(rect2); assert.equal(group._objects[1].canvas, canvas, 'canvas has been set on object 0'); }); - QUnit.test('addWithUpdate and coordinates', function(assert) { + QUnit.test('add and coordinates', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), group = new fabric.Group([]); - group.addWithUpdate(rect1); - group.addWithUpdate(rect2); + group.add(rect1); + group.add(rect2); group.left = 5; group.top = 5; group.scaleX = 3; group.scaleY = 2; - group.destroy(); + group.removeAll(); assert.equal(rect1.top, 5, 'top has been moved'); assert.equal(rect1.left, 11, 'left has been moved'); assert.equal(rect1.scaleX, 3, 'scaleX has been scaled'); @@ -840,7 +832,7 @@ assert.equal(rect2.scaleY, 3, 'scaleY has been scaled inverted because of angle 90'); }); - QUnit.test('addWithUpdate and coordinates with nested groups', function(assert) { + QUnit.skip('addRelativeToGroup and coordinates with nested groups', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), group0 = new fabric.Group([rect1, rect2]), @@ -850,13 +842,15 @@ group = new fabric.Group([group0, group1], { angle: 90, scaleX: 2, scaleY: 0.5 }), rect5 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }); - group1.addWithUpdate(rect5); + group1.addRelativeToGroup(rect5); + var t = group1.calcTransformMatrix(); + var pos = fabric.util.transformPoint(new fabric.Point(rect5.left, rect5.top), t); assert.equal(rect5.top, -5.5, 'top has been moved'); assert.equal(rect5.left, -19.5, 'left has been moved'); assert.equal(rect5.scaleX, 2, 'scaleX has been scaled'); assert.equal(rect5.scaleY, 0.5, 'scaleY has been scaled'); - group.destroy(); - group1.destroy(); + group.removeAll(); + group1.removeAll(); assert.equal(rect5.top, 1, 'top is back to original minus rounding errors'); assert.equal(rect5.left, 1, 'left is back to original'); assert.equal(rect5.scaleX, 1, 'scaleX is back to original'); diff --git a/test/unit/object.js b/test/unit/object.js index 855b64f9d55..54807171622 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -425,7 +425,7 @@ assert.equal(cObj.canvas, canvas, 'canvas is the main one step 2'); - activeSel.destroy(); + activeSel.removeAll(); assert.equal(cObj.canvas, canvas, 'canvas is the main one step 3'); diff --git a/test/visual/group_layout.js b/test/visual/group_layout.js new file mode 100644 index 00000000000..ebdae56cf34 --- /dev/null +++ b/test/visual/group_layout.js @@ -0,0 +1,338 @@ +(function () { + fabric.Object.prototype.objectCaching = true; + var visualTestLoop; + if (fabric.isLikelyNode) { + visualTestLoop = global.visualTestLoop; + } + else { + visualTestLoop = window.visualTestLoop; + } + + var tests = []; + + function createGroupForLayoutTests(text, options) { + var circle = new fabric.Circle({ + left: 100, + top: 50, + radius: 50 + }); + var itext = new fabric.IText(text, { + left: 100, + top: 150 + }); + var rect = new fabric.Rect({ + top: 200, + width: 50, + height: 50, + fill: 'red', + opacity: 0.3 + }) + return new fabric.Group([ + rect, + circle, + itext + ], options); + } + + function fixedLayout(canvas, callback) { + var g = createGroupForLayoutTests('fixed layout', { + backgroundColor: 'azure', + layout: 'fixed', + width: 50, + height: 50, + angle: 30 + }); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } +/* + tests.push({ + test: 'fixed layout with width, height, angle values', + code: fixedLayout, + golden: 'group-fixed-layout.png', + newModule: 'Group Layout', + percentage: 0.06, + width: 400, + height: 300 + }); +*/ + function fitContentLayout(canvas, callback) { + var g = createGroupForLayoutTests('fit-content layout', { + backgroundColor: 'blue' + }); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'fit-content layout', + code: fitContentLayout, + golden: 'group-layout/fit-content.png', + newModule: 'Group Layout', + percentage: 0.06, + width: 400, + height: 300 + }); + + function fitContentReLayout(canvas, callback) { + var g = createGroupForLayoutTests('fit-content layout', { + backgroundColor: 'blue' + }); + var objects = g.removeAll(); + // layout + objects.forEach(o => g.add(o)); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'fit-content relayout', + code: fitContentReLayout, + golden: 'group-layout/fit-content.png', + percentage: 0.06, + width: 400, + height: 300 + }); + + function nestedLayout(canvas, callback) { + var rect3 = new fabric.Rect({ + width: 100, + height: 100, + fill: 'yellow' + }); + var rect4 = new fabric.Rect({ + width: 100, + height: 100, + left: 100, + top: 100, + fill: 'purple' + }); + var group3 = new fabric.Group( + [rect3, rect4], + { scaleX: 0.5, scaleY: 0.5, top: 100, left: 0 }); + group3.subTargetCheck = true; + group3.setCoords(); + var rect1 = new fabric.Rect({ + width: 100, + height: 100, + fill: 'red' + }); + var rect2 = new fabric.Rect({ + width: 100, + height: 100, + left: 100, + top: 100, + fill: 'blue' + }); + var g = new fabric.Group([rect1, rect2, group3], { top: -150, left: -50 }); + g.subTargetCheck = true; + canvas.viewportTransform = [0.1, 0, 0, 0.1, 100, 200]; + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'unit test scene', + code: nestedLayout, + golden: 'group-layout/unit-test-scene.png', + percentage: 0.01, + width: 120, + height: 210 + }); + + function fitContentLayoutChange(canvas, callback) { + var g = createGroupForLayoutTests('fit-content layout', { + backgroundColor: 'blue' + }); + var point = fabric.util.transformPoint( + new fabric.Point(50, 0), + fabric.util.invertTransform(g.calcTransformMatrix()) + ); + g.item(0).set({ left: point.x }); + g.item(1).set({ skewX: -45 }); + g.item(2).rotate(45); + g.triggerLayout(); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'fit-content layout after change', + code: fitContentLayoutChange, + golden: 'group-layout/fit-content2.png', + percentage: 0.06, + width: 400, + height: 300 + }); + + function fitContentLayoutAdd(canvas, callback) { + var g = createGroupForLayoutTests('fit-content layout', { + backgroundColor: 'blue' + }); + var rect = new fabric.Rect({ + top: 200, + left: 50, + width: 50, + height: 50, + fill: 'red', + angle: 15, + skewY: 30 + }) + g.add(rect); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'fit-content layout add object', + code: fitContentLayoutAdd, + golden: 'group-layout/fit-content3.png', + newModule: 'Group Layout', + percentage: 0.06, + width: 400, + height: 300 + }); + + function clipPathLayout(canvas, callback) { + var g = createGroupForLayoutTests('clip path layout', { + backgroundColor: 'magenta', + clipPath: new fabric.Circle({ + radius: 110, + originX: 'center', + originY: 'center', + }), + layout: 'clip-path' + }); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'clip-path layout', + code: clipPathLayout, + golden: 'group-layout/clip-path.png', + percentage: 0.06, + width: 300, + height: 300 + }); + + function clipPathLayoutWithScale(canvas, callback) { + var g = createGroupForLayoutTests('clip path layout', { + backgroundColor: 'magenta', + clipPath: new fabric.Circle({ + radius: 110, + originX: 'center', + originY: 'center', + scaleX: 0.6 + }), + layout: 'clip-path' + }); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'clip-path layout scaleX value', + code: clipPathLayoutWithScale, + golden: 'group-layout/clip-path1.png', + percentage: 0.06, + width: 300, + height: 300 + }); + + function clipPathLayout2(canvas, callback) { + var g = createGroupForLayoutTests('clip path layout', { + backgroundColor: 'magenta', + clipPath: new fabric.Circle({ + radius: 110, + left: -150, + top: -100, + scaleX: 1.5, + }), + layout: 'clip-path' + }); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'clip-path layout left, top, originX, originY, scaleX values - WRONG', + code: clipPathLayout2, + golden: 'group-layout/clip-path2.png', + percentage: 0.06, + width: 330, + height: 330 + }); + + function absClipPathLayout(canvas, callback) { + var g = createGroupForLayoutTests('clip path layout', { + backgroundColor: '#0dcaf0', + clipPath: new fabric.Circle({ + radius: 110, + originX: 'center', + originY: 'center', + absolutePositioned: true, + left: 50, + top: 150, + skewX: 20 + }), + layout: 'clip-path', + }); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'absolute clip-path layout', + code: absClipPathLayout, + golden: 'group-layout/clip-path3.png', + percentage: 0.06, + width: 250, + height: 250 + }); + + function createObjectsForOriginTests(originX, originY, options) { + var rect1 = new fabric.Rect({ top: 100, left: 150, width: 30, height: 10, strokeWidth: 0 }), + rect2 = new fabric.Rect({ top: 120, left: 200, width: 10, height: 40, strokeWidth: 0 }), + controlPoint = new fabric.Circle({ radius: 5, fill: 'blue', left: 150, top: 100, originX: 'center', originY: 'center' }); + + var g = new fabric.Group([rect1, rect2], Object.assign({}, options, { + originX, originY, strokeWidth: 1, stroke: 'blue' + })); + return [controlPoint, g]; + } + + var originX = ['left', 'center', 'right']; + var originY = ['top', 'center', 'bottom']; +/* + for (let angle = 0; angle < 360; angle += 30) { + originX.forEach(ox => { + originY.forEach(oy => { + tests.push({ + test: `layout with originX=${ox}, originY=${oy} and angle=${angle} values - angle is WRONG`, + code: function (canvas, callback) { + canvas.add(...createObjectsForOriginTests(ox, oy, { angle })); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + }, + golden: `group-layout/origin-${ox}-${oy}-${angle}deg.png`, + percentage: 0.06, + width: 300, + height: 300 + }); + }); + }); + } +*/ + // tests.forEach(visualTestLoop(QUnit)); +})(); diff --git a/test/visual/svg_import.js b/test/visual/svg_import.js index d58b58db094..7230d11e593 100644 --- a/test/visual/svg_import.js +++ b/test/visual/svg_import.js @@ -22,10 +22,10 @@ fabric.loadSVGFromString(string, function(objects, options) { // something is disabling objectCaching and i cannot find where it is. var group = fabric.util.groupSVGElements(objects, options); + canvas.setDimensions({ width: group.width + group.left, height: group.height + group.top }); group.includeDefaultValues = false; canvas.includeDefaultValues = false; canvas.add(group); - canvas.setDimensions({ width: group.width + group.left, height: group.height + group.top }); canvas.renderAll(); callback(canvas.lowerCanvasEl); }); From 26e8392bebc375b8e78630036d90f4c1cec3ac1a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 3 Apr 2022 23:33:04 +0300 Subject: [PATCH 02/77] apply 7744 patch (binaries) --- test/visual/golden/group-layout/clip-path.png | Bin 0 -> 12684 bytes test/visual/golden/group-layout/clip-path1.png | Bin 0 -> 10197 bytes test/visual/golden/group-layout/clip-path2.png | Bin 0 -> 8690 bytes test/visual/golden/group-layout/clip-path3.png | Bin 0 -> 8033 bytes test/visual/golden/group-layout/fit-content.png | Bin 0 -> 6127 bytes .../visual/golden/group-layout/fit-content2.png | Bin 0 -> 7758 bytes .../visual/golden/group-layout/fit-content3.png | Bin 0 -> 7137 bytes .../golden/group-layout/unit-test-scene.png | Bin 0 -> 402 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/visual/golden/group-layout/clip-path.png create mode 100644 test/visual/golden/group-layout/clip-path1.png create mode 100644 test/visual/golden/group-layout/clip-path2.png create mode 100644 test/visual/golden/group-layout/clip-path3.png create mode 100644 test/visual/golden/group-layout/fit-content.png create mode 100644 test/visual/golden/group-layout/fit-content2.png create mode 100644 test/visual/golden/group-layout/fit-content3.png create mode 100644 test/visual/golden/group-layout/unit-test-scene.png diff --git a/test/visual/golden/group-layout/clip-path.png b/test/visual/golden/group-layout/clip-path.png new file mode 100644 index 0000000000000000000000000000000000000000..1dae5fcca2e0e15eebd31da8b7b7935f2d5a428e GIT binary patch literal 12684 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 literal 0 HcmV?d00001 diff --git a/test/visual/golden/group-layout/clip-path1.png b/test/visual/golden/group-layout/clip-path1.png new file mode 100644 index 0000000000000000000000000000000000000000..1ee7e86807bec5aa84a4f6bba7a57848c83b83fd GIT binary patch literal 10197 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 K)hq9y!~P4?%0*!S literal 0 HcmV?d00001 diff --git a/test/visual/golden/group-layout/clip-path3.png b/test/visual/golden/group-layout/clip-path3.png new file mode 100644 index 0000000000000000000000000000000000000000..46bd0fae24390bc2a5f87daf9ce5654ed325355d GIT binary patch literal 8033 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 literal 0 HcmV?d00001 diff --git a/test/visual/golden/group-layout/fit-content.png b/test/visual/golden/group-layout/fit-content.png new file mode 100644 index 0000000000000000000000000000000000000000..2f76f72e3114f480997e9f0907a61409c9f0995c GIT binary patch literal 6127 zcmd^D_g9lkm`*_u=^`x@QJOTR6X|L|N`NaxKm|e#(p%^PxrhV-DN;gJ5yViWNpC>| zp(9;77etiaq=m8{_w1gtyMMs`vM0%WIq%H-&NFX$X1thMPtydWl!xU~UVxg-I$ zWMuhSt6i=>WhkI3r{gEBP~T5NrF26*WWxV z&c7w}c%K)pTA{9$aRxMQ`Ae5wG3PsIXs_^I*f=5v>0D2t9*&msby8PoysNHIs1$Um z7FNqs%ViFl(h`h|&H(o~OJstS!IR*j;LC(Of(_&W%}zKSHYU6%TqOAOG&y`$F_lFe zq6}()G(aE!`Bf$OC&ttqwh%h!Iwv@H^xUfv@|_8e`?!~6^4Dx(S?UT|%T)d=@iuAv zOfOtOJ5vSZenE}F_2Me^@s-Ud_r+LQJ2bed`)I1cdyt#%Q)a?n5o{AV#Gk}YqK}S1 z(iJ2>R1|7(4OTv@>+=|q-b=$M44W||TMHUJ5>CMSaBphmy8Y8fD!+nJ_5_vDc^Xgr zl?Qf&aB&)f?t@FILQ@oybL!{=hE!r3hF%Q%(_z=J@5|i~88G35i6G7Zm4AXRqBwmd`4U1lUOs~;e<*%6XbD^HWc;ktqBG zURG^UOInq50Lnhq=H&1l?R-awXuh}+4L`r*@04L+b;<4qyy2i)Aq>cfkglQoyLuRW zar0S%k}Nja;DvEbTAN!y{#}A9u%;;*l~kpR;4-QM<7NK5K^bgvpCtkM*Xw_QbW<5A z#eZPb&<|11^8;X13SvOc^OS2Uo!F$!vf#fYj0TfRG6lkqw(6D{VFjzYuaM*v>54A~&*@!v^?)y-mH$3AYRC^5%D4Yy~ zKX!H-)9|OH0$ro6FLo=;F9baJFQCk2Hp~1}L@g5K8i5UPiW}fAa5Iw;S~$UI0)PrZ zEu2qw?woM~lo_O)58#qN9~f_(kym}W%nh()1o&jo7Zo*phrV|FKi&JZ=_UpQbssy| zMngnP6!U>#tmXV?Fsj(fo&v!b{y=0WMEn~qX9^H44c;tZz-7Qqeo|#2zytc7MJzVB@c8AXK0`rQsQ5n&8L+^iJ%>AhrqdJopEe+&37~Z7QvP!j@ zrgV|u4b-DIkCY^aS$;=lF2I$NAlye~B$^4jpU0T%s20odD(NMl)0ap*0%{|gAsmR? zzcsbk1L`BWm?B4g=i#5SfXc1_EPa;u^j+o`TjxY|0~wd%2oyFGI5?j%H+PR(EdiKr z8yF-{6_Vy;Znm#L)BG9`u>+!N*rwiGJ^@O=VX$=Jrp7eaM*xRxkAhD*%w;kBLW-~9 z1}o>~&A_TH>3T6MM)l=H`kmwL08M~<9s%6b?ni7R!U#~&bPfmyq*MoBHD%$1N8{AB zR|0g-`75935zcD_*iE9%^HZSyL~WI>lt5Ad-y}PW6T$!{{j9L1XWtJHV1=TmkOE%s z!~sGm83}&_5|M%ram!ZS6>dPyV!8;jd*6w}YJ?DW)v_tl!pYQXllgpM^(Y(}ZI087bdOa3Dnd zWpRRax>bk!b?F#!)!M}YbsuGh@<+v}HK)q`W$^snJxaL^E3ST*URtwjk=RRgBSKI* zShte9a?ks(!&=d-uX)u2q2{k_qmAHBDt=zn|JB%$^`P#@%r1 z6@fcAmY=5SOS*sL>7P&+V_#d3#?a;&<$%=gKcU#-IIo@3d&R}y3-4!*{sFOlB@U}O zv*{8@nd1YtbxPc%50DnXMxV09FWpO?g|>h~8J|EK!w)pQdAzdYMQ}_mqvq>6%th3b z8@S(2qcm!94rR^97mR52u!>M7))qM}S8xaeylm@Co`m@}t7qcjKn(sMp(|Ppa%EbD z@X)HH$-(}_1G_}?ozG7jvp?2%1uIGiuh+$$r9pEHP0*N?pxhhgO;Wcp@K;UWqa-=A z>33lBM&RXAIfs6+VqN&Bt(s{bqXO{N3(bgfI!WG3S*CM=&JUv&zOOtpFN&Hp3Vzy^ zLz}nje!<;i%8JmdBo4g~J?kUQ4n~qLMirl{bwiGQQ?EZrPKEQpFV7wre&~vDEN5h-t}v zfBVrK6Z68ooYj96Xddvsvg6&eVa^YmU5zx=-H9$b1yA7{&Ky(SZcIKORlW5hx`^hJ z+Rv@nUu-+Kh}KH3!Irt=j<;0r#J)WfFS}mG)%HoK;!15%xyn7?o8Ob8EeAaDwa5)* zs%mHrGurPK$Vzi1=-7&v+(rsM<{wC{@>;lUIuT_RoNz#|f_(Da)os4^vl8}^uCL{W zeJOkYiOxb@U@)^Pr?RNg-u9cN#)ogF-A5(+Dd*H3atHsWAuKcov;VG+`8080pvAv( z_1EkhFq+wny@zA8d~)X13|q4@))1V=JguAtF=H{4no2yNZPw?Qv_SYiRZGOP+o{?n zO#C4w-wcMlc{(-Q*A&b5rol-~tpH;E_7K_#lRi4LCcC05$X-?pRZGxP!9L-zu>e=@ zT<$$?-DajHKhaIv+?5SK1f?^MR36B(sb*_ku$UuDACh&$&aQ;PRnIreIS8+;fG5sg*o$ns9 z-`|#(q{za3{w6Oa((gH`RAZ<;`cakAE7?0}H{a(l%*hcTa z8(Z8bX{{EaJ&o5-(J}|x?%W&BTT}ngH&4SI1y*`HPy_JtlMm(VVeyqS_T4ib+X2+2 zYuf=TDXkAJizi36PW)ObqTKkaq>))(JUSkey21;-b0FXVNtOH3kyIc1sxXWlj)M=m zI`C}@&w0&_AySYt)ucw0i`?k=8;7E!7f#PYPVXXFD%rPU)*j9A%=hf;)%{V^LUPGk zSPS;ylW^ZsZ>phrk9jPZs0O`rp<);+U`H zfgBsJH!nsnHyH3;>l(bTR6G<@p=c{KU->QWH*uxmi6U@Xa&v|9XoiTU*ld$7#0ul= z%>AfTNrsy&dQO&UCs(B5+d6Yn6E#f5WG9d0d{SEHbRD_Ie)piKbNUQAPj`q&EeVA~ z%qzVpVBy)ZP`*u_T&pnmOpKp{FZE(9C%@9vK?^Lku!!}F@+EP zSTaYbJQ_IJE+-#nvhd0%u~OSklCmHvzHi0H}?oY?2m%W+wOhkQB<*(^U>K3?TT zPaCkaq9>SJZYb$1Z*Ahw!pIer9NVi$i_@{84_b=a77T~7)z8Bas%Ws&^ zIMdug3Z&k)8?{JZU+jPN22b2+uTL6qfA2qd8;cYO8jCZpY*P~5duROtdNwkD%8h$4 zs-9Pux%z#*CU6B5%<3kj7ajPcdDyne*y*%N=3&c!O)dW#Juq#!U59|wiThg@Fi`?N zbxyDE(fPOvEPY{LLH;=SG3j1GtKw1I;8U~D4;vB@Xj4N*(!=AO`pvaJzwd=M-`gL# z8^uaXl?z+GkhQhunbmz-$F9D(5%+CvtqJ$M5fBUxq8o^)}92bn^e4vIlbjk={_?< zjN)o~X?xn^gFmnxuZ~%xUvl^O)^=oU!^zJjiF-w=4y`rmvVSEK8tHrp`pZg>H`F?81AWs<{M9_rCY)w+~^G zdK6ivZfs0yxcw?~C`7ZfE_PO6nS;c&vpW9{BWZe%qFFb#;&(hKyHE-tnI36{)>&#J zr2=8nj&>?1V!?-eNn!3=0m^F=HiLc%=Vx)_x=FpD#*nY-a8Jc+cdRr`P&`-q6tpA}Y$y})A=_UVAn(HOOBOk7xznW&c zPR(y!0SM#pR(rW|@{dLCsyEIJ4=YEw^x3%{$k!_jU>dnkZUi5)<~2+dL#0KR<9*I* z^!N!gH7JN^)A8F1O}8=UR71J!KJOhjHoJn_+Kj#nAu1apKiyzn)(dCQaF5F;6m^}R zzk_aU2_5!JR?BAoWLo&=^HuFs7TNT>$GK1a84i!7c{jRmgdI;bxz~`lq%vsoEOl4+ z)e!Qt1VqHf9ilkyQ%gy^lt%qC{KpUrS5t;coAte07S+xhhKP|-MW6k}79?3Yravv) z<=3YpXM}p!%xJ8_H|x8x7_Jo+lw!v0>lmB`t^2z=w4J*8SIgfc#hEvotaJLb&_{wX z<1TMj=L400=2YrM{aNqGK>Mi*##rdjrnS~{si<@wY;>a*ruHT!T^v0hN3E%(wJQ6p zv`9y^*^P4b&9PeabR4C7&3E;CE1|P6vlDIQSsh-DA`x-*aAEt+#iaGGh|~~^kY_RE ztNp08DWxG9xJtxp8Ms6Qhny`w^D(+a%eBG5)15pm>NViD$^eLAv+wJLk;nHs5*0#nBThYMN?u{}u(Z_jU5tJ9@^x z)a=w}_7=)6s*(Zedp3>R!6DR?>hCknl2(+L^bcLi*N?c43(NaF$ST#36i$UeTB$zV z{o_7=QwEAE6#W9lV!UUUn`Xr$)a6GrGaJGx>6?>UBP6R8*MZeh$y~Yh*86r$pRr|Z zhkxnX&)aMD3#at(G(03Y7-gM7Y2E))5;s3 z=XJ6cS590o-ZeMah2ON_#Y^|Vm%YnP6_5Mr3Yqp%Eg)R$<9ZR*7YPjr40%-1vfNFrdP=uTFd% zeT})Bb?E0R;>pcJim&2I`AaFRc3~|#`UhCL{g>Z4tP zd~z9O=sLfq&Wg)};fKuSQjh~ld3TZg|D$<`@4W8l_bk6*`(+b8U5Q=?#wp%o&r)fg zp!7=TF;RjQC#Zj|s7z2N60h<9rYSDNrAq*}9B^Sb7Fpvc{{Qn^YeAiewKLEzqd7_6 TA{-0c;DdBD4G^F1qW=C54_IQI literal 0 HcmV?d00001 diff --git a/test/visual/golden/group-layout/fit-content2.png b/test/visual/golden/group-layout/fit-content2.png new file mode 100644 index 0000000000000000000000000000000000000000..574a3e47a5e25d7ad5460d3be5cd2e1293db78fa GIT binary patch literal 7758 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 literal 0 HcmV?d00001 diff --git a/test/visual/golden/group-layout/fit-content3.png b/test/visual/golden/group-layout/fit-content3.png new file mode 100644 index 0000000000000000000000000000000000000000..17f052c7edb1b07505ca836a093f3eb49b4b2c16 GIT binary patch literal 7137 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 Date: Mon, 4 Apr 2022 00:06:18 +0300 Subject: [PATCH 03/77] commit #7744 review https://github.com/fabricjs/fabric.js/pull/7744#pullrequestreview-922454881 --- src/shapes/group.class.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index af090dad895..9e9c47905d6 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -218,7 +218,6 @@ var directive = watch ? 'on' : 'off'; // make sure we listen only once watch && this._watchObject(false, object); - // TODO check what changed ischeck object[directive]('changed', this.__objectMonitor); object[directive]('modified', this.__objectMonitor); object[directive]('selected', this.__objectSelectionTracker); @@ -253,9 +252,10 @@ object.setCoords(); object._set('group', this); object._set('canvas', this.canvas); - this._watchObject(true, object); + this.interactive && this._watchObject(true, object); var activeObject = this.canvas && this.canvas.getActiveObject && this.canvas.getActiveObject(); // if we are adding the activeObject in a group + // TODO migrate back to isDescendantOf if (activeObject && (activeObject === object || (object.contains && object.contains(activeObject)))) { this._activeObjects.push(object); } From 2708029c5e0c85ada98d1b67c747ccaad2c522b5 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 10:28:53 +0300 Subject: [PATCH 04/77] patch2 --- src/mixins/animation.mixin.js | 8 +- src/mixins/object_ancestry.mixin.js | 68 +++++++++++++++- src/mixins/object_geometry.mixin.js | 121 ++++++++++++++++++++++++++-- src/mixins/object_stacking.mixin.js | 43 ++++++++++ 4 files changed, 227 insertions(+), 13 deletions(-) diff --git a/src/mixins/animation.mixin.js b/src/mixins/animation.mixin.js index 28d84661f36..4890a78094c 100644 --- a/src/mixins/animation.mixin.js +++ b/src/mixins/animation.mixin.js @@ -25,11 +25,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, - startValue: object.left, + startValue: object.getX(), endValue: this.getCenterPoint().x, duration: this.FX_DURATION, onChange: function(value) { - object.set('left', value); + object.setX(value); _this.requestRenderAll(); onChange(); }, @@ -58,11 +58,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, - startValue: object.top, + startValue: object.getY(), endValue: this.getCenterPoint().y, duration: this.FX_DURATION, onChange: function(value) { - object.set('top', value); + object.setY(value); _this.requestRenderAll(); onChange(); }, diff --git a/src/mixins/object_ancestry.mixin.js b/src/mixins/object_ancestry.mixin.js index 594da24a03d..31b93e015d4 100644 --- a/src/mixins/object_ancestry.mixin.js +++ b/src/mixins/object_ancestry.mixin.js @@ -1,10 +1,31 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** - * - * @param {boolean} [strict] returns only ancestors that are objects (without canvas) - * @returns {(fabric.Object | fabric.StaticCanvas)[]} ancestors from bottom to top - */ + * Checks if object is decendant of target + * Should be used instead of @link {fabric.Collection.contains} for performance reasons + * @param {fabric.Object|fabric.StaticCanvas} target + * @returns {boolean} + */ + isDescendantOf: function (target) { + var parent = this.group || this.canvas; + while (parent) { + if (target === parent) { + return true; + } + else if (parent instanceof fabric.StaticCanvas) { + // happens after all parents were traversed through without a match + return false; + } + parent = parent.group || parent.canvas; + } + return false; + }, + + /** + * + * @param {boolean} [strict] returns only ancestors that are objects (without canvas) + * @returns {(fabric.Object | fabric.StaticCanvas)[]} ancestors from bottom to top + */ getAncestors: function (strict) { var ancestors = []; var parent = this.group || (!strict ? this.canvas : undefined); @@ -13,5 +34,44 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot parent = parent.group || (!strict ? parent.canvas : undefined); } return ancestors; + }, + + /** + * + * @param {fabric.Object} other + * @returns {{ index: number, otherIndex: number, ancestors: fabric.Object[] }} ancestors may include the passed objects if one is an ancestor of the other resulting in index of -1 + */ + findCommonAncestors: function (other) { + if (this === other) { + return true; + } + else if (!other) { + return false; + } + var ancestors = this.getAncestors(); + ancestors.unshift(this); + var otherAncestors = other.getAncestors(); + otherAncestors.unshift(other); + for (var i = 0, ancestor; i < ancestors.length; i++) { + ancestor = ancestors[i]; + for (var j = 0; j < otherAncestors.length; j++) { + if (ancestor === otherAncestors[j] && !(ancestor instanceof fabric.StaticCanvas)) { + return { + index: i - 1, + otherIndex: j - 1, + ancestors: ancestors.slice(i) + }; + } + } + } + }, + + /** + * + * @param {fabric.Object} other + * @returns {boolean} + */ + hasCommonAncestors: function (other) { + return !!this.findCommonAncestors(other); } }); diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index 43cce0216fa..7c81d9255e2 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -66,6 +66,107 @@ */ controls: { }, + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane + */ + getX: function () { + return this.getXY().x; + }, + + /** + * @param {number} value x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane + */ + setX: function (value) { + this.setXY(this.getXY().setX(value)); + }, + + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#getX} + */ + getRelativeX: function () { + return this.left; + }, + + /** + * @param {number} value x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ + * if parent is canvas then this method is identical to {@link fabric.Object#setX} + */ + setRelativeX: function (value) { + this.left = value; + }, + + /** + * @returns {number} y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane + */ + getY: function () { + return this.getXY().y; + }, + + /** + * @param {number} value y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane + */ + setY: function (value) { + this.setXY(this.getXY().setY(value)); + }, + + /** + * @returns {number} y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#getY} + */ + getRelativeY: function () { + return this.top; + }, + + /** + * @param {number} value y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#setY} + */ + setRelativeY: function (value) { + this.top = value; + }, + + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in canvas coordinate plane + */ + getXY: function () { + var relativePosition = this.getRelativeXY(); + return this.group ? + fabric.util.transformPoint(relativePosition, this.group.calcTransformMatrix()) : + relativePosition; + }, + + /** + * @param {fabric.Point} point position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in canvas coordinate plane + * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' + * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' + */ + setXY: function (point, originX, originY) { + if (this.group) { + point = fabric.util.transformPoint( + point, + fabric.util.invertTransform(this.group.calcTransformMatrix()) + ); + } + this.setRelativeXY(point, originX, originY); + }, + + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane + */ + getRelativeXY: function () { + return new fabric.Point(this.left, this.top); + }, + + /** + * @param {fabric.Point} point position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane + * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' + * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' + */ + setRelativeXY: function (point, originX, originY) { + this.setPositionByOrigin(point, originX || this.originX, originY || this.originY); + }, + /** * return correct set of coordinates for intersection * this will return either aCoords or lineCoords. @@ -88,8 +189,15 @@ * The coords are returned in an array. * @return {Array} [tl, tr, br, bl] of points */ - getCoords: function(absolute, calculate) { - return arrayFromCoords(this._getCoords(absolute, calculate)); + getCoords: function (absolute, calculate) { + var coords = arrayFromCoords(this._getCoords(absolute, calculate)); + if (this.group) { + var t = this.group.calcTransformMatrix(); + return coords.map(function (p) { + return util.transformPoint(p, t); + }); + } + return coords; }, /** @@ -630,9 +738,12 @@ scaleY: this.scaleY, skewX: this.skewX, skewY: this.skewY, + width: this.width, + height: this.height, + strokeWidth: this.strokeWidth }, options || {}); // stroke is applied before/after transformations are applied according to `strokeUniform` - var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = this.strokeWidth; + var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = options.strokeWidth; if (this.strokeUniform) { preScalingStrokeValue = 0; postScalingStrokeValue = strokeWidth; @@ -641,8 +752,8 @@ preScalingStrokeValue = strokeWidth; postScalingStrokeValue = 0; } - var dimX = this.width + preScalingStrokeValue, - dimY = this.height + preScalingStrokeValue, + var dimX = options.width + preScalingStrokeValue, + dimY = options.height + preScalingStrokeValue, finalDimensions, noSkew = options.skewX === 0 && options.skewY === 0; if (noSkew) { diff --git a/src/mixins/object_stacking.mixin.js b/src/mixins/object_stacking.mixin.js index 8c8e87d5efd..f2fcdaee966 100644 --- a/src/mixins/object_stacking.mixin.js +++ b/src/mixins/object_stacking.mixin.js @@ -76,5 +76,48 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this.canvas.moveTo(this, index); } return this; + }, + + /** + * + * @param {fabric.Object} other object to compare against + * @returns {boolean | undefined} if objects do not share a common ancestor or they are strictly equal it is impossible to determine which is in front of the other; in such cases the function returns `undefined` + */ + isInFrontOf: function (other) { + if (this === other) { + return undefined; + } + var ancestors = this.getAncestors().reverse().concat(this); + var otherAncestors = other.getAncestors().reverse().concat(other); + var i, j, found = false; + // find the common ancestor + for (i = 0; i < ancestors.length; i++) { + for (j = 0; j < otherAncestors.length; j++) { + if (ancestors[i] === otherAncestors[j]) { + found = true; + break; + } + } + if (found) { + break; + } + } + if (!found) { + return undefined; + } + // compare trees from the common ancestor down + var tree = ancestors.slice(i), + otherTree = otherAncestors.slice(j), + a, b, parent; + for (i = 1; i < Math.min(tree.length, otherTree.length); i++) { + a = tree[i]; + b = otherTree[i]; + if (a !== b) { + parent = tree[i - 1]; + return parent._objects.indexOf(a) > parent._objects.indexOf(b); + } + } + // happens if a is ancestor of b or vice versa + return tree.length > otherTree.length; } }); From 02a8ffa986eddfbe967b9933ccd0aa0546a6967e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 10:28:58 +0300 Subject: [PATCH 05/77] Update spray_brush.class.js --- src/brushes/spray_brush.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/brushes/spray_brush.class.js b/src/brushes/spray_brush.class.js index 50202a4131b..2b8d733c5e4 100644 --- a/src/brushes/spray_brush.class.js +++ b/src/brushes/spray_brush.class.js @@ -112,7 +112,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric rects = this._getOptimizedRects(rects); } - var group = new fabric.Group(rects); + var group = new fabric.Group(rects, { objectCaching: true, layout: 'fixed', subTargetCheck: false }); this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); this.canvas.fire('before:path:created', { path: group }); this.canvas.add(group); From 0d3690f932df0173684ae20bd5939a18c08e19fe Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 10:29:14 +0300 Subject: [PATCH 06/77] enable text editing under group --- src/mixins/itext_click_behavior.mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js index eab105f4e88..3a9a8543032 100644 --- a/src/mixins/itext_click_behavior.mixin.js +++ b/src/mixins/itext_click_behavior.mixin.js @@ -152,7 +152,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ mouseUpHandler: function(options) { this.__isMousedown = false; - if (!this.editable || this.group || + if (!this.editable || (options.transform && options.transform.actionPerformed) || (options.e.button && options.e.button !== 1)) { return; From ff0346a12092c8b5a31dc4f61948fa79f6ea21ae Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 11:10:07 +0300 Subject: [PATCH 07/77] migrate back to isDescendantOf --- src/shapes/group.class.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 9e9c47905d6..f18b221701b 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -255,8 +255,7 @@ this.interactive && this._watchObject(true, object); var activeObject = this.canvas && this.canvas.getActiveObject && this.canvas.getActiveObject(); // if we are adding the activeObject in a group - // TODO migrate back to isDescendantOf - if (activeObject && (activeObject === object || (object.contains && object.contains(activeObject)))) { + if (activeObject && (activeObject === object || object.isDescendantOf(activeObject))) { this._activeObjects.push(object); } }, From 812f02af01d788fa4aba56388f22c9306515d5fb Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 11:21:29 +0300 Subject: [PATCH 08/77] render bg --- src/shapes/group.class.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index f18b221701b..6b52e54ec33 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -384,6 +384,7 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ drawObject: function(ctx) { + this._renderBackground(ctx); for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i].render(ctx); } From 053946cab05ec66fb5043b938b4a87ea16886c6c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 11:22:59 +0300 Subject: [PATCH 09/77] fix(): clear canvas from entire object tree in case object is a collection --- src/static_canvas.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 393ec40a257..1f802461578 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -620,7 +620,7 @@ _onObjectRemoved: function(obj) { this.fire('object:removed', { target: obj }); obj.fire('removed'); - delete obj.canvas; + obj._set('canvas', undefined); }, /** From db76c441a5faa19ddb0416336d4aeb931dda6727 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 11:24:25 +0300 Subject: [PATCH 10/77] fix(): center object in case object is nested --- src/static_canvas.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 1f802461578..17db115ca8a 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -959,7 +959,7 @@ * @chainable */ _centerObject: function(object, center) { - object.setPositionByOrigin(center, 'center', 'center'); + object.setXY(center, 'center', 'center'); object.setCoords(); this.renderOnAddRemove && this.requestRenderAll(); return this; From 04aad5bf2636d9b8d82154a7e4debeafa5eab27e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 11:28:34 +0300 Subject: [PATCH 11/77] Update eraser_brush.mixin.js --- src/mixins/eraser_brush.mixin.js | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/mixins/eraser_brush.mixin.js b/src/mixins/eraser_brush.mixin.js index 4400e5aca41..a8647165ff0 100644 --- a/src/mixins/eraser_brush.mixin.js +++ b/src/mixins/eraser_brush.mixin.js @@ -125,7 +125,6 @@ /* _TO_SVG_END_ */ }); - var __restoreObjectsState = fabric.Group.prototype._restoreObjectsState; fabric.util.object.extend(fabric.Group.prototype, { /** * @private @@ -181,15 +180,6 @@ }); } }); - }, - - /** - * 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); } }); @@ -217,14 +207,6 @@ */ 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 @@ -232,8 +214,14 @@ * @override * @private */ - _getBounds: function () { - // noop + layout: 'fixed', + + 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); }, /* _TO_SVG_START_ */ From 8c1c535c8d3c80d95212e592f5ca13ca69fa9931 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 11:35:59 +0300 Subject: [PATCH 12/77] Update canvas.class.js --- src/canvas.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canvas.class.js b/src/canvas.class.js index e95b2bbcfa2..0e4c1afa736 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -892,7 +892,7 @@ */ searchPossibleTargets: function (objects, pointer) { var target = this._searchPossibleTargets(objects, pointer); - return target; + return /*this.targets[0] ||*/ target; }, /** From 3fd4af2377d5d6e143cafe0cd680f1edf436760a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 11:39:43 +0300 Subject: [PATCH 13/77] Update canvas_events.mixin.js --- src/mixins/canvas_events.mixin.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index fb7c63f2b74..11502c85698 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -900,13 +900,17 @@ */ _transformObject: function(e) { var pointer = this.getPointer(e), - transform = this._currentTransform; - + transform = this._currentTransform, + target = transform.target, + // transform pointer to target's containing coordinate plane + // both pointer and object should agree on every point + localPointer = fabric.util.sendPointToPlane(pointer, null, target.group); + transform.reset = false; transform.shiftKey = e.shiftKey; transform.altKey = e[this.centeredKey]; - this._performTransformAction(e, transform, pointer); + this._performTransformAction(e, transform, localPointer); transform.actionPerformed && this.requestRenderAll(); }, From 58e9be20e2021c4776801b4268e180d7b55ecbfd Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 12:08:49 +0300 Subject: [PATCH 14/77] lint --- src/mixins/canvas_events.mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index 11502c85698..0b76aad409f 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -905,7 +905,7 @@ // transform pointer to target's containing coordinate plane // both pointer and object should agree on every point localPointer = fabric.util.sendPointToPlane(pointer, null, target.group); - + transform.reset = false; transform.shiftKey = e.shiftKey; transform.altKey = e[this.centeredKey]; From c2ef2f581e2bd84dfecc585e65a9f3f070a8fade Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 13:55:41 +0300 Subject: [PATCH 15/77] safeguard from adding group to itself --- src/shapes/group.class.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 6b52e54ec33..e8e38e5fc80 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -230,7 +230,13 @@ * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane */ enterGroup: function (object, removeParentTransform) { - if (object.group) { + if (object === this) { + /* _DEV_MODE_START_ */ + console.warn('fabric.Group: trying to add group to itself, this call has no effect'); + /* _DEV_MODE_END_ */ + return; + } + else if (object.group) { if (object.group === this) { /* _DEV_MODE_START_ */ console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); From f0145b51edd22ae5229a5806a16918a8e728167a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 13:59:16 +0300 Subject: [PATCH 16/77] Update object_ancestry.mixin.js --- src/mixins/object_ancestry.mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/object_ancestry.mixin.js b/src/mixins/object_ancestry.mixin.js index 31b93e015d4..7481f7f806b 100644 --- a/src/mixins/object_ancestry.mixin.js +++ b/src/mixins/object_ancestry.mixin.js @@ -39,7 +39,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * * @param {fabric.Object} other - * @returns {{ index: number, otherIndex: number, ancestors: fabric.Object[] }} ancestors may include the passed objects if one is an ancestor of the other resulting in index of -1 + * @returns {{ index: number, otherIndex: number, ancestors: fabric.Object[] } | boolean} ancestors may include the passed objects if one is an ancestor of the other resulting in index of -1 */ findCommonAncestors: function (other) { if (this === other) { From cb20b8107342bb9863131f3f21da6afd5fcb33b0 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 14:18:12 +0300 Subject: [PATCH 17/77] patch tests from v6! --- test/unit/itext_click_behaviour.js | 28 +++-- test/unit/object.js | 171 +++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 7 deletions(-) diff --git a/test/unit/itext_click_behaviour.js b/test/unit/itext_click_behaviour.js index 7d43901c7d3..2cc69821764 100644 --- a/test/unit/itext_click_behaviour.js +++ b/test/unit/itext_click_behaviour.js @@ -167,20 +167,34 @@ iText.selected = true; iText.__lastSelected = true; iText.mouseUpHandler({ e: {} }); - assert.equal(iText.isEditing, false, 'iText did not enter editing'); + assert.equal(iText.isEditing, false, 'iText should not enter editing'); iText.exitEditing(); }); - QUnit.test('_mouseUpHandler on a selected text in a group DOES NOT enter edit', function(assert) { + QUnit.test('_mouseUpHandler on a selected text in a group does NOT enter editing', function(assert) { var iText = new fabric.IText('test'); iText.initDelayedCursor = function() {}; iText.renderCursorOrSelection = function() {}; assert.equal(iText.isEditing, false, 'iText not editing'); - iText.canvas = canvas; + var group = new fabric.Group([iText], { subTargetCheck: false }); + canvas.add(group); iText.selected = true; iText.__lastSelected = true; - iText.group = true; - iText.mouseUpHandler({ e: {} }); - assert.equal(iText.isEditing, false, 'iText did not entered editing'); + canvas.__onMouseUp({ clientX: 1, clientY: 1 }); + assert.equal(canvas._target, group, 'group should be found as target'); + assert.equal(iText.isEditing, false, 'iText should not enter editing'); + iText.exitEditing(); + }); + QUnit.test('_mouseUpHandler on a text in a group does enter editing', function (assert) { + var iText = new fabric.IText('test'); + iText.initDelayedCursor = function () { }; + iText.renderCursorOrSelection = function () { }; + assert.equal(iText.isEditing, false, 'iText not editing'); + var group = new fabric.Group([iText], { subTargetCheck: true }); + canvas.add(group); + iText.selected = true; + iText.__lastSelected = true; + canvas.__onMouseUp({ clientX: 1, clientY: 1 }); + assert.equal(iText.isEditing, true, 'iText should enter editing'); iText.exitEditing(); }); QUnit.test('_mouseUpHandler on a corner of selected text DOES NOT enter edit', function(assert) { @@ -193,7 +207,7 @@ iText.__lastSelected = true; iText.__corner = 'mt'; iText.mouseUpHandler({ e: {} }); - assert.equal(iText.isEditing, false, 'iText did not entered editing'); + assert.equal(iText.isEditing, false, 'iText should not enter editing'); iText.exitEditing(); canvas.renderAll(); }); diff --git a/test/unit/object.js b/test/unit/object.js index 54807171622..7850a03ae78 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -803,6 +803,177 @@ assert.equal(object.moveTo(), object, 'should be chainable'); }); + QUnit.test('isDescendantOf', function (assert) { + var object = new fabric.Object(); + var parent = new fabric.Object(); + assert.ok(typeof object.isDescendantOf === 'function'); + parent.canvas = canvas; + object.group = parent; + assert.ok(object.isDescendantOf(parent)); + object.group = { + group: parent + }; + assert.ok(object.isDescendantOf(parent)); + assert.ok(object.isDescendantOf(canvas)); + object.group = undefined; + assert.ok(object.isDescendantOf(parent) === false); + assert.ok(object.isDescendantOf(canvas) === false); + object.canvas = canvas; + assert.ok(object.isDescendantOf(canvas)); + assert.ok(object.isDescendantOf(object) === false); + }); + + QUnit.test('getAncestors', function (assert) { + var object = new fabric.Object(); + var parent = new fabric.Object(); + var other = new fabric.Object(); + assert.ok(typeof object.getAncestors === 'function'); + assert.deepEqual(object.getAncestors(), []); + object.group = parent; + assert.deepEqual(object.getAncestors(), [parent]); + parent.canvas = canvas; + assert.deepEqual(object.getAncestors(), [parent, canvas]); + parent.group = other; + assert.deepEqual(object.getAncestors(), [parent, other]); + other.canvas = canvas; + assert.deepEqual(object.getAncestors(), [parent, other, canvas]); + delete object.group; + assert.deepEqual(object.getAncestors(), []); + }); + + QUnit.assert.isInFrontOf = function (object, other, expected) { + var actual = object.isInFrontOf(other); + this.pushResult({ + expected: expected, + actual: actual, + result: actual === expected, + message: `'${expected ? object.id : other.id}' should be in front of '${expected ? other.id : object.id}'` + }); + if (actual === expected && typeof expected === 'boolean') { + var actual2 = other.isInFrontOf(object); + this.pushResult({ + expected: !expected, + actual: actual2, + result: actual2 === !expected, + message: `should match opposite check between '${object.id}' and '${other.id}'` + }); + } + }; + + QUnit.test('isInFrontOf', function (assert) { + var Object = fabric.util.createClass(fabric.Object, { + toJSON: function () { + return { + id: this.id, + objects: this._objects?.map(o => o.id), + parent: this.parent?.id, + canvas: this.canvas?.id + } + }, + toString: function () { + return JSON.stringify(this.toJSON(), null, '\t'); + } + }) + var object = new Object({ id: 'object' }); + var other = new Object({ id: 'other' }); + var Collection = fabric.util.createClass(Object, fabric.Collection, { + initialize: function () { + this._objects = []; + }, + add: function () { + fabric.Collection.add.call(this, arguments, this._onObjectAdded); + }, + insertAt: function (objects, index, nonSplicing) { + fabric.Collection.insertAt.call(this, objects, index, nonSplicing, this._onObjectAdded); + }, + remove: function () { + fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); + }, + _onObjectAdded: function (object) { + object.group = this; + }, + _onObjectRemoved: function (object) { + delete object.group; + }, + removeAll: function () { + this.remove.apply(this, this._objects); + } + }); + var a = new Collection({ id: 'a' }); + var b = new Collection({ id: 'b' }); + var c = new Collection({ id: 'c' }); + var canvas = fabric.util.object.extend(new Collection({ id: 'canvas' }), { + _onObjectAdded: function (object) { + object.canvas = this; + }, + _onObjectRemoved: function (object) { + delete object.canvas; + }, + }); + assert.ok(typeof object.isInFrontOf === 'function'); + assert.ok(Array.isArray(a._objects)); + assert.ok(a._objects !== b._objects); + // same object + assert.isInFrontOf(object, object, undefined); + // foreign objects + assert.isInFrontOf(object, other, undefined); + // same level + a.add(object, other); + assert.isInFrontOf(object, other, false); + assert.isInFrontOf(object, a, true); + assert.isInFrontOf(other, a, true); + // different level + a.remove(object); + b.add(object); + a.add(b); + assert.isInFrontOf(object, b, true); + assert.isInFrontOf(b, a, true); + assert.isInFrontOf(object, other, true); + // with common ancestor + assert.equal(c.size(), 0, 'c should be empty'); + c.add(a); + assert.equal(c.size(), 1, 'c should contain a'); + assert.isInFrontOf(object, b, true); + assert.isInFrontOf(b, a, true); + assert.isInFrontOf(object, other, true); + assert.isInFrontOf(object, c, true); + assert.isInFrontOf(other, c, true); + assert.isInFrontOf(b, c, true); + assert.isInFrontOf(a, c, true); + // deeper asymmetrical + c.removeAll(); + assert.equal(c.size(), 0, 'c should be cleared'); + a.remove(other); + c.add(other, a); + assert.isInFrontOf(object, b, true); + assert.isInFrontOf(b, a, true); + assert.isInFrontOf(a, other, true); + assert.isInFrontOf(object, other, true); + assert.isInFrontOf(object, c, true); + assert.isInFrontOf(object, c, true); + assert.isInFrontOf(other, c, true); + assert.isInFrontOf(b, c, true); + assert.isInFrontOf(a, c, true); + // with canvas + a.removeAll(); + b.removeAll(); + c.removeAll(); + canvas.add(object, other); + assert.isInFrontOf(object, other, false); + assert.isInFrontOf(object, canvas, true); + assert.isInFrontOf(other, canvas, true); + // parent precedes canvas when checking ancestor + a.add(object); + assert.ok(object.canvas === canvas, 'object should have canvas set'); + assert.isInFrontOf(object, other, undefined); + canvas.insertAt(a, 0); + assert.isInFrontOf(object, other, false); + assert.isInFrontOf(a, other, false); + assert.isInFrontOf(a, canvas, true); + assert.isInFrontOf(object, canvas, true); + assert.isInFrontOf(other, canvas, true); + }); + QUnit.test('getTotalObjectScaling with zoom', function(assert) { var object = new fabric.Object({ scaleX: 3, scaleY: 2}); canvas.setZoom(3); From 347f80dac6a3bf9c70c7e6b73413df603322ff0f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 14:38:48 +0300 Subject: [PATCH 18/77] Update canvas_grouping.mixin.js --- src/mixins/canvas_grouping.mixin.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js index b59661a38ab..61e09ef7c4f 100644 --- a/src/mixins/canvas_grouping.mixin.js +++ b/src/mixins/canvas_grouping.mixin.js @@ -13,8 +13,12 @@ */ _shouldGroup: function(e, target) { var activeObject = this._activeObject; - return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && - (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); + return activeObject && this._isSelectionKeyPressed(e) + && target && target.selectable && this.selection + && (activeObject !== target || activeObject.type === 'activeSelection') + // make sure `activeObject` and `target` aren't ancestors of each other + && !target.isDescendantOf(activeObject) && !activeObject.isDescendantOf(target) + && !target.onSelect({ e: e }); }, /** @@ -80,17 +84,19 @@ this._fireSelectionEvents(currentActives, e); }, + /** * @private * @param {Object} target + * @returns {fabric.ActiveSelection} */ _createGroup: function(target) { - var objects = this._objects, - isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), - groupObjects = isActiveLower - ? [this._activeObject, target] - : [target, this._activeObject]; - this._activeObject.isEditing && this._activeObject.exitEditing(); + var activeObject = this._activeObject; + var groupObjects = target.isInFrontOf(activeObject) ? + [activeObject, target] : + [target, activeObject]; + activeObject.isEditing && activeObject.exitEditing(); + // handle case: target is nested return new fabric.ActiveSelection(groupObjects, { canvas: this }); From a56d55e0742d58dc84bb56e801b6894188a5a871 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 14:40:41 +0300 Subject: [PATCH 19/77] Update canvas.class.js --- src/canvas.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canvas.class.js b/src/canvas.class.js index 0e4c1afa736..e86164843a3 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -892,7 +892,7 @@ */ searchPossibleTargets: function (objects, pointer) { var target = this._searchPossibleTargets(objects, pointer); - return /*this.targets[0] ||*/ target; + return this.targets[0] || target; }, /** From 22045b6214565c8f23b7246cd964481aa8cdfa5c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 12:01:22 +0300 Subject: [PATCH 20/77] create layer --- build.js | 1 + src/mixins/canvas_events.mixin.js | 3 +- src/shapes/layer.class.js | 182 ++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/shapes/layer.class.js diff --git a/build.js b/build.js index 91fe3924799..43d454f869c 100644 --- a/build.js +++ b/build.js @@ -196,6 +196,7 @@ var filesToInclude = [ 'src/shapes/polygon.class.js', 'src/shapes/path.class.js', 'src/shapes/group.class.js', + 'src/shapes/layer.class.js', ifSpecifiedInclude('interaction', 'src/shapes/active_selection.class.js'), 'src/shapes/image.class.js', diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index 0b76aad409f..31533c5d297 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -383,8 +383,9 @@ /** * @private */ - _onResize: function () { + _onResize: function (e) { this.calcOffset(); + this.fire('resize', { e: e }); }, /** diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js new file mode 100644 index 00000000000..3f8d90cef00 --- /dev/null +++ b/src/shapes/layer.class.js @@ -0,0 +1,182 @@ +(function (global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = {}); + + if (fabric.Layer) { + fabric.warn('fabric.Layer is already defined'); + return; + } + + /** + * Layer class + * @class fabric.Layer + * @extends fabric.Group + * @mixes fabric.Collection + * @see {@link fabric.Layer#initialize} for constructor definition + */ + fabric.Layer = fabric.util.createClass(fabric.Group, /** @lends fabric.Group.prototype */ { + + /** + * @default + * @type string + */ + type: 'layer', + + /** + * @override + * @default + */ + layout: 'auto', + + /** + * @override + * @default + */ + objectCaching: false, + + /** + * @override + * @default + */ + strokeWidth: 0, + + /** + * @override + * @default + */ + hasControls: false, + + /** + * @override + * @default + */ + hasBorders: false, + + /** + * @override + * @default + */ + lockMovementX: true, + + /** + * @override + * @default + */ + lockMovementY: true, + + /** + * we don't want to int with the layer, only with it's objects + * this makes group selection possible over a layer + * @override + * @default + */ + selectable: false, + + /** + * Constructor + * + * @param {fabric.Object[]} [objects] instance objects + * @param {Object} [options] Options object + * @return {fabric.Group} thisArg + */ + initialize: function (objects, options) { + this.callSuper('initialize', objects, options); + this.__canvasMonitor = this.__canvasMonitor.bind(this); + }, + + /** + * + * @param {string} key + * @param {*} value + */ + _set: function (key, value) { + var settingCanvas = key === 'canvas'; + if (settingCanvas) { + if (!value && this.canvas) { + // detach canvas resize handler + this.canvas.off('resize', this.__canvasMonitor); + } + else if (value && (!this.canvas || this.canvas !== value)) { + // attach canvas resize handler, make sure we listen to the resize event only once + this.canvas && this.canvas.off('resize', this.__canvasMonitor); + value.off('resize', this.__canvasMonitor); + value.on('resize', this.__canvasMonitor); + } + } + this.callSuper('_set', key, value); + // apply layout after canvas is set + if (settingCanvas) { + this._applyLayoutStrategy({ type: 'canvas' }); + } + }, + + /** + * we do not need to invalidate layout because layer fills the entire canvas + * @override + * @private + */ + __objectMonitor: function () { + // noop + }, + + /** + * @private + */ + __canvasMonitor: function () { + this._applyLayoutStrategy({ type: 'canvas_resize' }); + }, + + /** + * Override this method to customize layout + * @public + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {object} context object with data regarding what triggered the call + * @param {'initializion'|'canvas'|'canvas_resize'|'layout_change'} context.type + * @param {fabric.Object[]} context.path array of objects starting from the object that triggered the call to the current one + * @returns {Object} options object + */ + getLayoutStrategyResult: function (layoutDirective, objects, context) { // eslint-disable-line no-unused-vars + if ((context.type === 'canvas' || context.type === 'canvas_resize') && this.canvas && !this.group) { + return { + centerX: this.canvas.width / 2, + centerY: this.canvas.height / 2, + width: this.canvas.width, + height: this.canvas.height + }; + } + }, + + toString: function () { + return '#'; + }, + + dispose: function () { + this.canvas && this.canvas.off('resize', this.__canvasMonitor); + this.callSuper('dispose'); + } + + }); + + /** + * Returns fabric.Layer instance from an object representation + * @static + * @memberOf fabric.Layer + * @param {Object} object Object to create an instance from + * @returns {Promise} + */ + fabric.Layer.fromObject = function (object) { + var objects = object.objects || [], + options = fabric.util.object.clone(object, true); + delete options.objects; + return Promise.all([ + fabric.util.enlivenObjects(objects), + fabric.util.enlivenObjectEnlivables(options) + ]).then(function (enlivened) { + return new fabric.Layer(enlivened[0], Object.assign(options, enlivened[1]), true); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); From 724792aef795cc30458eda1a68876275a57d36c8 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 12:05:29 +0300 Subject: [PATCH 21/77] Update layer.class.js --- src/shapes/layer.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 3f8d90cef00..789829d1d4e 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -16,7 +16,7 @@ * @mixes fabric.Collection * @see {@link fabric.Layer#initialize} for constructor definition */ - fabric.Layer = fabric.util.createClass(fabric.Group, /** @lends fabric.Group.prototype */ { + fabric.Layer = fabric.util.createClass(fabric.Group, /** @lends fabric.Layer.prototype */ { /** * @default From 7163ad3428ca1a404ba6bbea9e92cea17e9c341c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 14:57:18 +0300 Subject: [PATCH 22/77] warn --- src/shapes/group.class.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index e8e38e5fc80..2ce5dcd17a2 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -236,6 +236,12 @@ /* _DEV_MODE_END_ */ return; } + else if (object.type === 'layer') { + /* _DEV_MODE_START_ */ + console.warn('fabric.Group: nesting layers under groups is not supported, this call has no effect'); + /* _DEV_MODE_END_ */ + return; + } else if (object.group) { if (object.group === this) { /* _DEV_MODE_START_ */ From 2a426e73111f212aecfab52bdc9032cac0373586 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 15:40:34 +0300 Subject: [PATCH 23/77] Update group_layout.js --- test/visual/group_layout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/visual/group_layout.js b/test/visual/group_layout.js index ebdae56cf34..18db90be644 100644 --- a/test/visual/group_layout.js +++ b/test/visual/group_layout.js @@ -334,5 +334,5 @@ }); } */ - // tests.forEach(visualTestLoop(QUnit)); + tests.forEach(visualTestLoop(QUnit)); })(); From 5e3fc1f1e3586d3e6f8eb709ce4a13f2e72863cd Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 15:54:26 +0300 Subject: [PATCH 24/77] Update group_layout.js --- test/visual/group_layout.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/visual/group_layout.js b/test/visual/group_layout.js index 18db90be644..e7a36a96b01 100644 --- a/test/visual/group_layout.js +++ b/test/visual/group_layout.js @@ -76,6 +76,27 @@ height: 300 }); + function fitContentLayoutRelative(canvas, callback) { + var g = createGroupForLayoutTests('fit-content layout', { + backgroundColor: 'blue' + }); + g.clone().then((clone) => { + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + }) + } + + tests.push({ + test: 'fit-content layout', + code: fitContentLayoutRelative, + golden: 'group-layout/fit-content.png', + newModule: 'Group Layout', + percentage: 0.06, + width: 400, + height: 300 + }); + function fitContentReLayout(canvas, callback) { var g = createGroupForLayoutTests('fit-content layout', { backgroundColor: 'blue' From 2b1f680a9c5d7dea8fbbab1f4a4ebba1e959c3d7 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 15:55:47 +0300 Subject: [PATCH 25/77] remove redundant logic --- src/shapes/group.class.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index e8e38e5fc80..bfc10358c6d 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -97,20 +97,9 @@ this.__objectSelectionDisposer = this.__objectSelectionMonitor.bind(this, false); this._firstLayoutDone = false; this.callSuper('initialize', options); - if (objectsRelativeToGroup) { - this.forEachObject(function (object) { - this.enterGroup(object, false); - }, this); - } - else { - // we need to preserve object's center point in relation to canvas and apply group's transform to it - var inv = invertTransform(this.calcTransformMatrix()); - this.forEachObject(function (object) { - this.enterGroup(object, false); - var center = transformPoint(object.getCenterPoint(), inv); - object.setPositionByOrigin(center, 'center', 'center'); - }, this); - } + this.forEachObject(function (object) { + this.enterGroup(object, false); + }, this); this._applyLayoutStrategy({ type: 'initialization', options: options, From 1ad5cc8a098f74c36160efa2217968ba5238eda1 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 16:05:38 +0300 Subject: [PATCH 26/77] Update canvas_grouping.mixin.js --- src/mixins/canvas_grouping.mixin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js index 61e09ef7c4f..761177f9af7 100644 --- a/src/mixins/canvas_grouping.mixin.js +++ b/src/mixins/canvas_grouping.mixin.js @@ -13,8 +13,8 @@ */ _shouldGroup: function(e, target) { var activeObject = this._activeObject; - return activeObject && this._isSelectionKeyPressed(e) - && target && target.selectable && this.selection + return !!activeObject && this._isSelectionKeyPressed(e) + && !!target && target.selectable && this.selection && (activeObject !== target || activeObject.type === 'activeSelection') // make sure `activeObject` and `target` aren't ancestors of each other && !target.isDescendantOf(activeObject) && !activeObject.isDescendantOf(target) From 11c828ff195fdaf5eafaeb0028fdea7ddba89640 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 16:12:02 +0300 Subject: [PATCH 27/77] fix(): calling sendPointToPlane --- src/mixins/canvas_events.mixin.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index 0b76aad409f..affeda50b46 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -904,7 +904,9 @@ target = transform.target, // transform pointer to target's containing coordinate plane // both pointer and object should agree on every point - localPointer = fabric.util.sendPointToPlane(pointer, null, target.group); + localPointer = target.group ? + fabric.util.sendPointToPlane(pointer, null, target.group.calcTransformMatrix()) : + pointer; transform.reset = false; transform.shiftKey = e.shiftKey; From b6fa3dc4d307e134f7427fec1b6d0afd90a0b630 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 17:14:20 +0300 Subject: [PATCH 28/77] Update group.class.js --- src/shapes/group.class.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index bfc10358c6d..a620a53e4ee 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -217,20 +217,21 @@ * @private * @param {fabric.Object} object * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + * @returns {boolean} true if object entered group */ enterGroup: function (object, removeParentTransform) { if (object === this) { /* _DEV_MODE_START_ */ console.warn('fabric.Group: trying to add group to itself, this call has no effect'); /* _DEV_MODE_END_ */ - return; + return false; } else if (object.group) { if (object.group === this) { /* _DEV_MODE_START_ */ console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); /* _DEV_MODE_END_ */ - return; + return false; } object.group.remove(object); } @@ -253,6 +254,7 @@ if (activeObject && (activeObject === object || object.isDescendantOf(activeObject))) { this._activeObjects.push(object); } + return true; }, /** From e9afc89a0f87f574a595221d2974fa1b3440cb1c Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 17:39:30 +0300 Subject: [PATCH 29/77] refactor `enterGroup` --- src/shapes/active_selection.class.js | 27 ++++++++++++++++ src/shapes/group.class.js | 48 ++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/shapes/active_selection.class.js b/src/shapes/active_selection.class.js index a2b648752d0..98c0238fade 100644 --- a/src/shapes/active_selection.class.js +++ b/src/shapes/active_selection.class.js @@ -52,6 +52,27 @@ this.setCoords(); }, + + /** + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + * @returns {boolean} true if object entered group + */ + enterGroup: function (object, removeParentTransform) { + if (!this.canEnter(object)) { + return false; + } + if (object.group) { + // save ref to group for later in order to return to it + var parent = object.group; + parent._exitGroup(object); + object.__owningGroup = parent; + } + this._enterGroup(object, removeParentTransform); + return true; + }, + /** * we want objects to retain their canvas ref when exiting instance * @private @@ -60,6 +81,12 @@ */ exitGroup: function (object, removeParentTransform) { this._exitGroup(object, removeParentTransform); + var parent = object.__owningGroup; + if (parent) { + // return to owning group + parent.enterGroup(object); + delete object.__owningGroup; + } }, /** diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index a620a53e4ee..bf592435890 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -214,29 +214,52 @@ }, /** + * Checks if object can enter group and logs relevant warnings * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane - * @returns {boolean} true if object entered group + * @param {fabric.Object} object + * @returns */ - enterGroup: function (object, removeParentTransform) { + canEnter: function (object) { if (object === this) { /* _DEV_MODE_START_ */ console.warn('fabric.Group: trying to add group to itself, this call has no effect'); /* _DEV_MODE_END_ */ return false; } - else if (object.group) { - if (object.group === this) { - /* _DEV_MODE_START_ */ - console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); - /* _DEV_MODE_END_ */ - return false; - } + else if (object.group && object.group === this) { + /* _DEV_MODE_START_ */ + console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); + /* _DEV_MODE_END_ */ + return false; + } + return true; + }, + + /** + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + * @returns {boolean} true if object entered group + */ + enterGroup: function (object, removeParentTransform) { + if (!this.canEnter(object)) { + return false; + } + if (object.group) { object.group.remove(object); } - // can be this converted to utils? + this._enterGroup(object, removeParentTransform); + return true; + }, + + /** + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + */ + _enterGroup: function (object, removeParentTransform) { if (removeParentTransform) { + // can this be converted to utils (sendObjectToPlane)? applyTransformToObject( object, multiplyTransformMatrices( @@ -254,7 +277,6 @@ if (activeObject && (activeObject === object || object.isDescendantOf(activeObject))) { this._activeObjects.push(object); } - return true; }, /** From d8f8251d0b18d8d1c95805e8e5a4122712ba3020 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 17:51:46 +0300 Subject: [PATCH 30/77] invalidate groups after active selection change --- src/shapes/active_selection.class.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/shapes/active_selection.class.js b/src/shapes/active_selection.class.js index 98c0238fade..eff9a1f8585 100644 --- a/src/shapes/active_selection.class.js +++ b/src/shapes/active_selection.class.js @@ -89,6 +89,30 @@ } }, + /** + * @private + * @param {'added'|'removed'} type + * @param {fabric.Object[]} targets + */ + _onAfterObjectsChange: function (type, targets) { + var groups = []; + targets.forEach(function (object) { + object.group && !groups.includes(object.group) && groups.push(object.group); + }); + if (type === 'removed') { + // invalidate groups' layout and mark as dirty + groups.forEach(function (group) { + group._onAfterObjectsChange('added', targets); + }); + } + else { + // mark groups as dirty + groups.forEach(function (group) { + group._set('dirty', true); + }); + } + }, + /** * If returns true, deselection is cancelled. * @since 2.0.0 From c58a27480084b7630554f6c241a0e1c403786ce5 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 18:14:01 +0300 Subject: [PATCH 31/77] _shouldSetNestedCoords --- src/shapes/active_selection.class.js | 6 ++++++ src/shapes/group.class.js | 11 +++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/shapes/active_selection.class.js b/src/shapes/active_selection.class.js index eff9a1f8585..89e24917fb5 100644 --- a/src/shapes/active_selection.class.js +++ b/src/shapes/active_selection.class.js @@ -52,6 +52,12 @@ this.setCoords(); }, + /** + * @private + */ + _shouldSetNestedCoords: function () { + return true; + }, /** * @private diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index bf592435890..e87a6922326 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -129,6 +129,13 @@ return this; }, + /** + * @private + */ + _shouldSetNestedCoords: function () { + return this.subTargetCheck; + }, + /** * Add objects * @param {...fabric.Object} objects @@ -268,7 +275,7 @@ ) ); } - object.setCoords(); + this._shouldSetNestedCoords() && object.setCoords(); object._set('group', this); object._set('canvas', this.canvas); this.interactive && this._watchObject(true, object); @@ -439,7 +446,7 @@ */ setCoords: function () { this.callSuper('setCoords'); - (this.subTargetCheck || this.type === 'activeSelection') && this.forEachObject(function (object) { + this._shouldSetNestedCoords() && this.forEachObject(function (object) { object.setCoords(); }); }, From c0f5571b9bea1b407a84ca7f828ee8d3656cb8e8 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 18:37:14 +0300 Subject: [PATCH 32/77] perf(): refrain from `contains` --- src/mixins/canvas_grouping.mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js index 761177f9af7..1738d7b10e9 100644 --- a/src/mixins/canvas_grouping.mixin.js +++ b/src/mixins/canvas_grouping.mixin.js @@ -54,7 +54,7 @@ _updateActiveSelection: function(target, e) { var activeSelection = this._activeObject, currentActiveObjects = activeSelection._objects.slice(0); - if (activeSelection.contains(target)) { + if (target.group === activeSelection) { activeSelection.remove(target); this._hoveredTarget = target; this._hoveredTargets = this.targets.concat(); From 9a223d0f03c66d57be4bb33dca47be7feb71092d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 18:37:33 +0300 Subject: [PATCH 33/77] Update canvas_events.mixin.js --- src/mixins/canvas_events.mixin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index affeda50b46..8beea7a0e12 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -712,6 +712,7 @@ } } } + this._objectsToRender = this._chooseObjectsToRender(); this._handleEvent(e, 'down'); // we must renderAll so that we update the visuals (shouldRender || shouldGroup) && this.requestRenderAll(); From 2d5cc75d0bcd91f3e14dfff0a9768a272ad714c5 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 18:41:59 +0300 Subject: [PATCH 34/77] Update canvas.class.js --- src/canvas.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canvas.class.js b/src/canvas.class.js index e86164843a3..1ad68f09178 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -892,7 +892,7 @@ */ searchPossibleTargets: function (objects, pointer) { var target = this._searchPossibleTargets(objects, pointer); - return this.targets[0] || target; + return target && target.interactive && this.targets[0] ? this.targets[0] : target; }, /** From 01c4a87b89bf0a48a1276aaa2b069afa3b991f09 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 18:42:15 +0300 Subject: [PATCH 35/77] Update canvas.class.js --- src/canvas.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canvas.class.js b/src/canvas.class.js index 1ad68f09178..9992f735d0b 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -892,7 +892,7 @@ */ searchPossibleTargets: function (objects, pointer) { var target = this._searchPossibleTargets(objects, pointer); - return target && target.interactive && this.targets[0] ? this.targets[0] : target; + return target && target.interactive && this.targets[0] ? this.targets[0] : target; }, /** From 216416164156c6253681083b0107e54f154984b5 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 19:09:21 +0300 Subject: [PATCH 36/77] fix(): export svg backgroundColor --- src/shapes/group.class.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index e87a6922326..f10bed05b1a 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -876,6 +876,19 @@ /* _TO_SVG_START_ */ + /** + * @private + */ + _createSVGBgRect: function (reviver) { + if (!this.backgroundColor) { + return ''; + } + var fillStroke = fabric.Rect.prototype._toSVG.call(this, reviver); + var commons = fillStroke.indexOf('COMMON_PARTS'); + fillStroke[commons] = 'for="group" '; + return fillStroke.join(''); + }, + /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. @@ -883,6 +896,8 @@ */ _toSVG: function (reviver) { var svgString = ['\n']; + var bg = this._createSVGBgRect(reviver); + bg && svgString.push('\t\t', bg); for (var i = 0, len = this._objects.length; i < len; i++) { svgString.push('\t\t', this._objects[i].toSVG(reviver)); } @@ -912,6 +927,8 @@ */ toClipPathSVG: function (reviver) { var svgString = []; + var bg = this._createSVGBgRect(reviver); + bg && svgString.push('\t', bg); for (var i = 0, len = this._objects.length; i < len; i++) { svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); } From a5575bd4dde5eb4be7e6f299cc82f5ed6f2de15a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 22:46:10 +0300 Subject: [PATCH 37/77] Update group.js --- test/unit/group.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/group.js b/test/unit/group.js index 88aa19625f9..dd5b07865e9 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -424,6 +424,7 @@ var group = makeGroupWith2ObjectsWithOpacity(); var groupObject = group.toObject(); + groupObject.subTargetCheck = true; fabric.Group.fromObject(groupObject).then(function(newGroupFromObject) { assert.ok(newGroupFromObject._objects[0].lineCoords.tl, 'acoords 0 are restored'); From 5c56124d003164c1467102906faff7dd3dc2f79e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 22:58:58 +0300 Subject: [PATCH 38/77] backport collection fix to test --- test/unit/object.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/object.js b/test/unit/object.js index 7850a03ae78..16dadce8de7 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -883,8 +883,8 @@ add: function () { fabric.Collection.add.call(this, arguments, this._onObjectAdded); }, - insertAt: function (objects, index, nonSplicing) { - fabric.Collection.insertAt.call(this, objects, index, nonSplicing, this._onObjectAdded); + insertAt: function (objects, index) { + fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); }, remove: function () { fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); From 5e3aaf59a6f843e016918fd224f8130d4be22f1e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 22:59:27 +0300 Subject: [PATCH 39/77] Update group.class.js --- src/shapes/group.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index f10bed05b1a..a99bac9ddff 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -151,7 +151,7 @@ * @param {Number} index Index to insert object at */ insertAt: function (objects, index) { - fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); + fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); this._onAfterObjectsChange('added', Array.isArray(objects) ? objects : [objects]); }, From d53d6b61557d46846007abc0f68799d9266e4ee2 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 23:02:42 +0300 Subject: [PATCH 40/77] lint --- src/shapes/group.class.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index a99bac9ddff..2c9fa87af26 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -223,8 +223,8 @@ /** * Checks if object can enter group and logs relevant warnings * @private - * @param {fabric.Object} object - * @returns + * @param {fabric.Object} object + * @returns */ canEnter: function (object) { if (object === this) { From ab776a36b998141fac0a279c3be7e3c220619849 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 4 Apr 2022 23:11:36 +0300 Subject: [PATCH 41/77] Update group.class.js --- src/shapes/group.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 02d152d5395..fdfd4ca466d 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -237,7 +237,7 @@ /* _DEV_MODE_START_ */ console.warn('fabric.Group: nesting layers under groups is not supported, this call has no effect'); /* _DEV_MODE_END_ */ - return; + return false; } else if (object.group && object.group === this) { /* _DEV_MODE_START_ */ From 8e907bb82c9ecfb2cff67f7d2c6ff1c159df1d57 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 11:53:09 +0300 Subject: [PATCH 42/77] fix(): target prop on canvas added/removed event --- src/static_canvas.class.js | 4 ++-- test/unit/object.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 17db115ca8a..f7e31d8b057 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -610,7 +610,7 @@ obj._set('canvas', this); obj.setCoords(); this.fire('object:added', { target: obj }); - obj.fire('added'); + obj.fire('added', { target: this }); }, /** @@ -619,7 +619,7 @@ */ _onObjectRemoved: function(obj) { this.fire('object:removed', { target: obj }); - obj.fire('removed'); + obj.fire('removed', { target: this }); obj._set('canvas', undefined); }, diff --git a/test/unit/object.js b/test/unit/object.js index 16dadce8de7..0251348c4d3 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -622,7 +622,10 @@ var object = new fabric.Object(); var addedEventFired = false; - object.on('added', function() { addedEventFired = true; }); + object.on('added', function (opt) { + addedEventFired = true; + assert.ok(opt.target === canvas, 'target should equal to canvas'); + }); canvas.add(object); assert.ok(addedEventFired); @@ -645,7 +648,10 @@ canvas.add(object); - object.on('removed', function() { removedEventFired = true; }); + object.on('removed', function (opt) { + removedEventFired = true; + assert.ok(opt.target === canvas, 'target should equal to canvas'); + }); canvas.remove(object); assert.ok(removedEventFired); From f082fc955a8371e3fca97b5d67849474a87f5e2f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 11:54:05 +0300 Subject: [PATCH 43/77] fix(): modify handlers strictly on canvas events --- src/mixins/itext_behavior.mixin.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 8c58cf68d5d..9ded51c0747 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -25,8 +25,9 @@ */ initAddedHandler: function() { var _this = this; - this.on('added', function() { - var canvas = _this.canvas; + this.on('added', function (opt) { + // make sure we listen to the canvas added event + var canvas = opt.target; if (canvas) { if (!canvas._hasITextHandlers) { canvas._hasITextHandlers = true; @@ -40,8 +41,9 @@ initRemovedHandler: function() { var _this = this; - this.on('removed', function() { - var canvas = _this.canvas; + this.on('removed', function (opt) { + // make sure we listen to the canvas removed event + var canvas = opt.target; if (canvas) { canvas._iTextInstances = canvas._iTextInstances || []; fabric.util.removeFromArray(canvas._iTextInstances, _this); From c16bdfb1a9d7ff1bfd87e41ac05ac9a0761958da Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 14:04:04 +0300 Subject: [PATCH 44/77] feat(): initial support for layers under groups --- src/shapes/group.class.js | 49 ++++++++++++++------ src/shapes/layer.class.js | 96 ++++++++++++++++++++++++++++----------- 2 files changed, 105 insertions(+), 40 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index fdfd4ca466d..83b78a9c0c1 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -99,6 +99,7 @@ this.callSuper('initialize', options); this.forEachObject(function (object) { this.enterGroup(object, false); + object.fire('added:initialized', { target: this }); }, this); this._applyLayoutStrategy({ type: 'initialization', @@ -233,17 +234,16 @@ /* _DEV_MODE_END_ */ return false; } - else if (object.type === 'layer') { + else if (object.group && object.group === this) { /* _DEV_MODE_START_ */ - console.warn('fabric.Group: nesting layers under groups is not supported, this call has no effect'); + console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); /* _DEV_MODE_END_ */ return false; } - else if (object.group && object.group === this) { + else if (object.group) { /* _DEV_MODE_START_ */ - console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); + console.warn('fabric.Group: object is about to enter group and leave another'); /* _DEV_MODE_END_ */ - return false; } return true; }, @@ -486,10 +486,17 @@ * @param {fabric.Point} diff */ _adjustObjectPosition: function (object, diff) { - object.set({ - left: object.left + diff.x, - top: object.top + diff.y, - }); + if (object instanceof fabric.Layer) { + object.forEachObject(function (obj) { + this._adjustObjectPosition(obj, diff); + }.bind(this)); + } + else { + object.set({ + left: object.left + diff.x, + top: object.top + diff.y, + }); + } }, /** @@ -550,7 +557,14 @@ result: result, diff: diff }); - // recursive up + this._bubbleLayout(context); + }, + + /** + * bubble layout recursive up + * @private + */ + _bubbleLayout: function (context) { if (this.group && this.group._applyLayoutStrategy) { // append the path recursion to context if (!context.path) { @@ -562,7 +576,6 @@ } }, - /** * Override this method to customize layout. * If you need to run logic once layout completes use `onLayout` @@ -784,8 +797,18 @@ } var objCenter, sizeVector, min, max, a, b; objects.forEach(function (object, i) { - objCenter = object.getRelativeCenterPoint(); - sizeVector = object._getTransformedDimensions().scalarDivideEquals(2); + if (object instanceof fabric.Layer) { + var bbox = object.getObjectsBoundingBox(object._objects.slice(0)); + sizeVector = object._getTransformedDimensions({ + width: bbox.width, + height: bbox.height + }).scalarDivideEquals(2); + objCenter = new fabric.Point(bbox.centerX, bbox.centerY); + } + else { + sizeVector = object._getTransformedDimensions().scalarDivideEquals(2); + objCenter = object.getRelativeCenterPoint(); + } if (object.angle) { var rad = degreesToRadians(object.angle), sin = Math.abs(fabric.util.sin(rad)), diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 789829d1d4e..273aa0bb386 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -66,6 +66,18 @@ */ lockMovementY: true, + /** + * @default + * @override + */ + originX: 'center', + + /** + * @default + * @override + */ + originY:'center', + /** * we don't want to int with the layer, only with it's objects * this makes group selection possible over a layer @@ -84,48 +96,66 @@ initialize: function (objects, options) { this.callSuper('initialize', objects, options); this.__canvasMonitor = this.__canvasMonitor.bind(this); + this.__groupMonitor = this.__groupMonitor.bind(this); + this.__onAdded = this._watchParent.bind(this, true); + this.__onRemoved = this._watchParent.bind(this, false); + this.on('added:initialized', this.__onAdded); + this.on('added', this.__onAdded); + this.on('removed', this.__onRemoved); }, /** - * - * @param {string} key - * @param {*} value - */ - _set: function (key, value) { - var settingCanvas = key === 'canvas'; - if (settingCanvas) { - if (!value && this.canvas) { - // detach canvas resize handler - this.canvas.off('resize', this.__canvasMonitor); - } - else if (value && (!this.canvas || this.canvas !== value)) { - // attach canvas resize handler, make sure we listen to the resize event only once - this.canvas && this.canvas.off('resize', this.__canvasMonitor); - value.off('resize', this.__canvasMonitor); - value.on('resize', this.__canvasMonitor); - } + * we need to invalidate instance's group if objects have changed + * @override + * @private + */ + __objectMonitor: function (opt) { + this.group && this.group.__objectMonitor(opt); + }, + + /** + * @private + * @param {boolean} watch + * @param {{target:fabric.Group|fabric.Canvas}} opt + */ + _watchParent: function (watch, opt) { + var target = opt.target; + // make sure we listen only once + this.canvas && this.canvas.off('resize', this.__canvasMonitor); + this.group && this.group.off('layout', this.__groupMonitor); + if (!watch) { + return; } - this.callSuper('_set', key, value); - // apply layout after canvas is set - if (settingCanvas) { + else if (target instanceof fabric.Group) { + this._applyLayoutStrategy({ type: 'group' }); + this.group.on('layout', this.__groupMonitor); + } + else if (target instanceof fabric.Canvas) { this._applyLayoutStrategy({ type: 'canvas' }); + this.canvas.on('resize', this.__canvasMonitor); } }, /** - * we do not need to invalidate layout because layer fills the entire canvas - * @override * @private */ - __objectMonitor: function () { - // noop + __canvasMonitor: function () { + this._applyLayoutStrategy({ type: 'canvas_resize' }); }, /** * @private */ - __canvasMonitor: function () { - this._applyLayoutStrategy({ type: 'canvas_resize' }); + __groupMonitor: function (context) { + this._applyLayoutStrategy(Object.assign({}, context, { type: 'group_layout' })); + }, + + /** + * @private + * @override we do not want to bubble layout + */ + _bubbleLayout: function () { + // noop }, /** @@ -147,6 +177,15 @@ height: this.canvas.height }; } + else if ((context.type === 'group' || context.type === 'group_layout') && this.group) { + var w = this.group.width, h = this.group.height; + return { + centerX: 0, + centerY:0, + width: w, + height: h + }; + } }, toString: function () { @@ -154,7 +193,10 @@ }, dispose: function () { - this.canvas && this.canvas.off('resize', this.__canvasMonitor); + this.on('added:initialized', this.__onAdded); + this.off('added', this.__onAdded); + this.off('removed', this.__onRemoved); + this._watchParent(false); this.callSuper('dispose'); } From f3aa02ebe6ced62120b0dc325b423d059accc793 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 14:12:41 +0300 Subject: [PATCH 45/77] lint --- src/shapes/layer.class.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 273aa0bb386..5db26bf1275 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -71,12 +71,12 @@ * @override */ originX: 'center', - + /** * @default * @override - */ - originY:'center', + */ + originY: 'center', /** * we don't want to int with the layer, only with it's objects @@ -181,7 +181,7 @@ var w = this.group.width, h = this.group.height; return { centerX: 0, - centerY:0, + centerY: 0, width: w, height: h }; From c3f69b7725dfe134fcacdbe54f9fa591e738858e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 14:26:51 +0300 Subject: [PATCH 46/77] visual test --- .../group-layout/fit-content-nested-layer.png | Bin 0 -> 7140 bytes test/visual/group_layout.js | 21 ++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 test/visual/golden/group-layout/fit-content-nested-layer.png diff --git a/test/visual/golden/group-layout/fit-content-nested-layer.png b/test/visual/golden/group-layout/fit-content-nested-layer.png new file mode 100644 index 0000000000000000000000000000000000000000..034a603df20bb12a28f8df2037aea0e5c6c13165 GIT binary patch literal 7140 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 literal 0 HcmV?d00001 diff --git a/test/visual/group_layout.js b/test/visual/group_layout.js index e7a36a96b01..c210abc0e0e 100644 --- a/test/visual/group_layout.js +++ b/test/visual/group_layout.js @@ -118,6 +118,27 @@ height: 300 }); + function fitContentNestedLayer(canvas, callback) { + var g = createGroupForLayoutTests('fit-content layout', { + backgroundColor: 'blue' + }); + var objects = g.removeAll(); + var layer = new fabric.Layer(objects, { backgroundColor: 'yellow' }); + g.add(layer); + canvas.add(g); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'fit-content with nested layer', + code: fitContentNestedLayer, + golden: 'group-layout/fit-content-nested-layer.png', + percentage: 0.06, + width: 400, + height: 300 + }); + function nestedLayout(canvas, callback) { var rect3 = new fabric.Rect({ width: 100, From bca1515c2f51d05c024d1218c299a369b3a8aff3 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 14:36:32 +0300 Subject: [PATCH 47/77] Update layer.class.js --- src/shapes/layer.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 5db26bf1275..6cba4fb2fbb 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -193,7 +193,7 @@ }, dispose: function () { - this.on('added:initialized', this.__onAdded); + this.off('added:initialized', this.__onAdded); this.off('added', this.__onAdded); this.off('removed', this.__onRemoved); this._watchParent(false); From 80b384d8f54e0735fc82e5fbaf0b557ad5568c4a Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 14:39:22 +0300 Subject: [PATCH 48/77] Update layer.class.js --- src/shapes/layer.class.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 6cba4fb2fbb..587f8319110 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -116,10 +116,10 @@ /** * @private * @param {boolean} watch - * @param {{target:fabric.Group|fabric.Canvas}} opt + * @param {{target:fabric.Group|fabric.Canvas}} [opt] */ _watchParent: function (watch, opt) { - var target = opt.target; + var target = opt && opt.target; // make sure we listen only once this.canvas && this.canvas.off('resize', this.__canvasMonitor); this.group && this.group.off('layout', this.__groupMonitor); From 548d65c24f5abb562b500d0382405d277c8a1cfa Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 16:09:33 +0300 Subject: [PATCH 49/77] JSDOC --- src/shapes/layer.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 587f8319110..a77da21db47 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -13,7 +13,6 @@ * Layer class * @class fabric.Layer * @extends fabric.Group - * @mixes fabric.Collection * @see {@link fabric.Layer#initialize} for constructor definition */ fabric.Layer = fabric.util.createClass(fabric.Group, /** @lends fabric.Layer.prototype */ { @@ -159,6 +158,7 @@ }, /** + * Layer will layout itself once it is added to a canvas/group and by listening to it's parent `resize`/`layout` events respectively * Override this method to customize layout * @public * @param {string} layoutDirective From 656c36a151cddd50aa946b4101d195fd6839cb6b Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 17:32:13 +0300 Subject: [PATCH 50/77] Create layer.js --- test/unit/layer.js | 860 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 860 insertions(+) create mode 100644 test/unit/layer.js diff --git a/test/unit/layer.js b/test/unit/layer.js new file mode 100644 index 00000000000..8637d0d786f --- /dev/null +++ b/test/unit/layer.js @@ -0,0 +1,860 @@ +(function() { + var canvas = this.canvas = new fabric.StaticCanvas(null, {enableRetinaScaling: false, width: 600, height: 600}); + + function makeLayerWith2Objects() { + var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), + rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0 }); + + return new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); + } + + function makeLayerWith2ObjectsWithOpacity() { + var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0, opacity: 0.5 }), + rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0, opacity: 0.8 }); + + return new fabric.Layer([rect1, rect2], {strokeWidth: 0}); + } + + function makeLayerWith2ObjectsAndNoExport() { + var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), + rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0, excludeFromExport: true }); + + return new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); + } + + function makeLayerWith4Objects() { + var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10 }), + rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40 }), + rect3 = new fabric.Rect({ top: 40, left: 0, width: 20, height: 40 }), + rect4 = new fabric.Rect({ top: 75, left: 75, width: 40, height: 40 }); + + return new fabric.Layer([rect1, rect2, rect3, rect4]); + } + + QUnit.module('fabric.Layer', { + afterEach: function() { + fabric.Object.__uid = 0; + canvas.clear(); + canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor; + canvas.calcOffset(); + } + }); + + QUnit.test('constructor', function(assert) { + var layer = makeLayerWith2Objects(); + + assert.ok(layer); + assert.ok(layer instanceof fabric.Layer, 'should be instance of fabric.Layer'); + }); + + QUnit.test('toString', function(assert) { + var layer = makeLayerWith2Objects(); + assert.equal(layer.toString(), '#', 'should return proper representation'); + }); + + QUnit.test('getObjects', function(assert) { + var rect1 = new fabric.Rect(), + rect2 = new fabric.Rect(); + + var layer = new fabric.Layer([rect1, rect2]); + + assert.ok(typeof layer.getObjects === 'function'); + assert.ok(Array.isArray(layer.getObjects()), 'should be an array'); + assert.equal(layer.getObjects().length, 2, 'should have 2 items'); + assert.deepEqual(layer.getObjects(), [rect1, rect2], 'should return deepEqual objects as those passed to constructor'); + }); + + QUnit.test('getObjects with type', function(assert) { + var rect = new fabric.Rect({ width: 10, height: 20 }), + circle = new fabric.Circle({ radius: 30 }); + + var layer = new fabric.Layer([rect, circle]); + + assert.equal(layer.size(), 2, 'should have length=2 initially'); + + assert.deepEqual(layer.getObjects('rect'), [rect], 'should return rect only'); + assert.deepEqual(layer.getObjects('circle'), [circle], 'should return circle only'); + assert.deepEqual(layer.getObjects('circle', 'rect'), [rect, circle], 'should return circle and rect, in the same order they are'); + }); + + QUnit.test('add', function(assert) { + var layer = makeLayerWith2Objects(); + var rect1 = new fabric.Rect(), + rect2 = new fabric.Rect(), + rect3 = new fabric.Rect(); + + assert.ok(typeof layer.add === 'function'); + layer.add(rect1); + assert.strictEqual(layer.item(layer.size() - 1), rect1, 'last object should be newly added one'); + assert.equal(layer.getObjects().length, 3, 'there should be 3 objects'); + + layer.add(rect2, rect3); + assert.strictEqual(layer.item(layer.size() - 1), rect3, 'last object should be last added one'); + assert.equal(layer.size(), 5, 'there should be 5 objects'); + }); + + QUnit.test('remove', function(assert) { + var rect1 = new fabric.Rect(), + rect2 = new fabric.Rect(), + rect3 = new fabric.Rect(), + layer = new fabric.Layer([rect1, rect2, rect3]); + + assert.ok(typeof layer.remove === 'function'); + layer.remove(rect2); + assert.deepEqual(layer.getObjects(), [rect1, rect3], 'should remove object properly'); + + layer.remove(rect1, rect3); + assert.equal(layer.isEmpty(), true, 'layer should be empty'); + }); + + QUnit.test('size', function(assert) { + var layer = makeLayerWith2Objects(); + + assert.ok(typeof layer.size === 'function'); + assert.equal(layer.size(), 2); + layer.add(new fabric.Rect()); + assert.equal(layer.size(), 3); + layer.remove(layer.getObjects()[0]); + layer.remove(layer.getObjects()[0]); + assert.equal(layer.size(), 1); + }); + + QUnit.test('set', function(assert) { + var layer = makeLayerWith2Objects(), + firstObject = layer.getObjects()[0]; + + assert.ok(typeof layer.set === 'function'); + + layer.set('opacity', 0.12345); + assert.equal(layer.get('opacity'), 0.12345, 'layer\'s "own" property should be set properly'); + assert.equal(firstObject.get('opacity'), 1, 'objects\' value of non delegated property should stay same'); + + layer.set('left', 1234); + assert.equal(layer.get('left'), 1234, 'layer\'s own "left" property should be set properly'); + assert.ok(firstObject.get('left') !== 1234, 'objects\' value should not be affected'); + + layer.set({ left: 888, top: 999 }); + assert.equal(layer.get('left'), 888, 'layer\'s own "left" property should be set properly via object'); + assert.equal(layer.get('top'), 999, 'layer\'s own "top" property should be set properly via object'); + }); + + QUnit.test('contains', function(assert) { + var rect1 = new fabric.Rect(), + rect2 = new fabric.Rect(), + notIncludedRect = new fabric.Rect(), + layer = new fabric.Layer([rect1, rect2]); + + assert.ok(typeof layer.contains === 'function'); + + assert.ok(layer.contains(rect1), 'should contain first object'); + assert.ok(layer.contains(rect2), 'should contain second object'); + + assert.ok(!layer.contains(notIncludedRect), 'should report not-included one properly'); + }); + + QUnit.test('toObject', function(assert) { + var layer = makeLayerWith2Objects(); + + assert.ok(typeof layer.toObject === 'function'); + + var clone = layer.toObject(); + + var expectedObject = { + version: fabric.version, + type: 'layer', + originX: 'center', + originY: 'center', + left: 0, + top: 0, + width: 0, + height: 0, + fill: 'rgb(0,0,0)', + layout: 'auto', + stroke: null, + strokeWidth: 0, + strokeDashArray: null, + strokeLineCap: 'butt', + strokeDashOffset: 0, + strokeLineJoin: 'miter', + strokeMiterLimit: 4, + scaleX: 1, + scaleY: 1, + shadow: null, + visible: true, + backgroundColor: '', + angle: 0, + flipX: false, + flipY: false, + opacity: 1, + fillRule: 'nonzero', + paintFirst: 'fill', + globalCompositeOperation: 'source-over', + skewX: 0, + skewY: 0, + objects: clone.objects, + strokeUniform: false, + subTargetCheck: false, + interactive: false, + }; + + assert.deepEqual(clone, expectedObject); + + assert.ok(layer !== clone, 'should produce different object'); + assert.ok(layer.getObjects() !== clone.objects, 'should produce different object array'); + assert.ok(layer.getObjects()[0] !== clone.objects[0], 'should produce different objects in array'); + }); + + QUnit.test('toObject without default values', function(assert) { + var layer = makeLayerWith2Objects(); + layer.includeDefaultValues = false; + var clone = layer.toObject(); + var objects = [{ + version: fabric.version, + type: 'rect', + left: 10, + top: -30, + width: 30, + height: 10, + strokeWidth: 0, + }, { + version: fabric.version, + type: 'rect', + left: -40, + top: -10, + width: 10, + height: 40, + strokeWidth: 0, + }]; + var expectedObject = { + version: fabric.version, + type: 'layer', + left: 0, + top: 0, + objects: objects, + }; + assert.deepEqual(clone, expectedObject); + }); + + + QUnit.test('toObject with excludeFromExport set on an object', function (assert) { + var layer = makeLayerWith2Objects(); + var group2 = makeLayerWith2ObjectsAndNoExport(); + var clone = layer.toObject(); + var clone2 = group2.toObject(); + assert.deepEqual(clone2.objects, group2._objects.filter(obj => !obj.excludeFromExport).map(obj => obj.toObject())); + delete clone.objects; + delete clone2.objects; + assert.deepEqual(clone, clone2); + }); + + QUnit.test('render', function(assert) { + var layer = makeLayerWith2Objects(); + assert.ok(typeof layer.render === 'function'); + }); + + QUnit.test('item', function(assert) { + var layer = makeLayerWith2Objects(); + + assert.ok(typeof layer.item === 'function'); + assert.equal(layer.item(0), layer.getObjects()[0]); + assert.equal(layer.item(1), layer.getObjects()[1]); + assert.equal(layer.item(9999), undefined); + }); + + QUnit.test('moveTo', function(assert) { + var layer = makeLayerWith4Objects(), + groupEl1 = layer.getObjects()[0], + groupEl2 = layer.getObjects()[1], + groupEl3 = layer.getObjects()[2], + groupEl4 = layer.getObjects()[3]; + + assert.ok(typeof layer.item(0).moveTo === 'function'); + + // [ 1, 2, 3, 4 ] + assert.equal(layer.item(0), groupEl1); + assert.equal(layer.item(1), groupEl2); + assert.equal(layer.item(2), groupEl3); + assert.equal(layer.item(3), groupEl4); + assert.equal(layer.item(9999), undefined); + + layer.item(0).moveTo(3); + + // moved 1 to level 3 — [2, 3, 4, 1] + assert.equal(layer.item(3), groupEl1); + assert.equal(layer.item(0), groupEl2); + assert.equal(layer.item(1), groupEl3); + assert.equal(layer.item(2), groupEl4); + assert.equal(layer.item(9999), undefined); + + layer.item(0).moveTo(2); + + // moved 2 to level 2 — [3, 4, 2, 1] + assert.equal(layer.item(3), groupEl1); + assert.equal(layer.item(2), groupEl2); + assert.equal(layer.item(0), groupEl3); + assert.equal(layer.item(1), groupEl4); + assert.equal(layer.item(9999), undefined); + }); + + QUnit.test('complexity', function(assert) { + var layer = makeLayerWith2Objects(); + + assert.ok(typeof layer.complexity === 'function'); + assert.equal(layer.complexity(), 2); + }); + + QUnit.test('removeAll', function(assert) { + var layer = makeLayerWith2Objects(), + firstObject = layer.item(0), + initialLeftValue = 100, + initialTopValue = 100; + + assert.ok(typeof layer.removeAll === 'function'); + + assert.ok(initialLeftValue !== firstObject.get('left')); + assert.ok(initialTopValue !== firstObject.get('top')); + + layer.removeAll(); + assert.equal(firstObject.get('left'), initialLeftValue, 'should restore initial left value'); + assert.equal(firstObject.get('top'), initialTopValue, 'should restore initial top value'); + }); + + QUnit.test('containsPoint', function(assert) { + + var layer = makeLayerWith2Objects(); + layer.set({ originX: 'center', originY: 'center' }).setCoords(); + + // Rect #1 top: 100, left: 100, width: 30, height: 10 + // Rect #2 top: 120, left: 50, width: 10, height: 40 + + assert.ok(typeof layer.containsPoint === 'function'); + + assert.ok(!layer.containsPoint({ x: 0, y: 0 })); + + layer.scale(2); + assert.ok(layer.containsPoint({ x: 50, y: 120 })); + assert.ok(layer.containsPoint({ x: 100, y: 160 })); + assert.ok(!layer.containsPoint({ x: 0, y: 0 })); + + layer.scale(1); + layer.padding = 30; + layer.setCoords(); + assert.ok(layer.containsPoint({ x: 50, y: 120 })); + assert.ok(!layer.containsPoint({ x: 100, y: 170 })); + assert.ok(!layer.containsPoint({ x: 0, y: 0 })); + }); + + QUnit.test('forEachObject', function(assert) { + var layer = makeLayerWith2Objects(); + + assert.ok(typeof layer.forEachObject === 'function'); + + var iteratedObjects = []; + layer.forEachObject(function(groupObject) { + iteratedObjects.push(groupObject); + }); + + assert.equal(iteratedObjects[0], layer.getObjects()[0], 'iteration give back objects in same order'); + assert.equal(iteratedObjects[1], layer.getObjects()[1], 'iteration give back objects in same order'); + }); + + QUnit.test('fromObject', function(assert) { + var done = assert.async(); + var layer = makeLayerWith2ObjectsWithOpacity(); + + assert.ok(typeof fabric.Layer.fromObject === 'function'); + var groupObject = layer.toObject(); + + fabric.Layer.fromObject(groupObject).then(function(newGroupFromObject) { + + var objectFromOldGroup = layer.toObject(); + var objectFromNewGroup = newGroupFromObject.toObject(); + + assert.ok(newGroupFromObject instanceof fabric.Layer); + + assert.deepEqual(objectFromOldGroup.objects[0], objectFromNewGroup.objects[0]); + assert.deepEqual(objectFromOldGroup.objects[1], objectFromNewGroup.objects[1]); + + // delete `objects` arrays, since `assertHashEqual` fails to compare them for equality + delete objectFromOldGroup.objects; + delete objectFromNewGroup.objects; + + assert.deepEqual(objectFromOldGroup, objectFromNewGroup); + + done(); + }); + }); + + QUnit.test('fromObject with clipPath', function(assert) { + var done = assert.async(); + var clipPath = new fabric.Rect({ + width: 500, + height: 250, + top: 0, + left: 0, + absolutePositioned: true + }); + + var groupObject = new fabric.Layer([ + new fabric.Rect({ width: 100, height: 100, fill: 'red' }), + new fabric.Rect({ width: 100, height: 100, fill: 'yellow', left: 100 }), + new fabric.Rect({ width: 100, height: 100, fill: 'blue', top: 100 }), + new fabric.Rect({ width: 100, height: 100, fill: 'green', left: 100, top: 100 }) + ]); + groupObject.clipPath = clipPath; + + var groupToObject = groupObject.toObject(); + + fabric.Layer.fromObject(groupToObject).then(function(newGroupFromObject) { + + var objectFromNewGroup = newGroupFromObject.toObject(); + + assert.ok(newGroupFromObject instanceof fabric.Layer); + assert.ok(newGroupFromObject.clipPath instanceof fabric.Rect, 'clipPath has been restored'); + assert.deepEqual(objectFromNewGroup, groupToObject, 'double serialization gives same results'); + + done(); + }); + }); + + QUnit.test('fromObject restores oCoords', function(assert) { + var done = assert.async(); + var layer = makeLayerWith2ObjectsWithOpacity(); + + var groupObject = layer.toObject(); + groupObject.subTargetCheck = true; + + fabric.Layer.fromObject(groupObject).then(function(newGroupFromObject) { + assert.ok(newGroupFromObject._objects[0].lineCoords.tl, 'acoords 0 are restored'); + assert.ok(newGroupFromObject._objects[1].lineCoords.tl, 'acoords 1 are restored'); + done(); + }); + }); + + QUnit.test('fromObject does not delete objects from source', function(assert) { + var done = assert.async(); + var layer = makeLayerWith2ObjectsWithOpacity(); + var groupObject = layer.toObject(); + + fabric.Layer.fromObject(groupObject).then(function(newGroupFromObject) { + assert.equal(newGroupFromObject.objects, undefined, 'the objects array has not been pulled in'); + assert.notEqual(groupObject.objects, undefined, 'the objects array has not been deleted from object source'); + done(); + }); + }); + + QUnit.test('toSVG', function(assert) { + var layer = makeLayerWith2Objects(); + assert.ok(typeof layer.toSVG === 'function'); + var expectedSVG = '\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; + assert.equal(layer.toSVG(), expectedSVG); + }); + + QUnit.test('toSVG with a clipPath', function(assert) { + var layer = makeLayerWith2Objects(); + layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); + var expectedSVG = '\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; + assert.equal(layer.toSVG(), expectedSVG); + }); + + QUnit.test('toSVG with a clipPath absolutePositioned', function(assert) { + var layer = makeLayerWith2Objects(); + layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); + layer.clipPath.absolutePositioned = true; + var expectedSVG = '\n\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n'; + assert.equal(layer.toSVG(), expectedSVG); + }); + + QUnit.test('toSVG with a layer as a clipPath', function(assert) { + var layer = makeLayerWith2Objects(); + layer.clipPath = makeLayerWith2Objects(); + var expectedSVG = '\n\n\t\t\n\t\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; + assert.equal(layer.toSVG(), expectedSVG); + }); + + QUnit.test('cloning layer with 2 objects', function(assert) { + var done = assert.async(); + var layer = makeLayerWith2Objects(); + layer.clone().then(function(clone) { + assert.ok(clone !== layer); + assert.deepEqual(clone.toObject(), layer.toObject()); + done(); + }); + }); + + QUnit.test('get with locked objects', function(assert) { + var layer = makeLayerWith2Objects(); + + assert.equal(layer.get('lockMovementX'), false); + + // TODO acitveGroup + // layer.getObjects()[0].lockMovementX = true; + // assert.equal(layer.get('lockMovementX'), true); + // + // layer.getObjects()[0].lockMovementX = false; + // assert.equal(layer.get('lockMovementX'), false); + + layer.set('lockMovementX', true); + assert.equal(layer.get('lockMovementX'), true); + + // layer.set('lockMovementX', false); + // layer.getObjects()[0].lockMovementY = true; + // layer.getObjects()[1].lockRotation = true; + // + // assert.equal(layer.get('lockMovementY'), true); + // assert.equal(layer.get('lockRotation'), true); + }); + + QUnit.test('z-index methods with layer objects', function(assert) { + + var textBg = new fabric.Rect({ + fill: '#abc', + width: 100, + height: 100 + }); + + var text = new fabric.Text('text'); + var layer = new fabric.Layer([textBg, text]); + + canvas.add(layer); + + assert.ok(layer.getObjects()[0] === textBg); + assert.ok(layer.getObjects()[1] === text); + + textBg.bringToFront(); + + assert.ok(layer.getObjects()[0] === text); + assert.ok(layer.getObjects()[1] === textBg); + + textBg.sendToBack(); + + assert.ok(layer.getObjects()[0] === textBg); + assert.ok(layer.getObjects()[1] === text); + }); + + QUnit.test('layer reference on an object', function(assert) { + var layer = makeLayerWith2Objects(); + var firstObjInGroup = layer.getObjects()[0]; + var secondObjInGroup = layer.getObjects()[1]; + + assert.equal(firstObjInGroup.group, layer); + assert.equal(secondObjInGroup.group, layer); + + layer.remove(firstObjInGroup); + assert.ok(typeof firstObjInGroup.group === 'undefined'); + }); + + QUnit.test('insertAt', function (assert) { + var rect1 = new fabric.Rect({ id: 1 }), + rect2 = new fabric.Rect({ id: 2 }), + rect3 = new fabric.Rect({ id: 3 }), + rect4 = new fabric.Rect({ id: 4 }), + rect5 = new fabric.Rect({ id: 5 }), + rect6 = new fabric.Rect({ id: 6 }), + rect7 = new fabric.Rect({ id: 7 }), + rect8 = new fabric.Rect({ id: 8 }), + layer = new fabric.Layer(), + control = [], + fired = [], + firingControl = []; + + layer.add(rect1, rect2); + control.push(rect1, rect2); + + assert.ok(typeof layer.insertAt === 'function', 'should respond to `insertAt` method'); + + const equalsControl = (description) => { + assert.deepEqual(layer.getObjects().map(o => o.id), control.map(o => o.id), 'should equal control array ' + description); + assert.deepEqual(layer.getObjects(), control, 'should equal control array ' + description); + assert.deepEqual(fired.map(o => o.id), firingControl.map(o => o.id), 'fired events should equal control array ' + description); + assert.deepEqual(fired, firingControl, 'fired events should equal control array ' + description); + } + + assert.ok(typeof layer._onObjectAdded === 'function', 'has a standard _onObjectAdded method'); + [rect1, rect2, rect3, rect4, rect5, rect6, rect7, rect8].forEach(obj => { + obj.on('added', e => { + assert.equal(e.target, layer); + fired.push(obj); + }); + }); + + layer.insertAt(rect3, 1); + control.splice(1, 0, rect3); + firingControl.push(rect3); + equalsControl('rect3'); + layer.insertAt(rect4, 0); + control.splice(0, 0, rect4); + firingControl.push(rect4); + equalsControl('rect4'); + layer.insertAt(rect5, 2); + control.splice(2, 0, rect5); + firingControl.push(rect5); + equalsControl('rect5'); + layer.insertAt([rect6], 2); + control.splice(2, 0, rect6); + firingControl.push(rect6); + equalsControl('rect6'); + layer.insertAt([rect7, rect8], 3); + control.splice(3, 0, rect7, rect8); + firingControl.push(rect7, rect8); + equalsControl('rect7'); + }); + + QUnit.test('dirty flag propagation from children up', function(assert) { + var g1 = makeLayerWith4Objects(); + var obj = g1.item(0); + g1.dirty = false; + obj.dirty = false; + g1.ownCaching = true; + assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); + obj.set('fill', 'red'); + assert.equal(obj.dirty, true, 'Obj has dirty flag set'); + assert.equal(g1.dirty, true, 'Layer has dirty flag set'); + }); + + QUnit.test('dirty flag propagation from children up is stopped if layer is not caching', function(assert) { + var g1 = makeLayerWith4Objects(); + var obj = g1.item(0); + g1.dirty = false; + obj.dirty = false; + g1.ownCaching = false; + assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); + obj.set('fill', 'red'); + assert.equal(obj.dirty, true, 'Obj has dirty flag set'); + assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); + }); + + QUnit.test('dirty flag propagation from children up does not happen if value does not change really', function(assert) { + var g1 = makeLayerWith4Objects(); + var obj = g1.item(0); + obj.fill = 'red'; + g1.dirty = false; + obj.dirty = false; + g1.ownCaching = true; + assert.equal(obj.dirty, false, 'Obj has no dirty flag set'); + assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); + obj.set('fill', 'red'); + assert.equal(obj.dirty, false, 'Obj has no dirty flag set'); + assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); + }); + + QUnit.test('dirty flag propagation from children up with', function (assert) { + var g1 = makeLayerWith4Objects(); + var obj = g1.item(0); + g1.dirty = false; + obj.dirty = false; + // specify that the layer is caching or the test will fail under node since the + // object caching is disabled by default + g1.ownCaching = true; + assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); + obj.set('angle', 5); + assert.equal(obj.dirty, false, 'Obj has dirty flag still false'); + assert.equal(g1.dirty, true, 'Layer has dirty flag set'); + }); + + QUnit.test('_getCacheCanvasDimensions returns dimensions and zoom for cache canvas are influenced by layer', function(assert) { + var g1 = makeLayerWith4Objects(); + var obj = g1.item(0); + var dims = obj._getCacheCanvasDimensions(); + g1.scaleX = 2; + var dims2 = obj._getCacheCanvasDimensions(); + assert.equal((dims2.width - 2), (dims.width - 2) * g1.scaleX, 'width of cache has increased with layer scale'); + }); + + QUnit.test('test layer - pixels.', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + layer = new fabric.Layer([rect1, rect2], {opacity: 1, fill: '', strokeWidth: 0, objectCaching: false}), + isTransparent = fabric.util.isTransparent, + ctx = canvas.contextContainer; + canvas.add(layer); + canvas.renderAll(); + assert.equal(canvas.enableRetinaScaling, false, 'enable retina scaling is off'); + assert.equal(isTransparent(ctx, 0, 0, 0), true, '0,0 is transparent'); + assert.equal(isTransparent(ctx, 1, 1, 0), false, '1,1 is opaque'); + assert.equal(isTransparent(ctx, 2, 2, 0), false, '2,2 is opaque'); + assert.equal(isTransparent(ctx, 3, 3, 0), true, '3,3 is transparent'); + assert.equal(isTransparent(ctx, 4, 4, 0), true, '4,4 is transparent'); + assert.equal(isTransparent(ctx, 5, 5, 0), false, '5,5 is opaque'); + assert.equal(isTransparent(ctx, 6, 6, 0), false, '6,6 is opaque'); + assert.equal(isTransparent(ctx, 7, 7, 0), true, '7,7 is transparent'); + }); + + QUnit.test('layer add', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + layer = new fabric.Layer([rect1]); + + var coords = layer.oCoords; + layer.add(rect2); + var newCoords = layer.oCoords; + assert.notEqual(coords, newCoords, 'object coords have been recalculated - add'); + }); + + QUnit.test('layer remove', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + layer = new fabric.Layer([rect1, rect2]); + + var coords = layer.oCoords; + layer.remove(rect2); + var newCoords = layer.oCoords; + assert.notEqual(coords, newCoords, 'object coords have been recalculated - remove'); + }); + + QUnit.test('layer willDrawShadow', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect3 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect4 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + layer = new fabric.Layer([rect1, rect2]), + group2 = new fabric.Layer([rect3, rect4]), + group3 = new fabric.Layer([layer, group2]); + + assert.equal(group3.willDrawShadow(), false, 'layer will not cast shadow because objects do not have it'); + group3.shadow = { offsetX: 1, offsetY: 2, }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow'); + delete group3.shadow; + group2.shadow = { offsetX: 1, offsetY: 2, }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because inner group2 has shadow'); + delete group2.shadow; + rect1.shadow = { offsetX: 1, offsetY: 2, }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because inner rect1 has shadow'); + assert.equal(layer.willDrawShadow(), true, 'layer will cast shadow because inner rect1 has shadow'); + assert.equal(group2.willDrawShadow(), false, 'layer will not cast shadow because no child has shadow'); + }); + + QUnit.test('layer willDrawShadow with no offsets', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect3 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + rect4 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), + layer = new fabric.Layer([rect1, rect2]), + group2 = new fabric.Layer([rect3, rect4]), + group3 = new fabric.Layer([layer, group2]); + + assert.equal(group3.willDrawShadow(), false, 'layer will not cast shadow because objects do not have it'); + group3.shadow = { offsetX: 0, offsetY: 0 }; + assert.equal(group3.willDrawShadow(), false, 'layer will NOT cast shadow because layer itself has shadow but not offsets'); + group3.shadow = { offsetX: 2, offsetY: 0 }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetX different than 0'); + group3.shadow = { offsetX: 0, offsetY: 2 }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetY different than 0'); + group3.shadow = { offsetX: -2, offsetY: 0 }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetX different than 0'); + group3.shadow = { offsetX: 0, offsetY: -2 }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetY different than 0'); + rect1.shadow = { offsetX: 1, offsetY: 2, }; + group3.shadow = { offsetX: 0, offsetY: 0 }; + assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself will not, but rect 1 will'); + + }); + + QUnit.test('layer shouldCache', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + rect3 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + rect4 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + layer = new fabric.Layer([rect1, rect2], { objectCaching: true}), + group2 = new fabric.Layer([rect3, rect4], { objectCaching: true}), + group3 = new fabric.Layer([layer, group2], { objectCaching: true}); + + assert.equal(group3.shouldCache(), true, 'group3 will cache because no child has shadow'); + assert.equal(group2.shouldCache(), false, 'group2 will not cache because is drawing on parent group3 cache'); + assert.equal(rect3.shouldCache(), false, 'rect3 will not cache because is drawing on parent2 layer cache'); + + group2.shadow = { offsetX: 2, offsetY: 0 }; + rect1.shadow = { offsetX: 0, offsetY: 2 }; + + assert.equal(group3.shouldCache(), false, 'group3 will cache because children have shadow'); + assert.equal(group2.shouldCache(), true, 'group2 will cache because is not drawing on parent group3 cache and no children have shadow'); + assert.equal(layer.shouldCache(), false, 'layer will not cache because even if is not drawing on parent group3 cache children have shadow'); + + assert.equal(rect1.shouldCache(), true, 'rect1 will cache because none of its parent is caching'); + assert.equal(rect3.shouldCache(), false, 'rect3 will not cache because group2 is caching'); + + }); + + QUnit.test('canvas prop propagation with set', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + layer = new fabric.Layer([rect1, rect2]); + + layer.set('canvas', 'a-canvas'); + assert.equal(layer.canvas, 'a-canvas', 'canvas has been set'); + assert.equal(layer._objects[0].canvas, 'a-canvas', 'canvas has been set on object 0'); + assert.equal(layer._objects[1].canvas, 'a-canvas', 'canvas has been set on object 1'); + }); + + QUnit.test('canvas prop propagation with add', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + layer = new fabric.Layer([rect1, rect2]); + + canvas.add(layer); + assert.equal(layer.canvas, canvas, 'canvas has been set'); + assert.equal(layer._objects[0].canvas, canvas, 'canvas has been set on object 0'); + assert.equal(layer._objects[1].canvas, canvas, 'canvas has been set on object 1'); + }); + + QUnit.test('canvas prop propagation with add to layer', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), + layer = new fabric.Layer(); + + canvas.add(layer); + assert.equal(layer.canvas, canvas, 'canvas has been set'); + layer.add(rect1); + assert.equal(layer._objects[0].canvas, canvas, 'canvas has been set on object 0'); + layer.add(rect2); + assert.equal(layer._objects[1].canvas, canvas, 'canvas has been set on object 0'); + }); + + QUnit.test('add and coordinates', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), + layer = new fabric.Layer([]); + layer.add(rect1); + layer.add(rect2); + layer.left = 5; + layer.top = 5; + layer.scaleX = 3; + layer.scaleY = 2; + layer.removeAll(); + assert.equal(rect1.top, 5, 'top has been moved'); + assert.equal(rect1.left, 11, 'left has been moved'); + assert.equal(rect1.scaleX, 3, 'scaleX has been scaled'); + assert.equal(rect1.scaleY, 2, 'scaleY has been scaled'); + assert.equal(rect2.top, 13, 'top has been moved'); + assert.equal(rect2.left, 23, 'left has been moved'); + assert.equal(rect2.scaleX, 2, 'scaleX has been scaled inverted because of angle 90'); + assert.equal(rect2.scaleY, 3, 'scaleY has been scaled inverted because of angle 90'); + }); + + QUnit.skip('addRelativeToGroup and coordinates with nested groups', function(assert) { + var rect1 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), + rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), + group0 = new fabric.Layer([rect1, rect2]), + rect3 = new fabric.Rect({ top: 2, left: 9, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), + rect4 = new fabric.Rect({ top: 3, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), + group1 = new fabric.Layer([rect3, rect4], { scaleX: 3, scaleY: 4 }), + layer = new fabric.Layer([group0, group1], { angle: 90, scaleX: 2, scaleY: 0.5 }), + rect5 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }); + + group1.addRelativeToGroup(rect5); + var t = group1.calcTransformMatrix(); + var pos = fabric.util.transformPoint(new fabric.Point(rect5.left, rect5.top), t); + assert.equal(rect5.top, -5.5, 'top has been moved'); + assert.equal(rect5.left, -19.5, 'left has been moved'); + assert.equal(rect5.scaleX, 2, 'scaleX has been scaled'); + assert.equal(rect5.scaleY, 0.5, 'scaleY has been scaled'); + layer.removeAll(); + group1.removeAll(); + assert.equal(rect5.top, 1, 'top is back to original minus rounding errors'); + assert.equal(rect5.left, 1, 'left is back to original'); + assert.equal(rect5.scaleX, 1, 'scaleX is back to original'); + assert.equal(rect5.scaleY, 1, 'scaleY is back to original'); + }); + + +})(); From 70f80af2f2423a27efaa3aa8e5f269b025960720 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 17:33:47 +0300 Subject: [PATCH 51/77] Update layer.js --- test/unit/layer.js | 128 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 27 deletions(-) diff --git a/test/unit/layer.js b/test/unit/layer.js index 8637d0d786f..697597e5c65 100644 --- a/test/unit/layer.js +++ b/test/unit/layer.js @@ -1,34 +1,50 @@ (function() { var canvas = this.canvas = new fabric.StaticCanvas(null, {enableRetinaScaling: false, width: 600, height: 600}); - function makeLayerWith2Objects() { + function makeLayerWith2Objects(performLayout, subTargetCheck, interactive) { var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0 }); - return new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); + var layer = new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); + if (performLayout) { + new fabric.Group([layer], { subTargetCheck, interactive }); + } + return layer; } - function makeLayerWith2ObjectsWithOpacity() { + function makeLayerWith2ObjectsWithOpacity(performLayout, subTargetCheck, interactive) { var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0, opacity: 0.5 }), rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0, opacity: 0.8 }); - return new fabric.Layer([rect1, rect2], {strokeWidth: 0}); + var layer = new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); + if (performLayout) { + new fabric.Group([layer], { subTargetCheck, interactive }); + } + return layer; } - function makeLayerWith2ObjectsAndNoExport() { + function makeLayerWith2ObjectsAndNoExport(performLayout, subTargetCheck, interactive) { var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0, excludeFromExport: true }); - return new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); + var layer = new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); + if (performLayout) { + new fabric.Group([layer], { subTargetCheck, interactive }); + } + return layer; } - function makeLayerWith4Objects() { + function makeLayerWith4Objects(performLayout, subTargetCheck, interactive) { var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10 }), rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40 }), rect3 = new fabric.Rect({ top: 40, left: 0, width: 20, height: 40 }), rect4 = new fabric.Rect({ top: 75, left: 75, width: 40, height: 40 }); - return new fabric.Layer([rect1, rect2, rect3, rect4]); + var layer = new fabric.Layer([rect1, rect2, rect3, rect4]); + if (performLayout) { + new fabric.Group([layer], { subTargetCheck, interactive }); + } + return layer; } QUnit.module('fabric.Layer', { @@ -202,6 +218,17 @@ assert.ok(layer !== clone, 'should produce different object'); assert.ok(layer.getObjects() !== clone.objects, 'should produce different object array'); assert.ok(layer.getObjects()[0] !== clone.objects[0], 'should produce different objects in array'); + + // peform layout + new fabric.Group([layer]); + clone = layer.toObject(); + + Object.assign(expectedObject, { width: 80, height: 60 }); + assert.deepEqual(clone, expectedObject); + + assert.ok(layer !== clone, 'should produce different object'); + assert.ok(layer.getObjects() !== clone.objects, 'should produce different object array'); + assert.ok(layer.getObjects()[0] !== clone.objects[0], 'should produce different objects in array'); }); QUnit.test('toObject without default values', function(assert) { @@ -233,6 +260,19 @@ objects: objects, }; assert.deepEqual(clone, expectedObject); + // peform layout + new fabric.Group([layer]); + clone = layer.toObject(); + expectedObject = { + version: fabric.version, + type: 'layer', + left: 0, + top: 0, + width: 80, + height: 60, + objects: objects, + }; + assert.deepEqual(clone, expectedObject); }); @@ -304,7 +344,7 @@ }); QUnit.test('removeAll', function(assert) { - var layer = makeLayerWith2Objects(), + var layer = makeLayerWith2Objects(true), firstObject = layer.item(0), initialLeftValue = 100, initialTopValue = 100; @@ -319,29 +359,36 @@ assert.equal(firstObject.get('top'), initialTopValue, 'should restore initial top value'); }); - QUnit.test('containsPoint', function(assert) { + QUnit.test.only('containsPoint', function(assert) { - var layer = makeLayerWith2Objects(); - layer.set({ originX: 'center', originY: 'center' }).setCoords(); + var layer = makeLayerWith2Objects(true, true, true); + layer.group.setCoords(); // Rect #1 top: 100, left: 100, width: 30, height: 10 // Rect #2 top: 120, left: 50, width: 10, height: 40 assert.ok(typeof layer.containsPoint === 'function'); - assert.ok(!layer.containsPoint({ x: 0, y: 0 })); + function containsPoint(p) { + var localPoint = fabric.util.sendPointToPlane(p, null, layer.group.calcTransformMatrix()); + console.log(localPoint) + return layer.containsPoint(localPoint); + } + + assert.ok(!containsPoint({ x: 0, y: 0 })); layer.scale(2); - assert.ok(layer.containsPoint({ x: 50, y: 120 })); - assert.ok(layer.containsPoint({ x: 100, y: 160 })); - assert.ok(!layer.containsPoint({ x: 0, y: 0 })); + assert.ok(containsPoint({ x: 50, y: 120 })); + assert.ok(containsPoint({ x: 100, y: 160 })); + assert.ok(!containsPoint({ x: 0, y: 0 })); layer.scale(1); - layer.padding = 30; - layer.setCoords(); - assert.ok(layer.containsPoint({ x: 50, y: 120 })); - assert.ok(!layer.containsPoint({ x: 100, y: 170 })); - assert.ok(!layer.containsPoint({ x: 0, y: 0 })); + layer.group.padding = 30; + layer.group.triggerLayout(); + console.log(layer.width,layer.height) + assert.ok(containsPoint({ x: 50, y: 120 })); + assert.ok(!containsPoint({ x: 100, y: 170 })); + assert.ok(!containsPoint({ x: 0, y: 0 })); }); QUnit.test('forEachObject', function(assert) { @@ -385,6 +432,33 @@ }); }); + QUnit.test('fromObject after layout', function (assert) { + var done = assert.async(); + var layer = makeLayerWith2ObjectsWithOpacity(true); + + assert.ok(typeof fabric.Layer.fromObject === 'function'); + var groupObject = layer.toObject(); + + fabric.Layer.fromObject(groupObject).then(function (newGroupFromObject) { + + var objectFromOldGroup = layer.toObject(); + var objectFromNewGroup = newGroupFromObject.toObject(); + + assert.ok(newGroupFromObject instanceof fabric.Layer); + + assert.deepEqual(objectFromOldGroup.objects[0], objectFromNewGroup.objects[0]); + assert.deepEqual(objectFromOldGroup.objects[1], objectFromNewGroup.objects[1]); + + // delete `objects` arrays, since `assertHashEqual` fails to compare them for equality + delete objectFromOldGroup.objects; + delete objectFromNewGroup.objects; + + assert.deepEqual(objectFromOldGroup, objectFromNewGroup); + + done(); + }); + }); + QUnit.test('fromObject with clipPath', function(assert) { var done = assert.async(); var clipPath = new fabric.Rect({ @@ -419,7 +493,7 @@ QUnit.test('fromObject restores oCoords', function(assert) { var done = assert.async(); - var layer = makeLayerWith2ObjectsWithOpacity(); + var layer = makeLayerWith2ObjectsWithOpacity(true); var groupObject = layer.toObject(); groupObject.subTargetCheck = true; @@ -444,21 +518,21 @@ }); QUnit.test('toSVG', function(assert) { - var layer = makeLayerWith2Objects(); + var layer = makeLayerWith2Objects(true); assert.ok(typeof layer.toSVG === 'function'); var expectedSVG = '\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; assert.equal(layer.toSVG(), expectedSVG); }); QUnit.test('toSVG with a clipPath', function(assert) { - var layer = makeLayerWith2Objects(); + var layer = makeLayerWith2Objects(true); layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); var expectedSVG = '\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; assert.equal(layer.toSVG(), expectedSVG); }); QUnit.test('toSVG with a clipPath absolutePositioned', function(assert) { - var layer = makeLayerWith2Objects(); + var layer = makeLayerWith2Objects(true); layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); layer.clipPath.absolutePositioned = true; var expectedSVG = '\n\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n'; @@ -466,7 +540,7 @@ }); QUnit.test('toSVG with a layer as a clipPath', function(assert) { - var layer = makeLayerWith2Objects(); + var layer = makeLayerWith2Objects(true); layer.clipPath = makeLayerWith2Objects(); var expectedSVG = '\n\n\t\t\n\t\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; assert.equal(layer.toSVG(), expectedSVG); @@ -474,7 +548,7 @@ QUnit.test('cloning layer with 2 objects', function(assert) { var done = assert.async(); - var layer = makeLayerWith2Objects(); + var layer = makeLayerWith2Objects(true); layer.clone().then(function(clone) { assert.ok(clone !== layer); assert.deepEqual(clone.toObject(), layer.toObject()); From 4641e37f0172fc1801b6f4dfa05c0e9d532dc549 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 5 Apr 2022 18:29:28 +0300 Subject: [PATCH 52/77] fix(): getObjectsBBox when layer/group are empty --- src/shapes/group.class.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 83b78a9c0c1..af566de7827 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -795,10 +795,13 @@ if (objects.length === 0) { return null; } - var objCenter, sizeVector, min, max, a, b; + var objCenter, sizeVector, min = new fabric.Point(0, 0), max = new fabric.Point(0, 0), a, b; objects.forEach(function (object, i) { if (object instanceof fabric.Layer) { var bbox = object.getObjectsBoundingBox(object._objects.slice(0)); + if (!bbox) { + return; + } sizeVector = object._getTransformedDimensions({ width: bbox.width, height: bbox.height From 5728ba6a7ee728b5e5f73238fadd85a4afd3adf4 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 6 Apr 2022 10:21:24 +0300 Subject: [PATCH 53/77] Update group.class.js --- src/shapes/group.class.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index af566de7827..be41b2ac380 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -818,13 +818,13 @@ cos = Math.abs(fabric.util.cos(rad)), rx = sizeVector.x * cos + sizeVector.y * sin, ry = sizeVector.x * sin + sizeVector.y * cos; - sizeVector = new fabric.Point(rx, ry); + sizeVector.setXY(rx, ry); } a = objCenter.subtract(sizeVector); b = objCenter.add(sizeVector); if (i === 0) { - min = new fabric.Point(Math.min(a.x, b.x), Math.min(a.y, b.y)); - max = new fabric.Point(Math.max(a.x, b.x), Math.max(a.y, b.y)); + min.setXY(Math.min(a.x, b.x), Math.min(a.y, b.y)); + max.setXY(Math.max(a.x, b.x), Math.max(a.y, b.y)); } else { min.setXY(Math.min(min.x, a.x, b.x), Math.min(min.y, a.y, b.y)); From 25c83404e42b9a50380997152f2643ea9eb998a9 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Wed, 6 Apr 2022 12:34:15 +0300 Subject: [PATCH 54/77] Update layer.js --- test/unit/layer.js | 67 ++++++++++++++-------------------------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/test/unit/layer.js b/test/unit/layer.js index 697597e5c65..76a4df8c063 100644 --- a/test/unit/layer.js +++ b/test/unit/layer.js @@ -359,9 +359,9 @@ assert.equal(firstObject.get('top'), initialTopValue, 'should restore initial top value'); }); - QUnit.test.only('containsPoint', function(assert) { + QUnit.test('containsPoint', function(assert) { - var layer = makeLayerWith2Objects(true, true, true); + var layer = makeLayerWith2Objects(true); layer.group.setCoords(); // Rect #1 top: 100, left: 100, width: 30, height: 10 @@ -370,41 +370,22 @@ assert.ok(typeof layer.containsPoint === 'function'); function containsPoint(p) { - var localPoint = fabric.util.sendPointToPlane(p, null, layer.group.calcTransformMatrix()); - console.log(localPoint) - return layer.containsPoint(localPoint); + return layer.group.containsPoint(p); } assert.ok(!containsPoint({ x: 0, y: 0 })); - layer.scale(2); - assert.ok(containsPoint({ x: 50, y: 120 })); + assert.ok(containsPoint({ x: 51, y: 121 })); assert.ok(containsPoint({ x: 100, y: 160 })); assert.ok(!containsPoint({ x: 0, y: 0 })); - layer.scale(1); layer.group.padding = 30; layer.group.triggerLayout(); - console.log(layer.width,layer.height) - assert.ok(containsPoint({ x: 50, y: 120 })); + assert.ok(containsPoint({ x: 51, y: 121 })); assert.ok(!containsPoint({ x: 100, y: 170 })); assert.ok(!containsPoint({ x: 0, y: 0 })); }); - QUnit.test('forEachObject', function(assert) { - var layer = makeLayerWith2Objects(); - - assert.ok(typeof layer.forEachObject === 'function'); - - var iteratedObjects = []; - layer.forEachObject(function(groupObject) { - iteratedObjects.push(groupObject); - }); - - assert.equal(iteratedObjects[0], layer.getObjects()[0], 'iteration give back objects in same order'); - assert.equal(iteratedObjects[1], layer.getObjects()[1], 'iteration give back objects in same order'); - }); - QUnit.test('fromObject', function(assert) { var done = assert.async(); var layer = makeLayerWith2ObjectsWithOpacity(); @@ -505,36 +486,25 @@ }); }); - QUnit.test('fromObject does not delete objects from source', function(assert) { - var done = assert.async(); - var layer = makeLayerWith2ObjectsWithOpacity(); - var groupObject = layer.toObject(); - - fabric.Layer.fromObject(groupObject).then(function(newGroupFromObject) { - assert.equal(newGroupFromObject.objects, undefined, 'the objects array has not been pulled in'); - assert.notEqual(groupObject.objects, undefined, 'the objects array has not been deleted from object source'); - done(); - }); - }); - QUnit.test('toSVG', function(assert) { var layer = makeLayerWith2Objects(true); assert.ok(typeof layer.toSVG === 'function'); - var expectedSVG = '\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; - assert.equal(layer.toSVG(), expectedSVG); + var expectedSVG = '\n\n\t\t\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n\n'; + assert.equal(layer.group.toSVG(), expectedSVG); }); QUnit.test('toSVG with a clipPath', function(assert) { var layer = makeLayerWith2Objects(true); layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); - var expectedSVG = '\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; - assert.equal(layer.toSVG(), expectedSVG); + var expectedSVG = '\n\n\t\t\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n\n'; + assert.equal(layer.group.toSVG(), expectedSVG); }); QUnit.test('toSVG with a clipPath absolutePositioned', function(assert) { var layer = makeLayerWith2Objects(true); layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); layer.clipPath.absolutePositioned = true; + console.log(JSON.stringify(layer.group.toSVG())) var expectedSVG = '\n\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n'; assert.equal(layer.toSVG(), expectedSVG); }); @@ -757,7 +727,8 @@ QUnit.test('layer add', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - layer = new fabric.Layer([rect1]); + layer = new fabric.Layer([rect1]), + group = new fabric.Group([layer], {subTargetCheck:true}); var coords = layer.oCoords; layer.add(rect2); @@ -884,16 +855,18 @@ assert.equal(layer._objects[1].canvas, canvas, 'canvas has been set on object 0'); }); - QUnit.test('add and coordinates', function(assert) { + QUnit.test.only('add and coordinates', function(assert) { var rect1 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), - layer = new fabric.Layer([]); + layer = new fabric.Layer([]), + group = new fabric.Group([layer]); layer.add(rect1); layer.add(rect2); - layer.left = 5; - layer.top = 5; - layer.scaleX = 3; - layer.scaleY = 2; + group.left = 5; + group.top = 5; + group.scaleX = 3; + group.scaleY = 2; + group.triggerLayout(); layer.removeAll(); assert.equal(rect1.top, 5, 'top has been moved'); assert.equal(rect1.left, 11, 'left has been moved'); From 3d8b2e33e21d107abdd652958bc62b265ce49629 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Thu, 7 Apr 2022 08:18:29 +0300 Subject: [PATCH 55/77] Update layer.class.js --- src/shapes/layer.class.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index a77da21db47..2f080b7ffc0 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -101,6 +101,9 @@ this.on('added:initialized', this.__onAdded); this.on('added', this.__onAdded); this.on('removed', this.__onRemoved); + // trigger layout in case parent is passed in options + var parent = this.group || this.canvas; + parent && this.__onAdded({ target: parent }); }, /** From 92823b0c0f369baec126c5347f303ddfad6c8a0d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Fri, 8 Apr 2022 07:05:14 +0300 Subject: [PATCH 56/77] perf(): reduce iteration --- src/shapes/group.class.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 83b78a9c0c1..642d74d682e 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -484,11 +484,13 @@ * @private * @param {fabric.Object} object * @param {fabric.Point} diff + * @param {boolean} [setCoords] perf enhancement, instead of iterating over objects again */ - _adjustObjectPosition: function (object, diff) { + _adjustObjectPosition: function (object, diff, setCoords) { + // layer doesn't need coords so we don't set them if (object instanceof fabric.Layer) { object.forEachObject(function (obj) { - this._adjustObjectPosition(obj, diff); + this._adjustObjectPosition(obj, diff, setCoords); }.bind(this)); } else { @@ -496,6 +498,7 @@ left: object.left + diff.x, top: object.top + diff.y, }); + setCoords && object.setCoords(); } }, @@ -520,20 +523,25 @@ var newCenter = new fabric.Point(result.centerX, result.centerY); var vector = center.subtract(newCenter).add(new fabric.Point(result.correctionX || 0, result.correctionY || 0)); var diff = transformPoint(vector, invertTransform(this.calcOwnMatrix()), true); + var objectsSetCoords = false; // set dimensions this.set({ width: result.width, height: result.height }); + if (!newCenter.eq(center)) { + // set position + this.setPositionByOrigin(newCenter, 'center', 'center'); + // perf: avoid iterating over objects twice by setting coords only on instance + // and delegating the task to `_adjustObjectPosition` + this.callSuper('setCoords'); + objectsSetCoords = this.subTargetCheck; + } // adjust objects to account for new center !context.objectsRelativeToGroup && this.forEachObject(function (object) { - this._adjustObjectPosition(object, diff); + this._adjustObjectPosition(object, diff, objectsSetCoords); }, this); // clip path as well !isFirstLayout && this.layout !== 'clip-path' && this.clipPath && !this.clipPath.absolutePositioned - && this._adjustObjectPosition(this.clipPath, diff); - if (!newCenter.eq(center)) { - // set position - this.setPositionByOrigin(newCenter, 'center', 'center'); - this.setCoords(); - } + && this._adjustObjectPosition(this.clipPath, diff, objectsSetCoords); + } else if (isFirstLayout) { // fill `result` with initial values for the layout hook From 46ef26c158525e3ee9a83239015ea86afdc47af9 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 9 Apr 2022 09:39:45 +0300 Subject: [PATCH 57/77] Update layer.class.js --- src/shapes/layer.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 2f080b7ffc0..38bef341f16 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -132,7 +132,7 @@ this._applyLayoutStrategy({ type: 'group' }); this.group.on('layout', this.__groupMonitor); } - else if (target instanceof fabric.Canvas) { + else if (target instanceof fabric.StaticCanvas) { this._applyLayoutStrategy({ type: 'canvas' }); this.canvas.on('resize', this.__canvasMonitor); } From e9cbaa096baa40ee7d7b136247ff81dbfd8517ce Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 9 Apr 2022 10:07:50 +0300 Subject: [PATCH 58/77] fix(): apply transformations only on objects --- src/shapes/layer.class.js | 42 ++++++++++++++++++++++++++++++++++++++ src/shapes/object.class.js | 14 ++++++++++--- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index 38bef341f16..c18673b33b9 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -106,6 +106,48 @@ parent && this.__onAdded({ target: parent }); }, + /** + * @override we want instance to fill parent so we disregard transformations + * @param {CanvasRenderingContext2D} ctx Context + */ + transform: function (ctx) { + var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); + ctx.transform(1, 0, 0, 1, m[4], m[5]); + }, + + /** + * @override apply instance's transformations on objects + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function (ctx) { + this._renderBackground(ctx); + ctx.save(); + var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); + ctx.transform(m[0], m[1], m[2], m[3], 0, 0); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].render(ctx); + } + ctx.restore(); + this._drawClipPath(ctx, this.clipPath); + }, + + /** + * @private + * @override we want instance to fill parent so we disregard transformations + * @returns {fabric.Point} dimensions + */ + _getTransformedDimensions: function () { + return this.callSuper('_getTransformedDimensions', { + scaleX: 1, + scaleY: 1, + skewX: 0, + skewY: 0, + width: this.width, + height: this.height, + strokeWidth: 0 + }); + }, + /** * we need to invalidate instance's group if objects have changed * @override diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index c91e9adc063..a1e2cb8e14a 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -817,14 +817,22 @@ this._setOptions(options); }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx + * @returns {boolean} true if object needs to fully transform ctx + */ + needsFullTransform: function (ctx) { + return (this.group && !this.group._transformDone) || + (this.group && this.canvas && ctx === this.canvas.contextTop); + }, + /** * Transforms context when rendering an object * @param {CanvasRenderingContext2D} ctx Context */ transform: function(ctx) { - var needFullTransform = (this.group && !this.group._transformDone) || - (this.group && this.canvas && ctx === this.canvas.contextTop); - var m = this.calcTransformMatrix(!needFullTransform); + var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); }, From 590ba589ab18c5e90f5e2a179c62f26a5f82b01f Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 9 Apr 2022 10:39:47 +0300 Subject: [PATCH 59/77] Update group_layout.js --- test/visual/group_layout.js | 58 +++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/test/visual/group_layout.js b/test/visual/group_layout.js index c210abc0e0e..e7fc0b5a626 100644 --- a/test/visual/group_layout.js +++ b/test/visual/group_layout.js @@ -10,7 +10,7 @@ var tests = []; - function createGroupForLayoutTests(text, options) { + function createObjectsForLayoutTests(text) { var circle = new fabric.Circle({ left: 100, top: 50, @@ -27,11 +27,19 @@ fill: 'red', opacity: 0.3 }) - return new fabric.Group([ + return [ rect, circle, itext - ], options); + ]; + } + + function createGroupForLayoutTests(text, options) { + return new fabric.Group(createObjectsForLayoutTests(text), options); + } + + function createLayerForLayoutTests(text, options) { + return new fabric.Layer(createObjectsForLayoutTests(text), options); } function fixedLayout(canvas, callback) { @@ -119,7 +127,8 @@ }); function fitContentNestedLayer(canvas, callback) { - var g = createGroupForLayoutTests('fit-content layout', { + var g = createGroupForLayoutTests('fixed layout,\nlayer on top', { + layout: 'fixed', backgroundColor: 'blue' }); var objects = g.removeAll(); @@ -131,14 +140,51 @@ } tests.push({ - test: 'fit-content with nested layer', + test: 'layer nested in group', code: fitContentNestedLayer, - golden: 'group-layout/fit-content-nested-layer.png', + golden: 'group-layout/nested-layer.png', + percentage: 0.06, + width: 400, + height: 300 + }); + + function LayerLayout(canvas, callback) { + var layer = createLayerForLayoutTests('Layer', { + backgroundColor: 'blue', + }); + canvas.add(layer); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } + + tests.push({ + test: 'layer', + code: LayerLayout, + golden: 'group-layout/layer.png', percentage: 0.06, width: 400, height: 300 }); + function LayerLayoutWithSkew(canvas, callback) { + var layer = createLayerForLayoutTests('Layer', { + backgroundColor: 'blue', + skewX: 45 + }); + canvas.add(layer); + canvas.renderAll(); + callback(canvas.lowerCanvasEl); + } +/* + tests.push({ + test: 'layer with skewX', + code: LayerLayoutWithSkew, + golden: 'group-layout/layer-skewX.png', + percentage: 0.06, + width: 400, + height: 300 + }); +*/ function nestedLayout(canvas, callback) { var rect3 = new fabric.Rect({ width: 100, From 329db01695d2ae97d7a3cfe9ba76a392aef7dbe8 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 9 Apr 2022 10:40:02 +0300 Subject: [PATCH 60/77] replace golden --- .../group-layout/fit-content-nested-layer.png | Bin 7140 -> 0 bytes .../visual/golden/group-layout/nested-layer.png | Bin 0 -> 10529 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/visual/golden/group-layout/fit-content-nested-layer.png create mode 100644 test/visual/golden/group-layout/nested-layer.png diff --git a/test/visual/golden/group-layout/fit-content-nested-layer.png b/test/visual/golden/group-layout/fit-content-nested-layer.png deleted file mode 100644 index 034a603df20bb12a28f8df2037aea0e5c6c13165..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7140 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 diff --git a/test/visual/golden/group-layout/nested-layer.png b/test/visual/golden/group-layout/nested-layer.png new file mode 100644 index 0000000000000000000000000000000000000000..be7d767ef69837f7b18d4114d016eb1577b5ba1f GIT binary patch literal 10529 zcmd6NbyQT{yZZSE;#)E?1!8BYts8M2E30|5WnCGzNKHp%;Nfi z*%CNZR3KPCD4C}(-}1$IdK%G0Wx13c31K2<42_W2PJv?MV9I8+{WU> zj#-#;rk!D?{#AmK=I$FRcwnIjL@Q$`gjh5-U$V7#!l2H;FTHD|rZQwBZgB<1!d~-5V z39t4)NM{GqHtq;5)z1Y>?Zw#%vexFjTfm76BqOYG$;-T+E+>p;G;Qwd%0dJYc7G|$W191cV|B}eAMyul<_9Noc`7SV z`2e$#Gol^+UMixR11XpKvh*y7G40ud#zWODFEz!&PsHrekMY&g!5%wAJAVtMz9b|k zluzxQ?QC*J*{SVnxOHr?j6usPjNT*wjJ*U~u0Te$R6MN+h1_j~Z}hn?;w z5^M;zk1|9s-g(qta@uh-#z3s;Z3QM4Hg4!|)yfb@#VS@;8W*baj$v>$p_X7`Hd|Yv zr_0%pX0~si0xpAxvqG4HCT?#6extOX2Aej6K9Qqhf0?Q` zjbsZ=eK1+y0QdE2RlZRCrwi}?%IO}yQ>&)0X}pbcENGz_rv;9R4dwQK5)xXIue_ey zyQp;2H%#Ev9AWAo@%|$*yW(T~dH2t1e(E;^y>ULB5;JinXK8Zs6Kj2LpYQTC|Bec= z9rsXvPIUnYM+G}TuVqT5B1gr%`2619d+>--C0x?y{w6}!t%&RKW8`pO(tfe}7BHa4 z(~!_K*VUgvmcPallJTXnt$W#y&80TEjr1;3m8Ap*vwZGq` zbt329JWF>~oa)xEQjxv;S4Y8JdAHJk0O_4aVB>l~ zS;+crKes3AAvtCE(6>3Kt(6#g~0@f~S$4t%1?o`;1) z%OII^ML3mWUI_88?zcl@49~5@%?`+ z^rzlh&@IW80<&IPg1@DodEX8p|Negz{t|vEM1Z<_+ zs|@f^1}gUbzgzCGBnNDHQJ#XQvK$pHa0VPUHFDKx7Bm%8Hs4uvf^L0)0NCDgQ@YK0 zLpW7@p}l^tI>Ud9Gw7bm!|~!_YbwAtetyd~cCf1axr>EKlsL2;ytr=n12+Yr%Kn&`8e(+o#5)Fy z5|P{*fNW#dCghjmf@bI^0@C2(oGDF7nTxCgGYu$P*YzTwE@Yr+R@T z@rG73!Sx0nUyWDL z?Eg`gQzCG=+-=ThVFi>PGLRnc5MLeM**`~idQSJbd);RsH3@1{o2+$ZbLnA_OBZU+ zXJrL(mrH@h1bIjr*B|=)TVeF2-&~wrSgNXmFHUg7$nXW)bj;~*;~A0RL>0g38|4_V zm_yuY0&JjwcYkY~1Jm??7d&l(>#sJk6a79HNz!vM*;3pvz_cZ0wv;(v4y1E!e~k*h$7X$n4+75d zXi*G%N_EE+ty{$3HsezTOV-iCsQHLdbrc&39PpHtQI_pq_8Eiedlfomw@; zo8^v|MBb~UlBo6*J=^IgbkYWIM@+w~1b&Vs=ugHq^Z$0d%Ezy7&QC7q1Z%N)elRuN z+Vs@N%IJ%l<7*E4RVC<@gVnztZ}C^;e9M-s^s{VA-N7Bf=vF#@-D<}96H|Xzmucm_ z1wX;jd_qc<5-aexCZNvr+KF2yc`%<^YH%;@N#3z#ZQ_}?U-f4b7_9HrpzrZ01M!1L zb8-$&t4dbsCr4F0dUy9Oq&aL!vN4EMGRE6N#$j*&ja54FFWnmFf9xBKFTyfpZ$O-{ zfYgY82W#rEx2b8BjsX{I)H~%7^JYUDPaQ#N0W27 zhs4@m$Yi;(xp;L;%}-SS5Pk~ZL3M=`m%7T;Co-t6b|R--*0V*#D%7ifB8y4p?nDX8 z2lidJ7haojJJUc(R%9q@-(TPS@(drp-7n^3Z}(ym|IS+5TZo_PXDMeAl3-bEQqOGN z*u8vPZ8SXS_*i^pPYP39p*2>w8$uY2OBgNm5u1&gh~Jx_dggE_sPc9rs3Q9As?Sla zc1Pe;(00cSSmq3Lwy$6@Ex^q2`{JVLRp(1V9OHU}X-X#Y)}z=TJ6q#I!)rZEm7Y^T z-g+&&D;ZKyJb-*_4bnGZvB~bZ;i>-N#BUni;(7Gr(XXA^&b7YU*oXY=rIp59#89~Y zdNMCewxAHlaZdHJZ)51beGF|U1k)QB9`ST>)us0=GfT>JTEDJnkl6)QSw+k)s;f>E zOCOIA>@YdaE5>tM3-`cgKUMHWPFC{bvWq(c-*)ZH;3p23H`11EV(wkKf_ zO-+sJ)|CKAvHJ4`4@w<>>50K~4YE%Me-BnQK1;KrO0|1Bt|@n}Ay#1yHYeK}#`V-A zIgmRioB7?(2=2NBRCnQ=3`^bbs51d>jb~f5x;#*Re3^sw7Hn|inE3rjLDUZKM_}8@ z4^LDstw`L7P6d&uO)TB9H(ibI2p{=b>S?ptdJJ~Dd^`XDt zaZ>mb%M<2JESBU(+yiw>KpuNL5YFO8iS*PM);mDLd$(Z3308w1bOc>1kV8F*LG;o@ z^Zvpv+etqLi(Uyyu3`{=jJ-s?A*ghl0#e|>2BcS+#>|E!gBktQ$bNo=9E55@Ch|TY zNBp-R=c+y!sZLD-x)gV$wIs_wCnqJ`OJ(KJi1G1-YUFxqh~1?1`I#$_-_SWN7C2Ab z3Xs!BM=)kvvuw4htp8Hst>0AKK(UGL&-hhcq$c}lj_~QOmx{uFT%4qRtq!7Bz#^lAK zTNKg_^yFQfi7t3-_RW-J*Vk=DaqftNW3wc(f2diIsQW~%bKrTN|b3_RU}PI^W{gPeNl;v!vm zsvtfC6inGxaPITzzI#r?uzM-<1gN=2^V}whKso;4 z*3&N{HI(N-<#zW0M+j)FW$R=*COy#HUvqop6X{X4ncuEae$>`*)*_{fTJ$l!d-Eo% zl_XcsrkMZv`ps{~=vnV59szaV+6VQviP5z;$J+Mdvp*M{hwP>aE>4r{6W)W-9Q1I&05*jl&(uHV{oUPok9qH{)V9(4ZYrU+(PRi#iLBTi$w083n zC@TKj0Bw%h%bt!;dCPYx)%J3k;Ix7dyQsYOON`1Mm<#$>E(@) zT9v)qHNBm9_m_vW601s9dyJKJWHFgfx34xewH7m3#w~hABsI$4|g`?Z#&JSx_Js#J1mb1q_|KQ)z zpvQ0jSrS?g&0Via|gE9u4CWdox-Hjk+X;&UAp!h@DczYis4g)cUPaM+XKW-v8av z-gM-(>~k25rUSVlA8^S(K`twoC9!z{nWhH?Q3m_Wq|?t4(t6I z9|)je)mOHK*ZEub8YN0bWbQcRWMCFfrd!&xGw$ft6=`yE64*^}mypW@ z(>?{4+3BXY%}Aqmem@#`e$k9tvq15N0jXyqXFUMe@ZIe$mq{NR#Kdk zf_FCW-adW4-!+x^9;o{Lut!WKr@OF}jT2PmoWqU4AQwZ>)tjuRzGp78;+T0hC~{Nt zaf56AQ$Jzm0rNoH7yzvy@L*y;cL{KqWwo)}ool&KsCOV1b^JolJTxBeJPSTKozrMo z*tp0Jyk1kAGL+#b-R|dI|KYa2rsmg0FfFk$^uc%gu_X7GY5k^JlUb^49+SrAwErIITo2h_e zs);Y62`YC|U#W#&-1t4NbILmT!K0h|P~ccF`$g0mq|F!q6r-&HI=Mg{LY9MwcOtlQ zWiC|i!!yJZf;%qyJ5JV-3LIR1H;kG3x*O<~!-4hw@dfz7j8zq?=~$}-sJw-oJ9dqV zv^Yv2e$hf|%oi^ni*a4Ir$^Er{>s5f47Bjto28B`kQ<^7+X4dL7#end zeKE#>S%ccTI3djbDn$y}+ba?5ucwYpx!y>-^l^;6tawG|1Ds)tbGNP1iS{-uN|d+1 z-Tj%1oNuM$0uNh9bvx}Q0`b@0_S9i%&bbgiYYIO{Z0@=A`Wt8$sl2Y1ljO2cd!0&f zBm}cw8MLoX4$z&Txzm23`s^OP4BxI$U*A-=u{*Ng0jv;4jmrixtW~^`=}(H!P&q+6 zGYqV_^xj+_D5pOxg1y}OChm~-ofN0D0BRK$%^+o~_ox2-`v?GQdkdhz-~rQ?Dx_E_+BLeJkHV+0DPKY1xw>3mT9=>|{7khws|Sv0;i`kcU^ z%HYlXP3q1EeP%^k2ZR8|#P-k6G$Pj?G8%?h~U2_LjAIVJ?ch827OK#a?gS-zLzkOq+%wLR>8vo+x?UMt?&`F!#?d#kJ$};PhXas8@BV<_ zjp2Fkd|UaPA?Z!wz5ak%(s0Foo*LkxyjO@Vu6?mnuA3B3d}{p(Hx@OTnuH{d!{~yQ zVuJN5gOwR^PIvD}FDCPb@K%8qy}K^1LW!r)8^cnNYBuV*%tW@Q-)*fHHU`y}%&`fJ zp%D=PKA5m0<}7enlt2ogX%RaPxu#OLtQ3!KFu4pUg%2m&mt>}i@(j}vQoz0xC0K!u ziout;AOgrMNg@fRo>&~gc5YqPy}xhts_>LzlWI>;xb8^sBkxA8>nc+>Ip4E4OMmTQ zxod;zZlmTkAZj5+vLY$C5dwfe{I5kvtC=)f>gmJ9oRS!o=IU`Ojd2Cr@Em49S2t4o z+vS~?Xu^8z;Zevps}SFl;N7{xXPX0pAzS0?O~jP!-7E^h`O2mUa}DOp7H4w+*|iv? zo`nxDY9Io*F0t*G);>LC05umJLAkzkfy@c{y--A21uAr|$1qN5h=Fjw92Fn|fU{@inm6%~kNz`%-Le~OGu5Yjin@mvZ%Ir%K} zzRc(W0C-h=D=LtCmCaWTIi&^?h=!q|#z3mV4?9@WOLc*)j${j`qiCAiwq)KGr=t(# z@{?uwqMkexMXxlZyyS+m>=)jG?5@r_8{jLyo_=_VG$bB_Ag2eNqA@p31F5)OVK|S>Hg8T6xe6nLx@HAS`*W`WCQ zCD_F9c$e2bMUs@Bh+dL)QWFa8J`%R+*PTe=cT+;-;ks-Ux^dLMi@!^Yzcwh#TfX^? zkcWCs+M~q3;S*WkWcBzbrv3-ZDf~hluj}5*eIPiz$q58sV8r`lnaHoM+(*Sfs}cj< zY45ODij^e*g6WSKcuhY9gG^h%5Z~TftbG40({;ZNK*3{27by!(e%*{7`(?RN1Za255 zQJ21Tm{19XO9Ugc?>I=Q$O33PZ;0$D2t?F>`z}CSCLgv!w;T-dYZi1r_}zV2s+~P8 z>ihkhbU-swY3o&nby@O&AROD=g`<|iInZ8YRcekf5a0&sRl4z8F~%eEKG0Wuc=J~z zpRwIo91Rm})u<}TGz*yYRob&=yqYab#{8Pf4f(dCmdR1cUf<*fvuZMxf^KF5FpKnM z#lhuR;oh9VEj9B~PAfqNadq7wqLu+LhtfyUCw4!LUl%$pMDwHtTo$;_XHDo>)TGjG zM&FGfj1yLOZfjli9k^KRxN$QzhpKJ49dkbkYUfK8M7Kv)))+gBreLqeuN$Mj4l-)A zHGeaqW$S%uml0KFE^8$#NxxUF+jl#vqC>?WYGX|ttuzK_+cIfW$6I0=g;2Ry3S~tF%Ap>i~w~0 zX>6*Qn>iu6FOH@A9}fOkv@$~}f~h0>^}}q+(Xhg39BCrOYaqmB+BAeAnpY=|QA1t3 z$iO^DbinWAfkoyz_!QD2qQA}{yN5^%E#bBg=x7PElmd)pR0qpD$|hI<)V8gy7DG*7 z+jnv4!|*>45e;&Pb*ATVj-Yr0IC#9c@K2q-AR1k*_1mBxRbqA2*&}Ro#WaZANyY=E zn%tj=Q_ak(pPi>RPvW(%1&AJs)QmgxZc+3za}DAbyN~qSe3cR;f)=US4$qN7qLI~M zmPgOuqV%vm64BHwtDLF|Q4gmO*Sks?8N9N0rdL0`ADej_RAmQB&nAdl^q)3aMfmla z0nF3<*B468Tt^lH&R32V45Ix80EC&4Bs-}e?D0c%I-}$C3mUUC%Vzp1|K(P7E$n6b z_<-#nRs>oy;BQGa`cK!oN05p;3YD4d zwN$TJ-U)3g{!%P;Kc!m?xppQ{2e1CaaMs5akjmu;aI%s|vzaY4_CqllFH;(s;(quy zsg*9TIeD4B9wf{-ScN?!IZFO0j=gF46AmPzsY1rY@$m`{=EV_vIJZ%vEzo+gE9*%9E5fPZRD4 zM=|5Jzi`p10%+}S!~6>3Szk7Q$q)N8yNGL@U!Y=may}5kMpNtFcp%j!t#-xd8I$=h96_78d%WwAi!rxmqs%I)mf<}Dr&Oywm%Dmj>~gI&a?QEz?JuF9M!EiURG z;mUUD5OBA=yOF8s>ME3)<}Oo(S;UpOo(PzYzB`-E`O4Z!@U_>jP)a~djf~fzfxz(h zy+aZ#vT41l@3G9{#`S6|?q6gPB`Cq2X>*ud(Ks@vC9va#>yW4r`2*(h zNfCR_Ai|-Hfrv04`F;y0_cxPeeIf^N1V3@_IfOxixq)bNR{NP{g_&d9`=ZeqzQ z9p=MEQEn%1#NNftcK}SKJ-TAX07}mkz)raMv+hDYjd?-2x?|CoGbfd6lJO%(!4;TT zJ`&-t?OWG&`ekRNKN)s9KO&o|cy=eATn=8hjYYK#Wk*xv45s5=9em1kNfH!90cog=SN2kE zU@Nz?nL5g$4@5|uc8r0sjNj50~7h9$~ z6+w;*AWX+Q2cq5;5|=F0>X~u4-()Y#xxb}!d#$fS!3Sccc~UQC-`2UVt`*?Li6y(P zS{pt-&xnrkY$M^Xf+lv~rIGSV_*_M!lW^rgRyd~9?!QDxegu+%NBUZT*j-J0ZCw%w znF&RM$Q*vplK-Pont+8x-M6=O(9cB)_KRJz@kHhicF~mA^l}CD1X5^P1Egd1;n8}S zO2FlIY-spS7;$Lp7hz@Jm9sKhHh|m}qd4r9B%A_Xm0@FP5hf2m7WGc}NmwmMG&?KQ z-}oGtMxxR8M|32ks44*`C+Bu-+|IBxb3#LL480n2sLzo#1!6dBOu0jwE8VfdPIp*a zu^qrF>u4qTO^B`zquRYojse1xfHc7`4|mMG!`+5v>-y&%-&IyWcd~GYKR=(54j_UC zN-$?$GVcpvMGZxi>Nj=+lB13V+(K|w%C1?@gw5` zE(9v8_vRizruMIZLwo48TJve{{a^jWn_&shdJN9}$W5LA?>vH_FVq#Q Date: Sat, 9 Apr 2022 10:40:06 +0300 Subject: [PATCH 61/77] Create layer.png --- test/visual/golden/group-layout/layer.png | Bin 0 -> 4704 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/visual/golden/group-layout/layer.png diff --git a/test/visual/golden/group-layout/layer.png b/test/visual/golden/group-layout/layer.png new file mode 100644 index 0000000000000000000000000000000000000000..00df0f0a2ba4ce8fc6dd42f35a3db71903351a7c GIT binary patch literal 4704 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 Date: Sat, 9 Apr 2022 13:10:36 +0300 Subject: [PATCH 62/77] fix setting `initialTransform` --- src/shapes/group.class.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 642d74d682e..a7256f8674c 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -96,7 +96,8 @@ this.__objectSelectionTracker = this.__objectSelectionMonitor.bind(this, true); this.__objectSelectionDisposer = this.__objectSelectionMonitor.bind(this, false); this._firstLayoutDone = false; - this.callSuper('initialize', options); + // setting angle, skewX, skewY must occur after initial layout + this.callSuper('initialize', Object.assign({}, options, { angle: 0, skewX: 0, skewY: 0 })); this.forEachObject(function (object) { this.enterGroup(object, false); object.fire('added:initialized', { target: this }); @@ -516,6 +517,12 @@ // reject layout requests before initialization layout return; } + var options = isFirstLayout && context.options; + var initialTransform = options && { + angle: options.angle || 0, + skewX: options.skewX || 0, + skewY: options.skewY || 0, + }; var center = this.getRelativeCenterPoint(); var result = this.getLayoutStrategyResult(this.layout, this._objects.concat(), context); if (result) { @@ -526,9 +533,10 @@ var objectsSetCoords = false; // set dimensions this.set({ width: result.width, height: result.height }); - if (!newCenter.eq(center)) { + if (!newCenter.eq(center) || initialTransform) { // set position this.setPositionByOrigin(newCenter, 'center', 'center'); + initialTransform && this.set(initialTransform); // perf: avoid iterating over objects twice by setting coords only on instance // and delegating the task to `_adjustObjectPosition` this.callSuper('setCoords'); @@ -541,7 +549,6 @@ // clip path as well !isFirstLayout && this.layout !== 'clip-path' && this.clipPath && !this.clipPath.absolutePositioned && this._adjustObjectPosition(this.clipPath, diff, objectsSetCoords); - } else if (isFirstLayout) { // fill `result` with initial values for the layout hook @@ -551,6 +558,7 @@ width: this.width, height: this.height, }; + initialTransform && this.set(initialTransform); } else { // no `result` so we return From 5a38d5e004dcc84135dc5cf2b89792c4dc557b57 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Mon, 11 Apr 2022 07:29:48 +0300 Subject: [PATCH 63/77] fix(): fire `resize`/`window:resize` --- src/mixins/canvas_events.mixin.js | 2 +- src/static_canvas.class.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index fcaedb0eb75..b6f5519cc4b 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -385,7 +385,7 @@ */ _onResize: function (e) { this.calcOffset(); - this.fire('resize', { e: e }); + this.fire('window:resize', { e: e }); }, /** diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index f7e31d8b057..8ebf865d15b 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -405,6 +405,7 @@ } this._initRetinaScaling(); this.calcOffset(); + this.fire('resize', dimensions); if (!options.cssOnly) { this.requestRenderAll(); From 98afe42ad61439a7b60da2f863eb21f29ab574f1 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 17 Apr 2022 11:30:36 +0300 Subject: [PATCH 64/77] POC: use layer to render brush --- src/brushes/base_brush.class.js | 33 +++++++++++++++++++++++++++++-- src/brushes/pencil_brush.class.js | 26 +++++++++++++----------- src/canvas.class.js | 6 +++++- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/brushes/base_brush.class.js b/src/brushes/base_brush.class.js index 8f5105c45c1..ebe9463c9c7 100644 --- a/src/brushes/base_brush.class.js +++ b/src/brushes/base_brush.class.js @@ -64,13 +64,39 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype limitedToCanvasSize: false, + initialize: function (layer) { + this.layer = layer; + this.canvas = this.layer.canvas; + var ctx = this.getContext(); + ctx.save(); + ctx.translate(-this.layer.width / 2, -this.layer.height / 2); + }, + + _isMainEvent: function (e) { + return this.canvas._isMainEvent(e); + }, + + getContext: function () { + if (!this.layer._cacheCanvas || !this.layer._cacheContext) { + this.layer._createCacheCanvas(); + } + return this.layer._cacheContext; + }, + + resetContext: function () { + var canvas = this.layer._cacheCanvas, ctx = this.layer._cacheContext; + //console.log('reset') + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, canvas.width, canvas.height); + }, /** * Sets brush styles * @private * @param {CanvasRenderingContext2D} ctx */ - _setBrushStyles: function (ctx) { + _setBrushStyles: function () { + ctx = this.getContext(); ctx.strokeStyle = this.color; ctx.lineWidth = this.width; ctx.lineCap = this.strokeLineCap; @@ -86,8 +112,11 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ _saveAndTransform: function(ctx) { var v = this.canvas.viewportTransform; + + this.layer._updateCacheCanvas(); ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + //ctx.translate(-this.layer.width / 2, -this.layer.height / 2); + // ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); }, /** diff --git a/src/brushes/pencil_brush.class.js b/src/brushes/pencil_brush.class.js index 83539c4c5c3..8fbbd323c2e 100644 --- a/src/brushes/pencil_brush.class.js +++ b/src/brushes/pencil_brush.class.js @@ -34,8 +34,8 @@ * @param {fabric.Canvas} canvas * @return {fabric.PencilBrush} Instance of a pencil brush */ - initialize: function(canvas) { - this.canvas = canvas; + initialize: function(layer) { + this.callSuper('initialize', layer); this._points = []; }, @@ -58,7 +58,7 @@ * @param {Object} pointer */ onMouseDown: function(pointer, options) { - if (!this.canvas._isMainEvent(options.e)) { + if (!this._isMainEvent(options.e)) { return; } this.drawStraightLine = options.e[this.straightLineKey]; @@ -74,7 +74,7 @@ * @param {Object} pointer */ onMouseMove: function(pointer, options) { - if (!this.canvas._isMainEvent(options.e)) { + if (!this._isMainEvent(options.e)) { return; } this.drawStraightLine = options.e[this.straightLineKey]; @@ -85,11 +85,11 @@ if (this.needsFullRender()) { // redraw curve // clear top canvas - this.canvas.clearContext(this.canvas.contextTop); + this.getContext().reset(); this._render(); } else { - var points = this._points, length = points.length, ctx = this.canvas.contextTop; + var points = this._points, length = points.length, ctx = this.getContext(); // draw the curve update this._saveAndTransform(ctx); if (this.oldEnd) { @@ -98,6 +98,7 @@ } this.oldEnd = this._drawSegment(ctx, points[length - 2], points[length - 1], true); ctx.stroke(); + this.layer.drawCacheOnCanvas(this.canvas.contextTop); ctx.restore(); } } @@ -107,12 +108,13 @@ * Invoked on mouse up */ onMouseUp: function(options) { - if (!this.canvas._isMainEvent(options.e)) { + if (!this._isMainEvent(options.e)) { return true; } this.drawStraightLine = false; this.oldEnd = undefined; this._finalizeAndAddPath(); + this.resetContext(); return false; }, @@ -126,7 +128,7 @@ this._reset(); this._addPoint(p); - this.canvas.contextTop.moveTo(p.x, p.y); + this.getContext().moveTo(p.x, p.y); }, /** @@ -151,7 +153,7 @@ */ _reset: function() { this._points = []; - this._setBrushStyles(this.canvas.contextTop); + this._setBrushStyles(this.getContext()); this._setShadow(); this._hasStraightLine = false; }, @@ -174,7 +176,7 @@ var i, len, p1 = this._points[0], p2 = this._points[1]; - ctx = ctx || this.canvas.contextTop; + ctx = ctx || this.getContext(); this._saveAndTransform(ctx); ctx.beginPath(); //if we only have 2 points in the path and they are the same @@ -279,7 +281,7 @@ * and add it to the fabric canvas. */ _finalizeAndAddPath: function() { - var ctx = this.canvas.contextTop; + var ctx = this.getContext(); ctx.closePath(); if (this.decimate) { this._points = this.decimatePoints(this._points, this.decimate); @@ -295,7 +297,7 @@ } var path = this.createPath(pathData); - this.canvas.clearContext(this.canvas.contextTop); + this.getContext().reset(); this.canvas.fire('before:path:created', { path: path }); this.canvas.add(path); this.canvas.requestRenderAll(); diff --git a/src/canvas.class.js b/src/canvas.class.js index 9992f735d0b..3fbab985888 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -393,7 +393,11 @@ this._initRetinaScaling(); - this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); + if (fabric.PencilBrush) { + var drawingLayer = new fabric.Layer([], { canvas: this, objectCaching: true }); + this.add(drawingLayer); + this.freeDrawingBrush = new fabric.PencilBrush(drawingLayer); + } this.calcOffset(); }, From 37cc9bffda66ffd5aa667fd9a78bc5bf23207c4d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 17 Apr 2022 11:31:31 +0300 Subject: [PATCH 65/77] Update static_canvas.class.js --- src/static_canvas.class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 8ebf865d15b..07818a8973c 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -401,7 +401,7 @@ } } if (this._isCurrentlyDrawing) { - this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(this.contextTop); + this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(); } this._initRetinaScaling(); this.calcOffset(); From 56e17f2a54e14da6a376259a6512fde3933a5b61 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 17 Apr 2022 18:39:19 +0300 Subject: [PATCH 66/77] Update pencil_brush.class.js --- src/brushes/pencil_brush.class.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/brushes/pencil_brush.class.js b/src/brushes/pencil_brush.class.js index 8fbbd323c2e..4776771b6e2 100644 --- a/src/brushes/pencil_brush.class.js +++ b/src/brushes/pencil_brush.class.js @@ -98,6 +98,7 @@ } this.oldEnd = this._drawSegment(ctx, points[length - 2], points[length - 1], true); ctx.stroke(); + this.layer._drawClipPath(this.getContext(), this.layer.clipPath); this.layer.drawCacheOnCanvas(this.canvas.contextTop); ctx.restore(); } From dffe340a6347e0d513782f73dc284444eab23e6e Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sat, 23 Apr 2022 12:51:33 +0300 Subject: [PATCH 67/77] fix(): safegurad from objects without bbox --- src/shapes/group.class.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index a7256f8674c..f353b3e4e1c 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -811,10 +811,13 @@ if (objects.length === 0) { return null; } - var objCenter, sizeVector, min, max, a, b; + var objCenter, sizeVector, min = new fabric.Point(0, 0), max = new fabric.Point(0, 0), a, b; objects.forEach(function (object, i) { if (object instanceof fabric.Layer) { var bbox = object.getObjectsBoundingBox(object._objects.slice(0)); + if (!bbox) { + return; + } sizeVector = object._getTransformedDimensions({ width: bbox.width, height: bbox.height @@ -831,13 +834,13 @@ cos = Math.abs(fabric.util.cos(rad)), rx = sizeVector.x * cos + sizeVector.y * sin, ry = sizeVector.x * sin + sizeVector.y * cos; - sizeVector = new fabric.Point(rx, ry); + sizeVector.setXY(rx, ry); } a = objCenter.subtract(sizeVector); b = objCenter.add(sizeVector); if (i === 0) { - min = new fabric.Point(Math.min(a.x, b.x), Math.min(a.y, b.y)); - max = new fabric.Point(Math.max(a.x, b.x), Math.max(a.y, b.y)); + min.setXY(Math.min(a.x, b.x), Math.min(a.y, b.y)); + max.setXY(Math.max(a.x, b.x), Math.max(a.y, b.y)); } else { min.setXY(Math.min(min.x, a.x, b.x), Math.min(min.y, a.y, b.y)); From 6f28f7fed5700cab0c7ba45604c9050e1ae91c91 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 1 May 2022 00:18:08 +0200 Subject: [PATCH 68/77] remove whitespace --- dist/fabric.js | 4333 ++++++++++++++++----------- src/mixins/object_ancestry.mixin.js | 6 +- 2 files changed, 2597 insertions(+), 1742 deletions(-) diff --git a/dist/fabric.js b/dist/fabric.js index 9b7bf655d2a..d832d46591d 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -358,79 +358,68 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { */ fabric.Collection = { + /** + * @type {fabric.Object[]} + */ _objects: [], /** * Adds objects to collection, Canvas or Group, then renders canvas * (if `renderOnAddRemove` is not `false`). - * in case of Group no changes to bounding box are made. * Objects should be instances of (or inherit from) fabric.Object - * Use of this function is highly discouraged for groups. - * you can add a bunch of objects with the add method but then you NEED - * to run a addWithUpdate call for the Group class or position/bbox will be wrong. - * @param {...fabric.Object} object Zero or more fabric instances - * @return {Self} thisArg - * @chainable + * @private + * @param {fabric.Object[]} objects to add + * @param {(object:fabric.Object) => any} [callback] + * @returns {number} new array length */ - add: function () { - this._objects.push.apply(this._objects, arguments); - if (this._onObjectAdded) { - for (var i = 0, length = arguments.length; i < length; i++) { - this._onObjectAdded(arguments[i]); + add: function (objects, callback) { + var size = this._objects.push.apply(this._objects, objects); + if (callback) { + for (var i = 0; i < objects.length; i++) { + callback.call(this, objects[i]); } } - this.renderOnAddRemove && this.requestRenderAll(); - return this; + return size; }, /** * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) * An object should be an instance of (or inherit from) fabric.Object - * Use of this function is highly discouraged for groups. - * you can add a bunch of objects with the insertAt method but then you NEED - * to run a addWithUpdate call for the Group class or position/bbox will be wrong. - * @param {Object} object Object to insert + * @private + * @param {fabric.Object|fabric.Object[]} objects Object(s) to insert * @param {Number} index Index to insert object at - * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs - * @return {Self} thisArg - * @chainable + * @param {(object:fabric.Object) => any} [callback] */ - insertAt: function (object, index, nonSplicing) { - var objects = this._objects; - if (nonSplicing) { - objects[index] = object; - } - else { - objects.splice(index, 0, object); + insertAt: function (objects, index, callback) { + var args = [index, 0].concat(objects); + this._objects.splice.apply(this._objects, args); + if (callback) { + for (var i = 2; i < args.length; i++) { + callback.call(this, args[i]); + } } - this._onObjectAdded && this._onObjectAdded(object); - this.renderOnAddRemove && this.requestRenderAll(); - return this; }, /** * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) - * @param {...fabric.Object} object Zero or more fabric instances - * @return {Self} thisArg - * @chainable - */ - remove: function() { - var objects = this._objects, - index, somethingRemoved = false; - - for (var i = 0, length = arguments.length; i < length; i++) { - index = objects.indexOf(arguments[i]); - + * @private + * @param {fabric.Object[]} objectsToRemove objects to remove + * @param {(object:fabric.Object) => any} [callback] function to call for each object removed + * @returns {fabric.Object[]} removed objects + */ + remove: function(objectsToRemove, callback) { + var objects = this._objects, removed = []; + for (var i = 0, object, index; i < objectsToRemove.length; i++) { + object = objectsToRemove[i]; + index = objects.indexOf(object); // only call onObjectRemoved if an object was actually removed if (index !== -1) { - somethingRemoved = true; objects.splice(index, 1); - this._onObjectRemoved && this._onObjectRemoved(arguments[i]); + removed.push(object); + callback && callback.call(this, object); } } - - this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); - return this; + return removed; }, /** @@ -447,7 +436,7 @@ fabric.Collection = { */ forEachObject: function(callback, context) { var objects = this.getObjects(); - for (var i = 0, len = objects.length; i < len; i++) { + for (var i = 0; i < objects.length; i++) { callback.call(context, objects[i], i, objects); } return this; @@ -455,17 +444,16 @@ fabric.Collection = { /** * Returns an array of children objects of this instance - * Type parameter introduced in 1.3.10 - * since 2.3.5 this method return always a COPY of the array; - * @param {String} [type] When specified, only objects of this type are returned + * @param {...String} [types] When specified, only objects of these types are returned * @return {Array} */ - getObjects: function(type) { - if (typeof type === 'undefined') { + getObjects: function() { + if (arguments.length === 0) { return this._objects.concat(); } - return this._objects.filter(function(o) { - return o.type === type; + var types = Array.from(arguments); + return this._objects.filter(function (o) { + return types.indexOf(o.type) > -1; }); }, @@ -495,7 +483,9 @@ fabric.Collection = { }, /** - * Returns true if collection contains an object + * Returns true if collection contains an object.\ + * **Prefer using {@link `fabric.Object#isDescendantOf`} for performance reasons** + * instead of a.contains(b) use b.isDescendantOf(a) * @param {Object} object Object to check against * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` * @return {Boolean} `true` if collection contains an object @@ -540,32 +530,6 @@ fabric.CommonMethods = { } }, - /** - * @private - * @param {Object} [filler] Options object - * @param {String} [property] property to set the Gradient to - */ - _initGradient: function(filler, property) { - if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { - this.set(property, new fabric.Gradient(filler)); - } - }, - - /** - * @private - * @param {Object} [filler] Options object - * @param {String} [property] property to set the Pattern to - * @param {Function} [callback] callback to invoke after pattern load - */ - _initPattern: function(filler, property, callback) { - if (filler && filler.source && !(filler instanceof fabric.Pattern)) { - this.set(property, new fabric.Pattern(filler, callback)); - } - else { - callback && callback(); - } - }, - /** * @private */ @@ -629,6 +593,10 @@ fabric.CommonMethods = { PiBy180 = Math.PI / 180, PiBy2 = Math.PI / 2; + /** + * @typedef {[number,number,number,number,number,number]} Matrix + */ + /** * @namespace fabric.util */ @@ -740,7 +708,7 @@ fabric.CommonMethods = { rotatePoint: function(point, origin, radians) { var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), v = fabric.util.rotateVector(newPoint, radians); - return new fabric.Point(v.x, v.y).addEquals(origin); + return v.addEquals(origin); }, /** @@ -749,17 +717,14 @@ fabric.CommonMethods = { * @memberOf fabric.util * @param {Object} vector The vector to rotate (x and y) * @param {Number} radians The radians of the angle for the rotation - * @return {Object} The new rotated point + * @return {fabric.Point} The new rotated point */ rotateVector: function(vector, radians) { var sin = fabric.util.sin(radians), cos = fabric.util.cos(radians), rx = vector.x * cos - vector.y * sin, ry = vector.x * sin + vector.y * cos; - return { - x: rx, - y: ry - }; + return new fabric.Point(rx, ry); }, /** @@ -798,7 +763,7 @@ fabric.CommonMethods = { * @returns {Point} vector representing the unit vector of pointing to the direction of `v` */ getHatVector: function (v) { - return new fabric.Point(v.x, v.y).multiply(1 / Math.hypot(v.x, v.y)); + return new fabric.Point(v.x, v.y).scalarMultiply(1 / Math.hypot(v.x, v.y)); }, /** @@ -913,8 +878,70 @@ fabric.CommonMethods = { ); }, + /** + * Sends a point from the source coordinate plane to the destination coordinate plane.\ + * From the canvas/viewer's perspective the point remains unchanged. + * + * @example Send point from canvas plane to group plane + * var obj = new fabric.Rect({ left: 20, top: 20, width: 60, height: 60, strokeWidth: 0 }); + * var group = new fabric.Group([obj], { strokeWidth: 0 }); + * var sentPoint1 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), null, group.calcTransformMatrix()); + * var sentPoint2 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), fabric.iMatrix, group.calcTransformMatrix()); + * console.log(sentPoint1, sentPoint2) // both points print (0,0) which is the center of group + * + * @static + * @memberOf fabric.util + * @see {fabric.util.transformPointRelativeToCanvas} for transforming relative to canvas + * @param {fabric.Point} point + * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `point` exists in the canvas coordinate plane. + * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `point` should be sent to the canvas coordinate plane. + * @returns {fabric.Point} transformed point + */ + sendPointToPlane: function (point, from, to) { + // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) + // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) + var inv = fabric.util.invertTransform(to || fabric.iMatrix); + var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); + return fabric.util.transformPoint(point, t); + }, + + /** + * Transform point relative to canvas. + * From the viewport/viewer's perspective the point remains unchanged. + * + * `child` relation means `point` exists in the coordinate plane created by `canvas`. + * In other words point is measured acoording to canvas' top left corner + * meaning that if `point` is equal to (0,0) it is positioned at canvas' top left corner. + * + * `sibling` relation means `point` exists in the same coordinate plane as canvas. + * In other words they both relate to the same (0,0) and agree on every point, which is how an event relates to canvas. + * + * @static + * @memberOf fabric.util + * @param {fabric.Point} point + * @param {fabric.StaticCanvas} canvas + * @param {'sibling'|'child'} relationBefore current relation of point to canvas + * @param {'sibling'|'child'} relationAfter desired relation of point to canvas + * @returns {fabric.Point} transformed point + */ + transformPointRelativeToCanvas: function (point, canvas, relationBefore, relationAfter) { + if (relationBefore !== 'child' && relationBefore !== 'sibling') { + throw new Error('fabric.js: received bad argument ' + relationBefore); + } + if (relationAfter !== 'child' && relationAfter !== 'sibling') { + throw new Error('fabric.js: received bad argument ' + relationAfter); + } + if (relationBefore === relationAfter) { + return point; + } + var t = canvas.viewportTransform; + return fabric.util.transformPoint(point, relationAfter === 'child' ? fabric.util.invertTransform(t) : t); + }, + /** * Returns coordinates of points's bounding rectangle (left, top, width, height) + * @static + * @memberOf fabric.util * @param {Array} points 4 points array * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix * @return {Object} Object with left, top, width, height properties @@ -1080,185 +1107,84 @@ fabric.CommonMethods = { }, /** - * Loads image element from given url and passes it to a callback + * Loads image element from given url and resolve it, or catch. * @memberOf fabric.util * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {*} [context] Context to invoke callback in - * @param {Object} [crossOrigin] crossOrigin value to set image element to - */ - loadImage: function(url, callback, context, crossOrigin) { - if (!url) { - callback && callback.call(context, url); - return; - } - - var img = fabric.util.createImage(); - - /** @ignore */ - var onLoadCallback = function () { - callback && callback.call(context, img, false); - img = img.onload = img.onerror = null; - }; - - img.onload = onLoadCallback; - /** @ignore */ - img.onerror = function() { - fabric.log('Error loading ' + img.src); - callback && callback.call(context, null, true); - img = img.onload = img.onerror = null; - }; - - // data-urls appear to be buggy with crossOrigin - // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 - // see https://code.google.com/p/chromium/issues/detail?id=315152 - // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 - // crossOrigin null is the same as not set. - if (url.indexOf('data') !== 0 && - crossOrigin !== undefined && - crossOrigin !== null) { - img.crossOrigin = crossOrigin; - } - - // IE10 / IE11-Fix: SVG contents from data: URI - // will only be available if the IMG is present - // in the DOM (and visible) - if (url.substring(0,14) === 'data:image/svg') { - img.onload = null; - fabric.util.loadImageInDom(img, onLoadCallback); - } - - img.src = url; - }, - - /** - * Attaches SVG image with data: URL to the dom - * @memberOf fabric.util - * @param {Object} img Image object with data:image/svg src - * @param {Function} callback Callback; invoked with loaded image - * @return {Object} DOM element (div containing the SVG image) - */ - loadImageInDom: function(img, onLoadCallback) { - var div = fabric.document.createElement('div'); - div.style.width = div.style.height = '1px'; - div.style.left = div.style.top = '-100%'; - div.style.position = 'absolute'; - div.appendChild(img); - fabric.document.querySelector('body').appendChild(div); - /** - * Wrap in function to: - * 1. Call existing callback - * 2. Cleanup DOM - */ - img.onload = function () { - onLoadCallback(); - div.parentNode.removeChild(div); - div = null; - }; + * @param {Object} [options] image loading options + * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous + * @param {Promise} img the loaded image. + */ + loadImage: function(url, options) { + return new Promise(function(resolve, reject) { + var img = fabric.util.createImage(); + var done = function() { + img.onload = img.onerror = null; + resolve(img); + }; + if (!url) { + done(); + } + else { + img.onload = done; + img.onerror = function () { + reject(new Error('Error loading ' + img.src)); + }; + options && options.crossOrigin && (img.crossOrigin = options.crossOrigin); + img.src = url; + } + }); }, /** * Creates corresponding fabric instances from their object representations * @static * @memberOf fabric.util - * @param {Array} objects Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created + * @param {Object[]} objects Objects to enliven * @param {String} namespace Namespace to get klass "Class" object from * @param {Function} reviver Method for further parsing of object elements, * called after each fabric object created. */ - enlivenObjects: function(objects, callback, namespace, reviver) { - objects = objects || []; - - var enlivenedObjects = [], - numLoadedObjects = 0, - numTotalObjects = objects.length; - - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - callback && callback(enlivenedObjects.filter(function(obj) { - // filter out undefined objects (objects that gave error) - return obj; - })); - } - } - - if (!numTotalObjects) { - callback && callback(enlivenedObjects); - return; - } - - objects.forEach(function (o, index) { - // if sparse array - if (!o || !o.type) { - onLoaded(); - return; - } - var klass = fabric.util.getKlass(o.type, namespace); - klass.fromObject(o, function (obj, error) { - error || (enlivenedObjects[index] = obj); - reviver && reviver(o, obj, error); - onLoaded(); + enlivenObjects: function(objects, namespace, reviver) { + return Promise.all(objects.map(function(obj) { + var klass = fabric.util.getKlass(obj.type, namespace); + return klass.fromObject(obj).then(function(fabricInstance) { + reviver && reviver(obj, fabricInstance); + return fabricInstance; }); - }); + })); }, /** * Creates corresponding fabric instances residing in an object, e.g. `clipPath` - * @see {@link fabric.Object.ENLIVEN_PROPS} - * @param {Object} object - * @param {Object} [context] assign enlived props to this object (pass null to skip this) - * @param {(objects:fabric.Object[]) => void} callback - */ - enlivenObjectEnlivables: function (object, context, callback) { - var enlivenProps = fabric.Object.ENLIVEN_PROPS.filter(function (key) { return !!object[key]; }); - fabric.util.enlivenObjects(enlivenProps.map(function (key) { return object[key]; }), function (enlivedProps) { - var objects = {}; - enlivenProps.forEach(function (key, index) { - objects[key] = enlivedProps[index]; - context && (context[key] = enlivedProps[index]); - }); - callback && callback(objects); - }); - }, - - /** - * Create and wait for loading of patterns - * @static - * @memberOf fabric.util - * @param {Array} patterns Objects to enliven - * @param {Function} callback Callback to invoke when all objects are created - * called after each fabric object created. + * @param {Object} object with properties to enlive ( fill, stroke, clipPath, path ) + * @returns {Promise} the input object with enlived values */ - enlivenPatterns: function(patterns, callback) { - patterns = patterns || []; - function onLoaded() { - if (++numLoadedPatterns === numPatterns) { - callback && callback(enlivenedPatterns); + enlivenObjectEnlivables: function (serializedObject) { + // enlive every possible property + var promises = Object.values(serializedObject).map(function(value) { + if (!value) { + return value; } - } - - var enlivenedPatterns = [], - numLoadedPatterns = 0, - numPatterns = patterns.length; - - if (!numPatterns) { - callback && callback(enlivenedPatterns); - return; - } - - patterns.forEach(function (p, index) { - if (p && p.source) { - new fabric.Pattern(p, function(pattern) { - enlivenedPatterns[index] = pattern; - onLoaded(); + if (value.colorStops) { + return new fabric.Gradient(value); + } + if (value.type) { + return fabric.util.enlivenObjects([value]).then(function (enlived) { + return enlived[0]; }); } - else { - enlivenedPatterns[index] = p; - onLoaded(); + if (value.source) { + return fabric.Pattern.fromObject(value); } + return value; + }); + var keys = Object.keys(serializedObject); + return Promise.all(promises).then(function(enlived) { + return enlived.reduce(function(acc, instance, index) { + acc[keys[index]] = instance; + return acc; + }, {}); }); }, @@ -1267,32 +1193,13 @@ fabric.CommonMethods = { * @static * @memberOf fabric.util * @param {Array} elements SVG elements to group - * @param {Object} [options] Options object - * @param {String} path Value to set sourcePath to * @return {fabric.Object|fabric.Group} */ - groupSVGElements: function(elements, options, path) { - var object; + groupSVGElements: function(elements) { if (elements && elements.length === 1) { return elements[0]; } - if (options) { - if (options.width && options.height) { - options.centerPoint = { - x: options.width / 2, - y: options.height / 2 - }; - } - else { - delete options.width; - delete options.height; - } - } - object = new fabric.Group(elements, options); - if (typeof path !== 'undefined') { - object.sourcePath = path; - } - return object; + return new fabric.Group(elements); }, /** @@ -1304,7 +1211,7 @@ fabric.CommonMethods = { * @return {Array} properties Properties names to include */ populateWithProperties: function(source, destination, properties) { - if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + if (properties && Array.isArray(properties)) { for (var i = 0, len = properties.length; i < len; i++) { if (properties[i] in source) { destination[properties[i]] = source[properties[i]]; @@ -1705,7 +1612,7 @@ fabric.CommonMethods = { * this is equivalent to remove from that object that transformation, so that * added in a space with the removed transform, the object will be the same as before. * Removing from an object a transform that scale by 2 is like scaling it by 1/2. - * Removing from an object a transfrom that rotate by 30deg is like rotating by 30deg + * Removing from an object a transform that rotate by 30deg is like rotating by 30deg * in the opposite direction. * This util is used to add objects inside transformed groups or nested groups. * @memberOf fabric.util @@ -1753,6 +1660,50 @@ fabric.CommonMethods = { object.setPositionByOrigin(center, 'center', 'center'); }, + /** + * + * A util that abstracts applying transform to objects.\ + * Sends `object` to the destination coordinate plane by applying the relevant transformations.\ + * Changes the space/plane where `object` is drawn.\ + * From the canvas/viewer's perspective `object` remains unchanged. + * + * @example Move clip path from one object to another while preserving it's appearance as viewed by canvas/viewer + * let obj, obj2; + * let clipPath = new fabric.Circle({ radius: 50 }); + * obj.clipPath = clipPath; + * // render + * fabric.util.sendObjectToPlane(clipPath, obj.calcTransformMatrix(), obj2.calcTransformMatrix()); + * obj.clipPath = undefined; + * obj2.clipPath = clipPath; + * // render, clipPath now clips obj2 but seems unchanged from the eyes of the viewer + * + * @example Clip an object's clip path with an existing object + * let obj, existingObj; + * let clipPath = new fabric.Circle({ radius: 50 }); + * obj.clipPath = clipPath; + * let transformTo = fabric.util.multiplyTransformMatrices(obj.calcTransformMatrix(), clipPath.calcTransformMatrix()); + * fabric.util.sendObjectToPlane(existingObj, existingObj.group?.calcTransformMatrix(), transformTo); + * clipPath.clipPath = existingObj; + * + * @static + * @memberof fabric.util + * @param {fabric.Object} object + * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `object` is a direct child of canvas. + * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `object` should be sent to the canvas coordinate plane. + * @returns {Matrix} the transform matrix that was applied to `object` + */ + sendObjectToPlane: function (object, from, to) { + // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) + // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) + var inv = fabric.util.invertTransform(to || fabric.iMatrix); + var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); + fabric.util.applyTransformToObject( + object, + fabric.util.multiplyTransformMatrices(t, object.calcOwnMatrix()) + ); + return t; + }, + /** * given a width and height, return the size of the bounding box * that can contains the box with width/height with applied transform @@ -2825,7 +2776,7 @@ fabric.CommonMethods = { /** * Creates an empty object and copies all enumerable properties of another object to it - * This method is mostly for internal use, and not intended for duplicating shapes in canvas. + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. * @memberOf fabric.util.object * @param {Object} object Object to clone * @param {Boolean} [deep] Whether to clone nested objects @@ -2834,7 +2785,7 @@ fabric.CommonMethods = { //TODO: this function return an empty object if you try to clone null function clone(object, deep) { - return extend({ }, object, deep); + return deep ? extend({ }, object, deep) : Object.assign({}, object); } /** @namespace fabric.util.object */ @@ -3512,6 +3463,7 @@ fabric.CommonMethods = { /** * Cross-browser abstraction for sending XMLHttpRequest * @memberOf fabric.util + * @deprecated this has to go away, we can use a modern browser method to do the same. * @param {String} url URL to send XMLHttpRequest to * @param {Object} [options] Options object * @param {String} [options.method="GET"] @@ -3576,30 +3528,18 @@ fabric.warn = console.warn; clone = fabric.util.object.clone; /** + * * @typedef {Object} AnimationOptions * Animation of a value or list of values. - * When using lists, think of something like this: - * fabric.util.animate({ - * startValue: [1, 2, 3], - * endValue: [2, 4, 6], - * onChange: function([a, b, c]) { - * canvas.zoomToPoint({x: b, y: c}, a) - * canvas.renderAll() - * } - * }); - * @example * @property {Function} [onChange] Callback; invoked on every value change * @property {Function} [onComplete] Callback; invoked when value change is completed - * @example - * // Note: startValue, endValue, and byValue must match the type - * var animationOptions = { startValue: 0, endValue: 1, byValue: 0.25 } - * var animationOptions = { startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] } * @property {number | number[]} [startValue=0] Starting value * @property {number | number[]} [endValue=100] Ending value * @property {number | number[]} [byValue=100] Value to modify the property by * @property {Function} [easing] Easing function - * @property {Number} [duration=500] Duration of change (in ms) + * @property {number} [duration=500] Duration of change (in ms) * @property {Function} [abort] Additional function with logic. If returns true, animation aborts. + * @property {number} [delay] Delay of animation start (in ms) * * @typedef {() => void} CancelFunction * @@ -3709,10 +3649,27 @@ fabric.warn = console.warn; * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. * @memberOf fabric.util * @param {AnimationOptions} [options] Animation options + * When using lists, think of something like this: + * @example + * fabric.util.animate({ + * startValue: [1, 2, 3], + * endValue: [2, 4, 6], + * onChange: function([x, y, zoom]) { + * canvas.zoomToPoint(new fabric.Point(x, y), zoom); + * canvas.requestRenderAll(); + * } + * }); + * * @example - * // Note: startValue, endValue, and byValue must match the type - * fabric.util.animate({ startValue: 0, endValue: 1, byValue: 0.25 }) - * fabric.util.animate({ startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] }) + * fabric.util.animate({ + * startValue: 1, + * endValue: 0, + * onChange: function(v) { + * obj.set('opacity', v); + * canvas.requestRenderAll(); + * } + * }); + * * @returns {CancelFunction} cancel function */ function animate(options) { @@ -3735,7 +3692,7 @@ fabric.warn = console.warn; }); fabric.runningAnimations.push(context); - requestAnimFrame(function(timestamp) { + var runner = function (timestamp) { var start = timestamp || +new Date(), duration = options.duration || 500, finish = start + duration, time, @@ -3788,7 +3745,16 @@ fabric.warn = console.warn; requestAnimFrame(tick); } })(start); - }); + }; + + if (options.delay) { + setTimeout(function () { + requestAnimFrame(runner); + }, options.delay); + } + else { + requestAnimFrame(runner); + } return context.cancel; } @@ -4382,8 +4348,7 @@ fabric.warn = console.warn; } function normalizeValue(attr, value, parentAttributes, fontSize) { - var isArray = Object.prototype.toString.call(value) === '[object Array]', - parsed; + var isArray = Array.isArray(value), parsed; if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; @@ -4781,7 +4746,7 @@ fabric.warn = console.warn; return; } - var xlink = xlinkAttribute.substr(1), + var xlink = xlinkAttribute.slice(1), x = el.getAttribute('x') || 0, y = el.getAttribute('y') || 0, el2 = elementById(doc, xlink).cloneNode(true), @@ -5053,7 +5018,7 @@ fabric.warn = console.warn; function recursivelyParseGradientsXlink(doc, gradient) { var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], xlinkAttr = 'xlink:href', - xLink = gradient.getAttribute(xlinkAttr).substr(1), + xLink = gradient.getAttribute(xlinkAttr).slice(1), referencedGradient = elementById(doc, xLink); if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { recursivelyParseGradientsXlink(doc, referencedGradient); @@ -5668,47 +5633,61 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, + /** + * Multiplies this point by another value and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + multiply: function (that) { + return new Point(this.x * that.x, this.y * that.y); + }, + /** * Multiplies this point by a value and returns a new one - * TODO: rename in scalarMultiply in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - multiply: function (scalar) { + scalarMultiply: function (scalar) { return new Point(this.x * scalar, this.y * scalar); }, /** * Multiplies this point by a value - * TODO: rename in scalarMultiplyEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - multiplyEquals: function (scalar) { + scalarMultiplyEquals: function (scalar) { this.x *= scalar; this.y *= scalar; return this; }, + /** + * Divides this point by another and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + divide: function (that) { + return new Point(this.x / that.x, this.y / that.y); + }, + /** * Divides this point by a value and returns a new one - * TODO: rename in scalarDivide in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - divide: function (scalar) { + scalarDivide: function (scalar) { return new Point(this.x / scalar, this.y / scalar); }, /** * Divides this point by a value - * TODO: rename in scalarDivideEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - divideEquals: function (scalar) { + scalarDivideEquals: function (scalar) { this.x /= scalar; this.y /= scalar; return this; @@ -6726,7 +6705,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @return {Number} 0 - 7 a quadrant number */ function findCornerQuadrant(fabricObject, control) { - var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + // angle is relative to canvas plane + var angle = fabricObject.getTotalAngle(); + var cornerAngle = angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; return Math.round((cornerAngle % 360) / 45); } @@ -6895,7 +6876,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ function wrapWithFixedAnchor(actionHandler) { return function(eventData, transform, x, y) { - var target = transform.target, centerPoint = target.getCenterPoint(), + var target = transform.target, centerPoint = target.getRelativeCenterPoint(), constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), actionPerformed = actionHandler(eventData, transform, x, y); target.setPositionByOrigin(constraint, transform.originX, transform.originY); @@ -6933,7 +6914,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp control = target.controls[transform.corner], zoom = target.canvas.getZoom(), padding = target.padding / zoom, - localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); + localPoint = target.normalizePoint(new fabric.Point(x, y), originX, originY); if (localPoint.x >= padding) { localPoint.x -= padding; } @@ -6979,7 +6960,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectX(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions(0, target.skewY), + dimNoSkew = target._getTransformedDimensions({ skewX: 0, skewY: target.skewY }), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7022,7 +7003,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectY(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions(target.skewX, 0), + dimNoSkew = target._getTransformedDimensions({ skewX: target.skewX, skewY: 0 }), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7171,7 +7152,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function rotationWithSnapping(eventData, transform, x, y) { var t = transform, target = t.target, - pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + pivotPoint = target.translateToOriginPoint(target.getRelativeCenterPoint(), t.originX, t.originY); if (target.lockRotation) { return false; @@ -7390,9 +7371,10 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), multiplier = isTransformCentered(transform) ? 2 : 1, oldWidth = target.width, - newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; + newWidth = Math.ceil(Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding); target.set('width', Math.max(newWidth, 0)); - return oldWidth !== newWidth; + // check against actual target width in case `newWidth` was rejected + return oldWidth !== target.width; } /** @@ -7526,7 +7508,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp // this is still wrong ctx.lineWidth = 1; ctx.translate(left, top); - ctx.rotate(degreesToRadians(fabricObject.angle)); + // angle is relative to canvas plane + var angle = fabricObject.getTotalAngle(); + ctx.rotate(degreesToRadians(angle)); // this does not work, and fixed with ( && ) does not make sense. // to have real transparent corners we need the controls on upperCanvas // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); @@ -8429,30 +8413,18 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ patternTransform: null, + type: 'pattern', + /** * Constructor * @param {Object} [options] Options object - * @param {Function} [callback] function to invoke after callback init. + * @param {option.source} [source] the pattern source, eventually empty or a drawable * @return {fabric.Pattern} thisArg */ - initialize: function(options, callback) { + initialize: function(options) { options || (options = { }); - this.id = fabric.Object.__uid++; this.setOptions(options); - if (!options.source || (options.source && typeof options.source !== 'string')) { - callback && callback(this); - return; - } - else { - // img src string - var _this = this; - this.source = fabric.util.createImage(); - fabric.util.loadImage(options.source, function(img, isError) { - _this.source = img; - callback && callback(_this, isError); - }, null, this.crossOrigin); - } }, /** @@ -8564,6 +8536,15 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return ctx.createPattern(source, this.repeat); } }); + + fabric.Pattern.fromObject = function(object) { + var patternOptions = Object.assign({}, object); + return fabric.util.loadImage(object.source, { crossOrigin: object.crossOrigin }) + .then(function(img) { + patternOptions.source = img; + return new fabric.Pattern(patternOptions); + }); + }; })(); @@ -8798,7 +8779,8 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @fires object:added * @fires object:removed */ - fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { + // eslint-disable-next-line max-len + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, fabric.Collection, /** @lends fabric.StaticCanvas.prototype */ { /** * Constructor @@ -8815,7 +8797,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Background color of canvas instance. - * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. * @type {(String|fabric.Pattern)} * @default */ @@ -8833,7 +8814,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Overlay color of canvas instance. - * Should be set via {@link fabric.StaticCanvas#setOverlayColor} * @since 1.3.9 * @type {(String|fabric.Pattern)} * @default @@ -8970,7 +8950,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @param {Object} [options] Options object */ _initStatic: function(el, options) { - var cb = this.requestRenderAllBound; this._objects = []; this._createLowerCanvas(el); this._initOptions(options); @@ -8978,19 +8957,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp if (!this.interactive) { this._initRetinaScaling(); } - - if (options.overlayImage) { - this.setOverlayImage(options.overlayImage, cb); - } - if (options.backgroundImage) { - this.setBackgroundImage(options.backgroundImage, cb); - } - if (options.backgroundColor) { - this.setBackgroundColor(options.backgroundColor, cb); - } - if (options.overlayColor) { - this.setOverlayColor(options.overlayColor, cb); - } this.calcOffset(); }, @@ -9041,202 +9007,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, - /** - * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas - * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to - * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} - * @example Normal overlayImage with left/top = 0 - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * // Needed to position overlayImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example overlayImage with different properties - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top' - * }); - * @example Stretched overlayImage #1 - width/height correspond to canvas width/height - * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { - * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); - * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); - * }); - * @example Stretched overlayImage #2 - width/height correspond to canvas width/height - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * width: canvas.width, - * height: canvas.height, - * // Needed to position overlayImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example overlayImage loaded from cross-origin - * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top', - * crossOrigin: 'anonymous' - * }); - */ - setOverlayImage: function (image, callback, options) { - return this.__setBgOverlayImage('overlayImage', image, callback, options); - }, - - /** - * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas - * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to - * @param {Function} callback Callback to invoke when image is loaded and set as background - * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} - * @example Normal backgroundImage with left/top = 0 - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * // Needed to position backgroundImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example backgroundImage with different properties - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top' - * }); - * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height - * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { - * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); - * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); - * }); - * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * width: canvas.width, - * height: canvas.height, - * // Needed to position backgroundImage at 0/0 - * originX: 'left', - * originY: 'top' - * }); - * @example backgroundImage loaded from cross-origin - * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { - * opacity: 0.5, - * angle: 45, - * left: 400, - * top: 400, - * originX: 'left', - * originY: 'top', - * crossOrigin: 'anonymous' - * }); - */ - // TODO: fix stretched examples - setBackgroundImage: function (image, callback, options) { - return this.__setBgOverlayImage('backgroundImage', image, callback, options); - }, - - /** - * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas - * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to - * @param {Function} callback Callback to invoke when foreground color is set - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} - * @example Normal overlayColor - color value - * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as overlayColor - * canvas.setOverlayColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png' - * }, canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as overlayColor with repeat and offset - * canvas.setOverlayColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png', - * repeat: 'repeat', - * offsetX: 200, - * offsetY: 100 - * }, canvas.renderAll.bind(canvas)); - */ - setOverlayColor: function(overlayColor, callback) { - return this.__setBgOverlayColor('overlayColor', overlayColor, callback); - }, - - /** - * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas - * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to - * @param {Function} callback Callback to invoke when background color is set - * @return {fabric.Canvas} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} - * @example Normal backgroundColor - color value - * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as backgroundColor - * canvas.setBackgroundColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png' - * }, canvas.renderAll.bind(canvas)); - * @example fabric.Pattern used as backgroundColor with repeat and offset - * canvas.setBackgroundColor({ - * source: 'http://fabricjs.com/assets/escheresque_ste.png', - * repeat: 'repeat', - * offsetX: 200, - * offsetY: 100 - * }, canvas.renderAll.bind(canvas)); - */ - setBackgroundColor: function(backgroundColor, callback) { - return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); - }, - - /** - * @private - * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} - * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) - * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to - * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. - * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. - */ - __setBgOverlayImage: function(property, image, callback, options) { - if (typeof image === 'string') { - fabric.util.loadImage(image, function(img, isError) { - if (img) { - var instance = new fabric.Image(img, options); - this[property] = instance; - instance.canvas = this; - } - callback && callback(img, isError); - }, this, options && options.crossOrigin); - } - else { - options && image.setOptions(options); - this[property] = image; - image && (image.canvas = this); - callback && callback(image, false); - } - - return this; - }, - - /** - * @private - * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} - * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) - * @param {(Object|String|null)} color Object with pattern information, color value or null - * @param {Function} [callback] Callback is invoked when color is set - */ - __setBgOverlayColor: function(property, color, callback) { - this[property] = color; - this._initGradient(color, property); - this._initPattern(color, property, callback); - return this; - }, - /** * @private */ @@ -9291,10 +9061,15 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp else { this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); } - + if (this.lowerCanvasEl.hasAttribute('data-fabric')) { + /* _DEV_MODE_START_ */ + throw new Error('fabric.js: trying to initialize a canvas that has already been initialized'); + /* _DEV_MODE_END_ */ + } fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); - this._originalCanvasStyle = this.lowerCanvasEl.style; + this.lowerCanvasEl.setAttribute('data-fabric', 'main'); if (this.interactive) { + this._originalCanvasStyle = this.lowerCanvasEl.style.cssText; this._applyCanvasStyle(this.lowerCanvasEl); } @@ -9377,6 +9152,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp } this._initRetinaScaling(); this.calcOffset(); + this.fire('resize', dimensions); if (!options.cssOnly) { this.requestRenderAll(); @@ -9537,31 +9313,75 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }, /** - * @private - * @param {fabric.Object} obj Object that was added + * @param {...fabric.Object} objects to add + * @return {Self} thisArg + * @chainable */ - _onObjectAdded: function(obj) { - this.stateful && obj.setupState(); - obj._set('canvas', this); - obj.setCoords(); - this.fire('object:added', { target: obj }); - obj.fire('added'); + add: function () { + fabric.Collection.add.call(this, arguments, this._onObjectAdded); + arguments.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); + return this; }, /** - * @private - * @param {fabric.Object} obj Object that was removed + * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) + * An object should be an instance of (or inherit from) fabric.Object + * @param {fabric.Object|fabric.Object[]} objects Object(s) to insert + * @param {Number} index Index to insert object at + * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs + * @return {Self} thisArg + * @chainable */ - _onObjectRemoved: function(obj) { - this.fire('object:removed', { target: obj }); - obj.fire('removed'); - delete obj.canvas; + insertAt: function (objects, index) { + fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); + this.renderOnAddRemove && this.requestRenderAll(); + return this; }, /** - * Clears specified context of canvas element - * @param {CanvasRenderingContext2D} ctx Context to clear - * @return {fabric.Canvas} thisArg + * @param {...fabric.Object} objects to remove + * @return {Self} thisArg + * @chainable + */ + remove: function () { + var removed = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); + removed.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + if (obj.canvas && obj.canvas !== this) { + /* _DEV_MODE_START_ */ + console.warn('fabric.Canvas: trying to add an object that belongs to a different canvas.\n' + + 'Resulting to default behavior: removing object from previous canvas and adding to new canvas'); + /* _DEV_MODE_END_ */ + obj.canvas.remove(obj); + } + obj._set('canvas', this); + obj.setCoords(); + this.fire('object:added', { target: obj }); + obj.fire('added', { target: this }); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + this.fire('object:removed', { target: obj }); + obj.fire('removed', { target: this }); + obj._set('canvas', undefined); + }, + + /** + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg * @chainable */ clearContext: function(ctx) { @@ -9793,6 +9613,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * Returns coordinates of a center of canvas. * Returned value is an object with top and left properties * @return {Object} object with "top" and "left" number values + * @deprecated migrate to `getCenterPoint` */ getCenter: function () { return { @@ -9801,13 +9622,21 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }; }, + /** + * Returns coordinates of a center of canvas. + * @return {fabric.Point} + */ + getCenterPoint: function () { + return new fabric.Point(this.width / 2, this.height / 2); + }, + /** * Centers object horizontally in the canvas * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { - return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + return this._centerObject(object, new fabric.Point(this.getCenterPoint().x, object.getCenterPoint().y)); }, /** @@ -9817,7 +9646,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObjectV: function (object) { - return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenterPoint().y)); }, /** @@ -9827,9 +9656,8 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObject: function(object) { - var center = this.getCenter(); - - return this._centerObject(object, new fabric.Point(center.left, center.top)); + var center = this.getCenterPoint(); + return this._centerObject(object, center); }, /** @@ -9840,7 +9668,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ viewportCenterObject: function(object) { var vpCenter = this.getVpCenter(); - return this._centerObject(object, vpCenter); }, @@ -9874,9 +9701,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ getVpCenter: function() { - var center = this.getCenter(), + var center = this.getCenterPoint(), iVpt = invertTransform(this.viewportTransform); - return transformPoint({ x: center.left, y: center.top }, iVpt); + return transformPoint(center, iVpt); }, /** @@ -9887,7 +9714,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ _centerObject: function(object, center) { - object.setPositionByOrigin(center, 'center', 'center'); + object.setXY(center, 'center', 'center'); object.setCoords(); this.renderOnAddRemove && this.requestRenderAll(); return this; @@ -10540,10 +10367,13 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp this.overlayImage = null; this._iTextInstances = null; this.contextContainer = null; - // restore canvas style + // restore canvas style and attributes this.lowerCanvasEl.classList.remove('lower-canvas'); - fabric.util.setStyle(this.lowerCanvasEl, this._originalCanvasStyle); - delete this._originalCanvasStyle; + this.lowerCanvasEl.removeAttribute('data-fabric'); + if (this.interactive) { + this.lowerCanvasEl.style.cssText = this._originalCanvasStyle; + delete this._originalCanvasStyle; + } // restore canvas size to original size in case retina scaling was applied this.lowerCanvasEl.setAttribute('width', this.width); this.lowerCanvasEl.setAttribute('height', this.height); @@ -10563,7 +10393,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }); extend(fabric.StaticCanvas.prototype, fabric.Observable); - extend(fabric.StaticCanvas.prototype, fabric.Collection); extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { @@ -11354,7 +11183,12 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric rects = this._getOptimizedRects(rects); } - var group = new fabric.Group(rects); + var group = new fabric.Group(rects, { + objectCaching: true, + layout: 'fixed', + subTargetCheck: false, + interactive: false + }); this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); this.canvas.fire('before:path:created', { path: group }); this.canvas.add(group); @@ -11568,6 +11402,28 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @fires after:render at the end of the render process, receives the context in the callback * @fires before:render at start the render process, receives the context in the callback * + * @fires contextmenu:before + * @fires contextmenu + * @example + * let handler; + * targets.forEach(target => { + * target.on('contextmenu:before', opt => { + * // decide which target should handle the event before canvas hijacks it + * if (someCaseHappens && opt.targets.includes(target)) { + * handler = target; + * } + * }); + * target.on('contextmenu', opt => { + * // do something fantastic + * }); + * }); + * canvas.on('contextmenu', opt => { + * if (!handler) { + * // no one takes responsibility, it's always left to me + * // let's show them how it's done! + * } + * }); + * */ fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { @@ -11878,6 +11734,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _hoveredTargets: [], + /** + * hold the list of objects to render + * @type fabric.Object[] + * @private + */ + _objectsToRender: undefined, + /** * @private */ @@ -11895,6 +11758,23 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); }, + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function (obj) { + this._objectsToRender = undefined; + this.callSuper('_onObjectAdded', obj); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function (obj) { + this._objectsToRender = undefined; + this.callSuper('_onObjectRemoved', obj); + }, /** * Divides objects in two groups, one to render immediately * and one to render as activeGroup. @@ -11904,7 +11784,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var activeObjects = this.getActiveObjects(), object, objsToRender, activeGroupObjects; - if (activeObjects.length > 0 && !this.preserveObjectStacking) { + if (!this.preserveObjectStacking && activeObjects.length > 1) { objsToRender = []; activeGroupObjects = []; for (var i = 0, length = this._objects.length; i < length; i++) { @@ -11921,6 +11801,15 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } objsToRender.push.apply(objsToRender, activeGroupObjects); } + // in case a single object is selected render it's entire parent above the other objects + else if (!this.preserveObjectStacking && activeObjects.length === 1) { + var target = activeObjects[0], ancestors = target.getAncestors(true); + var topAncestor = ancestors.length === 0 ? target : ancestors.pop(); + objsToRender = this._objects.slice(); + var index = objsToRender.indexOf(topAncestor); + index > -1 && objsToRender.splice(objsToRender.indexOf(topAncestor), 1); + objsToRender.push(topAncestor); + } else { objsToRender = this._objects; } @@ -11942,7 +11831,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.hasLostContext = false; } var canvasToDrawOn = this.contextContainer; - this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + !this._objectsToRender && (this._objectsToRender = this._chooseObjectsToRender()); + this.renderCanvas(canvasToDrawOn, this._objectsToRender); return this; }, @@ -12033,7 +11923,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _isSelectionKeyPressed: function(e) { var selectionKeyPressed = false; - if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { + if (Array.isArray(this.selectionKey)) { selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); } else { @@ -12148,14 +12038,22 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (!target) { return; } - - var pointer = this.getPointer(e), corner = target.__corner, + var pointer = this.getPointer(e); + if (target.group) { + // transform pointer to target's containing coordinate plane + pointer = fabric.util.transformPoint(pointer, fabric.util.invertTransform(target.group.calcTransformMatrix())); + } + var corner = target.__corner, control = target.controls[corner], actionHandler = (alreadySelected && corner) ? control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, action = this._getActionFromCorner(alreadySelected, corner, e, target), origin = this._getOriginFromCorner(target, corner), altKey = e[this.centeredKey], + /** + * relative to target's containing coordinate plane + * both agree on every point + **/ transform = { target: target, action: action, @@ -12165,7 +12063,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, - // used by transation offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, @@ -12174,11 +12071,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ey: pointer.y, lastX: pointer.x, lastY: pointer.y, - // unsure they are useful anymore. - // left: target.left, - // top: target.top, theta: degreesToRadians(target.angle), - // end of unsure width: target.width * target.scaleX, shiftKey: e.shiftKey, altKey: altKey, @@ -12271,11 +12164,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { return activeObject; } - if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (aObjects.length > 1 && activeObject.type === 'activeSelection' + && !skipGroup && this.searchPossibleTargets([activeObject], pointer)) { return activeObject; } if (aObjects.length === 1 && - activeObject === this._searchPossibleTargets([activeObject], pointer)) { + activeObject === this.searchPossibleTargets([activeObject], pointer)) { if (!this.preserveObjectStacking) { return activeObject; } @@ -12285,7 +12179,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.targets = []; } } - var target = this._searchPossibleTargets(this._objects, pointer); + var target = this.searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; this.targets = activeTargetSubs; @@ -12322,10 +12216,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * Internal Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted * @param {Array} [objects] objects array to look into * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {fabric.Object} object that contains pointer + * @return {fabric.Object} **top most object from given `objects`** that contains pointer * @private */ _searchPossibleTargets: function(objects, pointer) { @@ -12339,7 +12233,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._normalizePointer(objToCheck.group, pointer) : pointer; if (this._checkTarget(pointerToUse, objToCheck, pointer)) { target = objects[i]; - if (target.subTargetCheck && target instanceof fabric.Group) { + if (target.subTargetCheck && Array.isArray(target._objects)) { subTarget = this._searchPossibleTargets(target._objects, pointer); subTarget && this.targets.push(subTarget); } @@ -12349,6 +12243,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return target; }, + /** + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * @see {@link fabric.Canvas#_searchPossibleTargets} + * @param {Array} [objects] objects array to look into + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @return {fabric.Object} **top most object on screen** that contains pointer + */ + searchPossibleTargets: function (objects, pointer) { + var target = this._searchPossibleTargets(objects, pointer); + return target && target.interactive && this.targets[0] ? this.targets[0] : target; + }, + /** * Returns pointer coordinates without the effect of the viewport * @param {Object} pointer with "x" and "y" number values @@ -12364,27 +12270,27 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * Returns pointer coordinates relative to canvas. * Can return coordinates with or without viewportTransform. - * ignoreZoom false gives back coordinates that represent + * ignoreVpt false gives back coordinates that represent * the point clicked on canvas element. - * ignoreZoom true gives back coordinates after being processed + * ignoreVpt true gives back coordinates after being processed * by the viewportTransform ( sort of coordinates of what is displayed * on the canvas where you are clicking. - * ignoreZoom true = HTMLElement coordinates relative to top,left - * ignoreZoom false, default = fabric space coordinates, the same used for shape position - * To interact with your shapes top and left you want to use ignoreZoom true - * most of the time, while ignoreZoom false will give you coordinates + * ignoreVpt true = HTMLElement coordinates relative to top,left + * ignoreVpt false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreVpt true + * most of the time, while ignoreVpt false will give you coordinates * compatible with the object.oCoords system. * of the time. * @param {Event} e - * @param {Boolean} ignoreZoom + * @param {Boolean} ignoreVpt * @return {Object} object with "x" and "y" number values */ - getPointer: function (e, ignoreZoom) { + getPointer: function (e, ignoreVpt) { // return cached values if we are in the event processing chain - if (this._absolutePointer && !ignoreZoom) { + if (this._absolutePointer && !ignoreVpt) { return this._absolutePointer; } - if (this._pointer && ignoreZoom) { + if (this._pointer && ignoreVpt) { return this._pointer; } @@ -12407,7 +12313,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; - if (!ignoreZoom) { + if (!ignoreVpt) { pointer = this.restorePointerVpt(pointer); } @@ -12451,7 +12357,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.upperCanvasEl = upperCanvasEl; } fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); - + this.upperCanvasEl.setAttribute('data-fabric', 'top'); this.wrapperEl.appendChild(upperCanvasEl); this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); @@ -12473,9 +12379,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @private */ _initWrapperElement: function () { + if (this.wrapperEl) { + return; + } this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { 'class': this.containerClass }); + this.wrapperEl.setAttribute('data-fabric', 'wrapper'); fabric.util.setStyle(this.wrapperEl, { width: this.width + 'px', height: this.height + 'px', @@ -12516,8 +12426,17 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab toEl.style.cssText = fromEl.style.cssText; }, + /** + * Returns context of top canvas where interactions are drawn + * @returns {CanvasRenderingContext2D} + */ + getTopContext: function () { + return this.contextTop; + }, + /** * Returns context of canvas where object selection is drawn + * @alias * @return {CanvasRenderingContext2D} */ getSelectionContext: function() { @@ -12583,7 +12502,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _fireSelectionEvents: function(oldObjects, e) { var somethingChanged = false, objects = this.getActiveObjects(), - added = [], removed = []; + added = [], removed = [], invalidate = false; oldObjects.forEach(function(oldObject) { if (objects.indexOf(oldObject) === -1) { somethingChanged = true; @@ -12605,6 +12524,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } }); if (oldObjects.length > 0 && objects.length > 0) { + invalidate = true; somethingChanged && this.fire('selection:updated', { e: e, selected: added, @@ -12612,17 +12532,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }); } else if (objects.length > 0) { + invalidate = true; this.fire('selection:created', { e: e, selected: added, }); } else if (oldObjects.length > 0) { + invalidate = true; this.fire('selection:cleared', { e: e, deselected: removed, }); } + invalidate && (this._objectsToRender = undefined); }, /** @@ -12710,21 +12633,24 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ dispose: function () { - var wrapper = this.wrapperEl; + var wrapperEl = this.wrapperEl, + lowerCanvasEl = this.lowerCanvasEl, + upperCanvasEl = this.upperCanvasEl, + cacheCanvasEl = this.cacheCanvasEl; this.removeListeners(); - wrapper.removeChild(this.upperCanvasEl); - wrapper.removeChild(this.lowerCanvasEl); + this.callSuper('dispose'); + wrapperEl.removeChild(upperCanvasEl); + wrapperEl.removeChild(lowerCanvasEl); this.contextCache = null; this.contextTop = null; - ['upperCanvasEl', 'cacheCanvasEl'].forEach((function(element) { - fabric.util.cleanUpJsdomNode(this[element]); - this[element] = undefined; - }).bind(this)); - if (wrapper.parentNode) { - wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); + fabric.util.cleanUpJsdomNode(upperCanvasEl); + this.upperCanvasEl = undefined; + fabric.util.cleanUpJsdomNode(cacheCanvasEl); + this.cacheCanvasEl = undefined; + if (wrapperEl.parentNode) { + wrapperEl.parentNode.replaceChild(lowerCanvasEl, wrapperEl); } delete this.wrapperEl; - fabric.StaticCanvas.prototype.dispose.call(this); return this; }, @@ -12763,7 +12689,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var originalProperties = this._realizeGroupTransformOnObject(instance), object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); //Undo the damage we did by changing all of its properties - this._unwindGroupTransformOnObject(instance, originalProperties); + originalProperties && instance.set(originalProperties); return object; }, @@ -12789,18 +12715,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } }, - /** - * Restores the changed properties of instance - * @private - * @param {fabric.Object} [instance] the object to un-transform (gets mutated) - * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject - */ - _unwindGroupTransformOnObject: function(instance, originalValues) { - if (originalValues) { - instance.set(originalValues); - } - }, - /** * @private */ @@ -12809,7 +12723,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab //object when the group is deselected var originalProperties = this._realizeGroupTransformOnObject(instance); this.callSuper('_setSVGObject', markup, instance, reviver); - this._unwindGroupTransformOnObject(instance, originalProperties); + originalProperties && instance.set(originalProperties); }, setViewportTransform: function (vpt) { @@ -13067,10 +12981,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mousedown */ _onContextMenu: function (e) { + this._simpleEventHandler('contextmenu:before', e); if (this.stopContextMenu) { e.stopPropagation(); e.preventDefault(); } + this._simpleEventHandler('contextmenu', e); return false; }, @@ -13213,8 +13129,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * @private */ - _onResize: function () { + _onResize: function (e) { this.calcOffset(); + this.fire('window:resize', { e: e }); }, /** @@ -13542,9 +13459,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } } + var invalidate = shouldRender || shouldGroup; + // we clear `_objectsToRender` in case of a change in order to repopulate it at rendering + // run before firing the `down` event to give the dev a chance to populate it themselves + invalidate && (this._objectsToRender = undefined); this._handleEvent(e, 'down'); // we must renderAll so that we update the visuals - (shouldRender || shouldGroup) && this.requestRenderAll(); + invalidate && this.requestRenderAll(); }, /** @@ -13730,13 +13651,19 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _transformObject: function(e) { var pointer = this.getPointer(e), - transform = this._currentTransform; + transform = this._currentTransform, + target = transform.target, + // transform pointer to target's containing coordinate plane + // both pointer and object should agree on every point + localPointer = target.group ? + fabric.util.sendPointToPlane(pointer, null, target.group.calcTransformMatrix()) : + pointer; transform.reset = false; transform.shiftKey = e.shiftKey; transform.altKey = e[this.centeredKey]; - this._performTransformAction(e, transform, pointer); + this._performTransformAction(e, transform, localPointer); transform.actionPerformed && this.requestRenderAll(); }, @@ -13829,8 +13756,19 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _shouldGroup: function(e, target) { var activeObject = this._activeObject; - return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && - (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); + // check if an active object exists on canvas and if the user is pressing the `selectionKey` while canvas supports multi selection. + return !!activeObject && this._isSelectionKeyPressed(e) && this.selection + // on top of that the user also has to hit a target that is selectable. + && !!target && target.selectable + // if all pre-requisite pass, the target is either something different from the current + // activeObject or if an activeSelection already exists + // TODO at time of writing why `activeObject.type === 'activeSelection'` matter is unclear. + // is a very old condition uncertain if still valid. + && (activeObject !== target || activeObject.type === 'activeSelection') + // make sure `activeObject` and `target` aren't ancestors of each other + && !target.isDescendantOf(activeObject) && !activeObject.isDescendantOf(target) + // target accepts selection + && !target.onSelect({ e: e }); }, /** @@ -13866,8 +13804,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _updateActiveSelection: function(target, e) { var activeSelection = this._activeObject, currentActiveObjects = activeSelection._objects.slice(0); - if (activeSelection.contains(target)) { - activeSelection.removeWithUpdate(target); + if (target.group === activeSelection) { + activeSelection.remove(target); this._hoveredTarget = target; this._hoveredTargets = this.targets.concat(); if (activeSelection.size() === 1) { @@ -13876,7 +13814,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } else { - activeSelection.addWithUpdate(target); + activeSelection.add(target); this._hoveredTarget = activeSelection; this._hoveredTargets = this.targets.concat(); } @@ -13896,17 +13834,19 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._fireSelectionEvents(currentActives, e); }, + /** * @private * @param {Object} target + * @returns {fabric.ActiveSelection} */ _createGroup: function(target) { - var objects = this._objects, - isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), - groupObjects = isActiveLower - ? [this._activeObject, target] - : [target, this._activeObject]; - this._activeObject.isEditing && this._activeObject.exitEditing(); + var activeObject = this._activeObject; + var groupObjects = target.isInFrontOf(activeObject) ? + [activeObject, target] : + [target, activeObject]; + activeObject.isEditing && activeObject.exitEditing(); + // handle case: target is nested return new fabric.ActiveSelection(groupObjects, { canvas: this }); @@ -14007,8 +13947,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 + * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format - * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} + * @see {@link https://jsfiddle.net/xsjua1rd/ demo} * @example Generate jpeg dataURL with lower quality * var dataURL = canvas.toDataURL({ * format: 'jpeg', @@ -14027,6 +13968,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * format: 'png', * multiplier: 2 * }); + * @example Generate dataURL with objects that overlap a specified object + * var myObject; + * var dataURL = canvas.toDataURL({ + * filter: (object) => object.isContainedWithinObject(myObject) || object.intersectsWithObject(myObject) + * }); */ toDataURL: function (options) { options || (options = { }); @@ -14045,29 +13991,31 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * This is an intermediary step used to get to a dataUrl but also it is useful to * create quick image copies of a canvas without passing for the dataUrl string * @param {Number} [multiplier] a zoom factor. - * @param {Object} [cropping] Cropping informations - * @param {Number} [cropping.left] Cropping left offset. - * @param {Number} [cropping.top] Cropping top offset. - * @param {Number} [cropping.width] Cropping width. - * @param {Number} [cropping.height] Cropping height. - */ - toCanvasElement: function(multiplier, cropping) { + * @param {Object} [options] Cropping informations + * @param {Number} [options.left] Cropping left offset. + * @param {Number} [options.top] Cropping top offset. + * @param {Number} [options.width] Cropping width. + * @param {Number} [options.height] Cropping height. + * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. + */ + toCanvasElement: function (multiplier, options) { multiplier = multiplier || 1; - cropping = cropping || { }; - var scaledWidth = (cropping.width || this.width) * multiplier, - scaledHeight = (cropping.height || this.height) * multiplier, + options = options || { }; + var scaledWidth = (options.width || this.width) * multiplier, + scaledHeight = (options.height || this.height) * multiplier, zoom = this.getZoom(), originalWidth = this.width, originalHeight = this.height, newZoom = zoom * multiplier, vp = this.viewportTransform, - translateX = (vp[4] - (cropping.left || 0)) * multiplier, - translateY = (vp[5] - (cropping.top || 0)) * multiplier, + translateX = (vp[4] - (options.left || 0)) * multiplier, + translateY = (vp[5] - (options.top || 0)) * multiplier, originalInteractive = this.interactive, newVp = [newZoom, 0, 0, newZoom, translateX, translateY], originalRetina = this.enableRetinaScaling, canvasEl = fabric.util.createCanvasElement(), - originalContextTop = this.contextTop; + originalContextTop = this.contextTop, + objectsToRender = options.filter ? this._objects.filter(options.filter) : this._objects; canvasEl.width = scaledWidth; canvasEl.height = scaledHeight; this.contextTop = null; @@ -14077,7 +14025,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.width = scaledWidth; this.height = scaledHeight; this.calcViewportBoundaries(); - this.renderCanvas(canvasEl.getContext('2d'), this._objects); + this.renderCanvas(canvasEl.getContext('2d'), objectsToRender); this.viewportTransform = vp; this.width = originalWidth; this.height = originalHeight; @@ -14097,24 +14045,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} * @param {String|Object} json JSON string or object - * @param {Function} callback Callback, invoked when json is parsed - * and corresponding objects (e.g: {@link fabric.Image}) - * are initialized * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. - * @return {fabric.Canvas} instance + * @return {Promise} instance * @chainable * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} * @example loadFromJSON - * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); + * canvas.loadFromJSON(json).then((canvas) => canvas.requestRenderAll()); * @example loadFromJSON with reviver - * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { + * canvas.loadFromJSON(json, function(o, object) { * // `o` = json object * // `object` = fabric.Object instance * // ... do some stuff ... + * }).then((canvas) => { + * ... canvas is restored, add your code. * }); */ - loadFromJSON: function (json, callback, reviver) { + loadFromJSON: function (json, reviver) { if (!json) { return; } @@ -14125,38 +14072,35 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati : fabric.util.object.clone(json); var _this = this, - clipPath = serialized.clipPath, renderOnAddRemove = this.renderOnAddRemove; this.renderOnAddRemove = false; - delete serialized.clipPath; - - this._enlivenObjects(serialized.objects, function (enlivenedObjects) { - _this.clear(); - _this._setBgOverlay(serialized, function () { - if (clipPath) { - _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { - _this.clipPath = enlivenedCanvasClip[0]; - _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) + .then(function(enlived) { + _this.clear(); + return fabric.util.enlivenObjectEnlivables({ + backgroundImage: serialized.backgroundImage, + backgroundColor: serialized.background, + overlayImage: serialized.overlayImage, + overlayColor: serialized.overlay, + clipPath: serialized.clipPath, + }) + .then(function(enlivedMap) { + _this.__setupCanvas(serialized, enlived, renderOnAddRemove); + _this.set(enlivedMap); + return _this; }); - } - else { - _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); - } }); - }, reviver); - return this; }, /** * @private * @param {Object} serialized Object with background and overlay information - * @param {Array} restored canvas objects - * @param {Function} cached renderOnAddRemove callback - * @param {Function} callback Invoked after all background and overlay images/patterns loaded + * @param {Array} enlivenedObjects canvas objects + * @param {boolean} renderOnAddRemove renderOnAddRemove setting for the canvas */ - __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove) { var _this = this; enlivenedObjects.forEach(function(obj, index) { // we splice the array just in case some custom classes restored from JSON @@ -14175,122 +14119,17 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // create the Object instance. Here the Canvas is // already an instance and we are just loading things over it this._setOptions(serialized); - this.renderAll(); - callback && callback(); - }, - - /** - * @private - * @param {Object} serialized Object with background and overlay information - * @param {Function} callback Invoked after all background and overlay images/patterns loaded - */ - _setBgOverlay: function(serialized, callback) { - var loaded = { - backgroundColor: false, - overlayColor: false, - backgroundImage: false, - overlayImage: false - }; - - if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { - callback && callback(); - return; - } - - var cbIfLoaded = function () { - if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { - callback && callback(); - } - }; - - this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); - this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); - this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); - this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); - }, - - /** - * @private - * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) - * @param {(Object|String)} value Value to set - * @param {Object} loaded Set loaded property to true if property is set - * @param {Object} callback Callback function to invoke after property is set - */ - __setBgOverlay: function(property, value, loaded, callback) { - var _this = this; - - if (!value) { - loaded[property] = true; - callback && callback(); - return; - } - - if (property === 'backgroundImage' || property === 'overlayImage') { - fabric.util.enlivenObjects([value], function(enlivedObject){ - _this[property] = enlivedObject[0]; - loaded[property] = true; - callback && callback(); - }); - } - else { - this['set' + fabric.util.string.capitalize(property, true)](value, function() { - loaded[property] = true; - callback && callback(); - }); - } - }, - - /** - * @private - * @param {Array} objects - * @param {Function} callback - * @param {Function} [reviver] - */ - _enlivenObjects: function (objects, callback, reviver) { - if (!objects || objects.length === 0) { - callback && callback([]); - return; - } - - fabric.util.enlivenObjects(objects, function(enlivenedObjects) { - callback && callback(enlivenedObjects); - }, null, reviver); - }, - - /** - * @private - * @param {String} format - * @param {Function} callback - */ - _toDataURL: function (format, callback) { - this.clone(function (clone) { - callback(clone.toDataURL(format)); - }); - }, - - /** - * @private - * @param {String} format - * @param {Number} multiplier - * @param {Function} callback - */ - _toDataURLWithMultiplier: function (format, multiplier, callback) { - this.clone(function (clone) { - callback(clone.toDataURLWithMultiplier(format, multiplier)); - }); }, /** * Clones canvas instance - * @param {Object} [callback] Receives cloned instance as a first argument * @param {Array} [properties] Array of properties to include in the cloned canvas and children + * @returns {Promise} */ - clone: function (callback, properties) { + clone: function (properties) { var data = JSON.stringify(this.toJSON(properties)); - this.cloneWithoutData(function(clone) { - clone.loadFromJSON(data, function() { - callback && callback(clone); - }); + return this.cloneWithoutData().then(function(clone) { + return clone.loadFromJSON(data); }); }, @@ -14298,26 +14137,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Clones canvas instance without cloning existing data. * This essentially copies canvas dimensions, clipping properties, etc. * but leaves data empty (so that you can populate it with your own) - * @param {Object} [callback] Receives cloned instance as a first argument + * @returns {Promise} */ - cloneWithoutData: function(callback) { + cloneWithoutData: function() { var el = fabric.util.createCanvasElement(); el.width = this.width; el.height = this.height; - + // this seems wrong. either Canvas or StaticCanvas var clone = new fabric.Canvas(el); + var data = {}; if (this.backgroundImage) { - clone.setBackgroundImage(this.backgroundImage.src, function() { - clone.renderAll(); - callback && callback(clone); - }); - clone.backgroundImageOpacity = this.backgroundImageOpacity; - clone.backgroundImageStretch = this.backgroundImageStretch; + data.backgroundImage = this.backgroundImage.toObject(); } - else { - callback && callback(clone); + if (this.backgroundColor) { + data.background = this.backgroundColor.toObject ? this.backgroundColor.toObject() : this.backgroundColor; } + return clone.loadFromJSON(data); } }); @@ -15051,17 +14887,17 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _getCacheCanvasDimensions: function() { var objectScale = this.getTotalObjectScaling(), // caculate dimensions without skewing - dim = this._getTransformedDimensions(0, 0), - neededX = dim.x * objectScale.scaleX / this.scaleX, - neededY = dim.y * objectScale.scaleY / this.scaleY; + dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }), + neededX = dim.x * objectScale.x / this.scaleX, + neededY = dim.y * objectScale.y / this.scaleY; return { // for sure this ALIASING_LIMIT is slightly creating problem // in situation in which the cache canvas gets an upper limit // also objectScale contains already scaleX and scaleY width: neededX + ALIASING_LIMIT, height: neededY + ALIASING_LIMIT, - zoomX: objectScale.scaleX, - zoomY: objectScale.scaleY, + zoomX: objectScale.x, + zoomY: objectScale.y, x: neededX, y: neededY }; @@ -15139,10 +14975,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ setOptions: function(options) { this._setOptions(options); - this._initGradient(options.fill, 'fill'); - this._initGradient(options.stroke, 'stroke'); - this._initPattern(options.fill, 'fill'); - this._initPattern(options.stroke, 'stroke'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx + * @returns {boolean} true if object needs to fully transform ctx + */ + needsFullTransform: function (ctx) { + return (this.group && !this.group._transformDone) || + (this.group && this.canvas && ctx === this.canvas.contextTop); }, /** @@ -15150,9 +14992,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {CanvasRenderingContext2D} ctx Context */ transform: function(ctx) { - var needFullTransform = (this.group && !this.group._transformDone) || - (this.group && this.canvas && ctx === this.canvas.contextTop); - var m = this.calcTransformMatrix(!needFullTransform); + var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); }, @@ -15227,20 +15067,17 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Object} object */ _removeDefaultValues: function(object) { - var prototype = fabric.util.getKlass(object.type).prototype, - stateProperties = prototype.stateProperties; - stateProperties.forEach(function(prop) { - if (prop === 'left' || prop === 'top') { + var prototype = fabric.util.getKlass(object.type).prototype; + Object.keys(object).forEach(function(prop) { + if (prop === 'left' || prop === 'top' || prop === 'type') { return; } if (object[prop] === prototype[prop]) { delete object[prop]; } - var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && - Object.prototype.toString.call(prototype[prop]) === '[object Array]'; - // basically a check for [] === [] - if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { + if (Array.isArray(object[prop]) && Array.isArray(prototype[prop]) + && object[prop].length === 0 && prototype[prop].length === 0) { delete object[prop]; } }); @@ -15258,7 +15095,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Return the object scale factor counting also the group scaling - * @return {Object} object with scaleX and scaleY properties + * @return {fabric.Point} */ getObjectScaling: function() { // if the object is a top level one, on the canvas, we go for simple aritmetic @@ -15266,14 +15103,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // and will likely kill the cache when not needed // https://github.com/fabricjs/fabric.js/issues/7157 if (!this.group) { - return { - scaleX: this.scaleX, - scaleY: this.scaleY, - }; + return new fabric.Point(Math.abs(this.scaleX), Math.abs(this.scaleY)); } // if we are inside a group total zoom calculation is complex, we defer to generic matrices var options = fabric.util.qrDecompose(this.calcTransformMatrix()); - return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; + return new fabric.Point(Math.abs(options.scaleX), Math.abs(options.scaleY)); }, /** @@ -15281,14 +15115,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Object} object with scaleX and scaleY properties */ getTotalObjectScaling: function() { - var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; + var scale = this.getObjectScaling(); if (this.canvas) { var zoom = this.canvas.getZoom(); var retina = this.canvas.getRetinaScaling(); - scaleX *= zoom * retina; - scaleY *= zoom * retina; + scale.scalarMultiplyEquals(zoom * retina); } - return { scaleX: scaleX, scaleY: scaleY }; + return scale; }, /** @@ -15303,6 +15136,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return opacity; }, + /** + * Returns the object angle relative to canvas counting also the group property + * @returns {number} + */ + getTotalAngle: function () { + return this.group ? + fabric.util.qrDecompose(this.calcTransformMatrix()).angle : + this.angle; + }, + /** * @private * @param {String} key @@ -15346,16 +15189,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this; }, - /** - * This callback function is called by the parent group of an object every - * time a non-delegated property changes on the group. It is passed the key - * and value as parameters. Not adding in this function's signature to avoid - * Travis build error about unused variables. - */ - setOnGroup: function() { - // implemented by sub-classes, as needed. - }, - /** * Retrieves viewportTransform from Object's canvas if possible * @method getViewportTransform @@ -15416,7 +15249,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati renderCache: function(options) { options = options || {}; - if (!this._cacheCanvas) { + if (!this._cacheCanvas || !this._cacheContext) { this._createCacheCanvas(); } if (this.isCacheDirty()) { @@ -15431,6 +15264,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _removeCacheCanvas: function() { this._cacheCanvas = null; + this._cacheContext = null; this.cacheWidth = 0; this.cacheHeight = 0; }, @@ -15503,6 +15337,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Check if this object or a child object will cast a shadow * used by Group.shouldCache to know if child has a shadow recursively * @return {Boolean} + * @deprecated */ willDrawShadow: function() { return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); @@ -15589,7 +15424,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (this.isNotVisible()) { return false; } - if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { + if (this._cacheCanvas && this._cacheContext && !skipCanvas && this._updateCacheCanvas()) { // in this case the context is already cleared. return true; } @@ -15598,7 +15433,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati (this.clipPath && this.clipPath.absolutePositioned) || (this.statefullCache && this.hasStateChanged('cacheProperties')) ) { - if (this._cacheCanvas && !skipCanvas) { + if (this._cacheCanvas && this._cacheContext && !skipCanvas) { var width = this.cacheWidth / this.zoomX; var height = this.cacheHeight / this.zoomY; this._cacheContext.clearRect(-width / 2, -height / 2, width, height); @@ -15754,24 +15589,19 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return; } - var shadow = this.shadow, canvas = this.canvas, scaling, + var shadow = this.shadow, canvas = this.canvas, multX = (canvas && canvas.viewportTransform[0]) || 1, - multY = (canvas && canvas.viewportTransform[3]) || 1; - if (shadow.nonScaling) { - scaling = { scaleX: 1, scaleY: 1 }; - } - else { - scaling = this.getObjectScaling(); - } + multY = (canvas && canvas.viewportTransform[3]) || 1, + scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); if (canvas && canvas._isRetinaScaling()) { multX *= fabric.devicePixelRatio; multY *= fabric.devicePixelRatio; } ctx.shadowColor = shadow.color; ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * - (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; - ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; - ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; + (multX + multY) * (scaling.x + scaling.y) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.x; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.y; }, /** @@ -15874,12 +15704,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati } ctx.save(); - if (this.strokeUniform && this.group) { + if (this.strokeUniform) { var scaling = this.getObjectScaling(); - ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); - } - else if (this.strokeUniform) { - ctx.scale(1 / this.scaleX, 1 / this.scaleY); + ctx.scale(1 / scaling.x, 1 / scaling.y); } this._setLineDash(ctx, this.strokeDashArray); this._setStrokeStyles(ctx, this); @@ -15981,18 +15808,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Clones an instance, using a callback method will work for every object. - * @param {Function} callback Callback is invoked with a clone as a first argument + * Clones an instance. * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @returns {Promise} */ - clone: function(callback, propertiesToInclude) { + clone: function(propertiesToInclude) { var objectForm = this.toObject(propertiesToInclude); - if (this.constructor.fromObject) { - this.constructor.fromObject(objectForm, callback); - } - else { - fabric.Object._fromObject('Object', objectForm, callback); - } + return this.constructor.fromObject(objectForm); }, /** @@ -16002,9 +15824,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * and format option. toCanvasElement is faster and produce no loss of quality. * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. * toCanvasElement and then toBlob from the obtained canvas is also a good option. - * This method is sync now, but still support the callback because we did not want to break. - * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. - * @param {Function} callback callback, invoked with an instance as a first argument * @param {Object} [options] for clone as image, passed to toDataURL * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 @@ -16014,14 +15833,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {fabric.Object} thisArg + * @return {fabric.Image} Object cloned as image. */ - cloneAsImage: function(callback, options) { + cloneAsImage: function(options) { var canvasEl = this.toCanvasElement(options); - if (callback) { - callback(new fabric.Image(canvasEl)); - } - return this; + return new fabric.Image(canvasEl); }, /** @@ -16043,7 +15859,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var utils = fabric.util, origParams = utils.saveObjectTransform(this), originalGroup = this.group, originalShadow = this.shadow, abs = Math.abs, - multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); + retinaScaling = options.enableRetinaScaling ? Math.max(fabric.devicePixelRatio, 1) : 1, + multiplier = (options.multiplier || 1) * retinaScaling; delete this.group; if (options.withoutTransform) { utils.resetObjectTransform(this); @@ -16055,21 +15872,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var el = fabric.util.createCanvasElement(), // skip canvas zoom and calculate with setCoords now. boundingRect = this.getBoundingRect(true, true), - shadow = this.shadow, scaling, - shadowOffset = { x: 0, y: 0 }, shadowBlur, + shadow = this.shadow, shadowOffset = { x: 0, y: 0 }, width, height; if (shadow) { - shadowBlur = shadow.blur; - if (shadow.nonScaling) { - scaling = { scaleX: 1, scaleY: 1 }; - } - else { - scaling = this.getObjectScaling(); - } + var shadowBlur = shadow.blur; + var scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); // consider non scaling shadow. - shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); - shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.x)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.y)); } width = boundingRect.width + shadowOffset.x; height = boundingRect.height + shadowOffset.y; @@ -16132,7 +15943,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Boolean} */ isType: function(type) { - return this.type === type; + return arguments.length > 1 ? Array.from(arguments).includes(this.type) : this.type === type; }, /** @@ -16242,23 +16053,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Returns coordinates of a pointer relative to an object - * @param {Event} e Event to operate upon - * @param {Object} [pointer] Pointer to operate upon (instead of event) - * @return {Object} Coordinates of a pointer (x, y) + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. */ - getLocalPointer: function(e, pointer) { - pointer = pointer || this.canvas.getPointer(e); - var pClicked = new fabric.Point(pointer.x, pointer.y), - objectLeftTop = this._getLeftTopCoords(); - if (this.angle) { - pClicked = fabric.util.rotatePoint( - pClicked, objectLeftTop, degreesToRadians(-this.angle)); - } - return { - x: pClicked.x - objectLeftTop.x, - y: pClicked.y - objectLeftTop.y - }; + setOnGroup: function() { + // implemented by sub-classes, as needed. }, /** @@ -16304,25 +16105,19 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @constant * @type string[] */ - fabric.Object.ENLIVEN_PROPS = ['clipPath']; - fabric.Object._fromObject = function(className, object, callback, extraParam) { - var klass = fabric[className]; - object = clone(object, true); - fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { - if (typeof patterns[0] !== 'undefined') { - object.fill = patterns[0]; - } - if (typeof patterns[1] !== 'undefined') { - object.stroke = patterns[1]; - } - fabric.util.enlivenObjectEnlivables(object, object, function () { - var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); - callback && callback(instance); - }); + fabric.Object._fromObject = function(klass, object, extraParam) { + var serializedObject = clone(object, true); + return fabric.util.enlivenObjectEnlivables(serializedObject).then(function(enlivedMap) { + var newObject = Object.assign(object, enlivedMap); + return extraParam ? new klass(object[extraParam], newObject) : new klass(newObject); }); }; + fabric.Object.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Object, object); + }; + /** * Unique id used internally when creating SVG elements * @static @@ -16347,68 +16142,67 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati bottom: 0.5 }; + /** + * @typedef {number | 'left' | 'center' | 'right'} OriginX + * @typedef {number | 'top' | 'center' | 'bottom'} OriginY + */ + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Resolves origin value relative to center + * @private + * @param {OriginX} originX + * @returns number + */ + resolveOriginX: function (originX) { + return typeof originX === 'string' ? + originXOffset[originX] : + originX - 0.5; + }, + + /** + * Resolves origin value relative to center + * @private + * @param {OriginY} originY + * @returns number + */ + resolveOriginY: function (originY) { + return typeof originY === 'string' ? + originYOffset[originY] : + originY - 0.5; + }, + /** * Translates the coordinates from a set of origin to another (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' - * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} toOriginY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { var x = point.x, y = point.y, - offsetX, offsetY, dim; - - if (typeof fromOriginX === 'string') { - fromOriginX = originXOffset[fromOriginX]; - } - else { - fromOriginX -= 0.5; - } + dim, + offsetX = this.resolveOriginX(toOriginX) - this.resolveOriginX(fromOriginX), + offsetY = this.resolveOriginY(toOriginY) - this.resolveOriginY(fromOriginY); - if (typeof toOriginX === 'string') { - toOriginX = originXOffset[toOriginX]; - } - else { - toOriginX -= 0.5; + if (offsetX || offsetY) { + dim = this._getTransformedDimensions(); + x = point.x + offsetX * dim.x; + y = point.y + offsetY * dim.y; } - offsetX = toOriginX - fromOriginX; - - if (typeof fromOriginY === 'string') { - fromOriginY = originYOffset[fromOriginY]; - } - else { - fromOriginY -= 0.5; - } - - if (typeof toOriginY === 'string') { - toOriginY = originYOffset[toOriginY]; - } - else { - toOriginY -= 0.5; - } - - offsetY = toOriginY - fromOriginY; - - if (offsetX || offsetY) { - dim = this._getTransformedDimensions(); - x = point.x + offsetX * dim.x; - y = point.y + offsetY * dim.y; - } - - return new fabric.Point(x, y); - }, + return new fabric.Point(x, y); + }, /** * Translates the coordinates from origin to center coordinates (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToCenterPoint: function(point, originX, originY) { @@ -16422,8 +16216,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Translates the coordinates from center to origin coordinates (based on the object's dimensions) * @param {fabric.Point} center The point which corresponds to center of the object - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { @@ -16435,12 +16229,30 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Returns the real center coordinates of the object + * Returns the center coordinates of the object relative to canvas * @return {fabric.Point} */ getCenterPoint: function() { - var leftTop = new fabric.Point(this.left, this.top); - return this.translateToCenterPoint(leftTop, this.originX, this.originY); + var relCenter = this.getRelativeCenterPoint(); + return this.group ? + fabric.util.transformPoint(relCenter, this.group.calcTransformMatrix()) : + relCenter; + }, + + /** + * Returns the center coordinates of the object relative to it's containing group or null + * @return {fabric.Point|null} point or null of object has no parent group + */ + getCenterPointRelativeToParent: function () { + return this.group ? this.getRelativeCenterPoint() : null; + }, + + /** + * Returns the center coordinates of the object relative to it's parent + * @return {fabric.Point} + */ + getRelativeCenterPoint: function () { + return this.translateToCenterPoint(new fabric.Point(this.left, this.top), this.originX, this.originY); }, /** @@ -16454,26 +16266,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Returns the coordinates of the object as if it has a different origin - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ getPointByOrigin: function(originX, originY) { - var center = this.getCenterPoint(); + var center = this.getRelativeCenterPoint(); return this.translateToOriginPoint(center, originX, originY); }, /** - * Returns the point in local coordinates - * @param {fabric.Point} point The point relative to the global coordinate system - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * Returns the normalized point (rotated relative to center) in local coordinates + * @param {fabric.Point} point The point relative to instance coordinate system + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ - toLocalPoint: function(point, originX, originY) { - var center = this.getCenterPoint(), - p, p2; - + normalizePoint: function(point, originX, originY) { + var center = this.getRelativeCenterPoint(), p, p2; if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); } @@ -16488,6 +16298,20 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return p2.subtractEquals(p); }, + /** + * Returns coordinates of a pointer relative to object's top left corner in object's plane + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) + */ + getLocalPointer: function (e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + return fabric.util.transformPoint( + new fabric.Point(pointer.x, pointer.y), + fabric.util.invertTransform(this.calcTransformMatrix()) + ).addEquals(new fabric.Point(this.width / 2, this.height / 2)); + }, + /** * Returns the point in global coordinates * @param {fabric.Point} The point relative to the local coordinate system @@ -16500,8 +16324,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Sets the position of the object taking into consideration the object's origin * @param {fabric.Point} pos The new position of the object - * @param {String} originX Horizontal origin: 'left', 'center' or 'right' - * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {void} */ setPositionByOrigin: function(pos, originX, originY) { @@ -16549,7 +16373,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this._originalOriginX = this.originX; this._originalOriginY = this.originY; - var center = this.getCenterPoint(); + var center = this.getRelativeCenterPoint(); this.originX = 'center'; this.originY = 'center'; @@ -16565,7 +16389,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _resetOrigin: function() { var originPoint = this.translateToOriginPoint( - this.getCenterPoint(), + this.getRelativeCenterPoint(), this._originalOriginX, this._originalOriginY); @@ -16583,7 +16407,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @private */ _getLeftTopCoords: function() { - return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + return this.translateToOriginPoint(this.getRelativeCenterPoint(), 'left', 'top'); }, }); @@ -16658,6 +16482,113 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ controls: { }, + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane + */ + getX: function () { + return this.getXY().x; + }, + + /** + * @param {number} value x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane + */ + setX: function (value) { + this.setXY(this.getXY().setX(value)); + }, + + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#getX} + */ + getRelativeX: function () { + return this.left; + }, + + /** + * @param {number} value x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ + * if parent is canvas then this method is identical to {@link fabric.Object#setX} + */ + setRelativeX: function (value) { + this.left = value; + }, + + /** + * @returns {number} y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane + */ + getY: function () { + return this.getXY().y; + }, + + /** + * @param {number} value y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane + */ + setY: function (value) { + this.setXY(this.getXY().setY(value)); + }, + + /** + * @returns {number} y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#getY} + */ + getRelativeY: function () { + return this.top; + }, + + /** + * @param {number} value y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#setY} + */ + setRelativeY: function (value) { + this.top = value; + }, + + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in canvas coordinate plane + */ + getXY: function () { + var relativePosition = this.getRelativeXY(); + return this.group ? + fabric.util.transformPoint(relativePosition, this.group.calcTransformMatrix()) : + relativePosition; + }, + + /** + * Set an object position to a particular point, the point is intended in absolute ( canvas ) coordinate. + * You can specify {@link fabric.Object#originX} and {@link fabric.Object#originY} values, + * that otherwise are the object's current values. + * @example Set object's bottom left corner to point (5,5) on canvas + * object.setXY(new fabric.Point(5, 5), 'left', 'bottom'). + * @param {fabric.Point} point position in canvas coordinate plane + * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' + * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' + */ + setXY: function (point, originX, originY) { + if (this.group) { + point = fabric.util.transformPoint( + point, + fabric.util.invertTransform(this.group.calcTransformMatrix()) + ); + } + this.setRelativeXY(point, originX, originY); + }, + + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane + */ + getRelativeXY: function () { + return new fabric.Point(this.left, this.top); + }, + + /** + * As {@link fabric.Object#setXY}, but in current parent's coordinate plane ( the current group if any or the canvas) + * @param {fabric.Point} point position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane + * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' + * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' + */ + setRelativeXY: function (point, originX, originY) { + this.setPositionByOrigin(point, originX || this.originX, originY || this.originY); + }, + /** * return correct set of coordinates for intersection * this will return either aCoords or lineCoords. @@ -16680,8 +16611,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * The coords are returned in an array. * @return {Array} [tl, tr, br, bl] of points */ - getCoords: function(absolute, calculate) { - return arrayFromCoords(this._getCoords(absolute, calculate)); + getCoords: function (absolute, calculate) { + var coords = arrayFromCoords(this._getCoords(absolute, calculate)); + if (this.group) { + var t = this.group.calcTransformMatrix(); + return coords.map(function (p) { + return util.transformPoint(p, t); + }); + } + return coords; }, /** @@ -17023,7 +16961,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati calcLineCoords: function() { var vpt = this.getViewportTransform(), - padding = this.padding, angle = degreesToRadians(this.angle), + padding = this.padding, angle = degreesToRadians(this.getTotalAngle()), cos = util.cos(angle), sin = util.sin(angle), cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); @@ -17053,7 +16991,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var rotateMatrix = this._calcRotateMatrix(), translateMatrix = this._calcTranslateMatrix(), vpt = this.getViewportTransform(), - startMatrix = multiplyMatrices(vpt, translateMatrix), + startMatrix = this.group ? multiplyMatrices(vpt, this.group.calcTransformMatrix()) : vpt, + startMatrix = multiplyMatrices(startMatrix, translateMatrix), finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), dim = this._calculateCurrentDimensions(), @@ -17063,15 +17002,18 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }); // debug code - // var canvas = this.canvas; - // setTimeout(function() { - // canvas.contextTop.clearRect(0, 0, 700, 700); - // canvas.contextTop.fillStyle = 'green'; - // Object.keys(coords).forEach(function(key) { - // var control = coords[key]; - // canvas.contextTop.fillRect(control.x, control.y, 3, 3); - // }); - // }, 50); + /* + var canvas = this.canvas; + setTimeout(function () { + if (!canvas) return; + canvas.contextTop.clearRect(0, 0, 700, 700); + canvas.contextTop.fillStyle = 'green'; + Object.keys(coords).forEach(function(key) { + var control = coords[key]; + canvas.contextTop.fillRect(control.x, control.y, 3, 3); + }); + }, 50); + */ return coords; }, @@ -17128,7 +17070,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Array} rotation matrix for the object */ _calcTranslateMatrix: function() { - var center = this.getCenterPoint(); + var center = this.getRelativeCenterPoint(); return [1, 0, 0, 1, center.x, center.y]; }, @@ -17193,77 +17135,65 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return cache.value; }, - /* + /** * Calculate object dimensions from its properties * @private - * @return {Object} .x width dimension - * @return {Object} .y height dimension + * @returns {fabric.Point} dimensions */ _getNonTransformedDimensions: function() { - var strokeWidth = this.strokeWidth, - w = this.width + strokeWidth, - h = this.height + strokeWidth; - return { x: w, y: h }; + return new fabric.Point(this.width, this.height).scalarAddEquals(this.strokeWidth); }, - /* + /** * Calculate object bounding box dimensions from its properties scale, skew. - * @param {Number} skewX, a value to override current skewX - * @param {Number} skewY, a value to override current skewY + * @param {Object} [options] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewY] * @private - * @return {Object} .x width dimension - * @return {Object} .y height dimension + * @returns {fabric.Point} dimensions */ - _getTransformedDimensions: function(skewX, skewY) { - if (typeof skewX === 'undefined') { - skewX = this.skewX; - } - if (typeof skewY === 'undefined') { - skewY = this.skewY; - } - var dimensions, dimX, dimY, - noSkew = skewX === 0 && skewY === 0; - + _getTransformedDimensions: function (options) { + options = Object.assign({ + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + width: this.width, + height: this.height, + strokeWidth: this.strokeWidth + }, options || {}); + // stroke is applied before/after transformations are applied according to `strokeUniform` + var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = options.strokeWidth; if (this.strokeUniform) { - dimX = this.width; - dimY = this.height; + preScalingStrokeValue = 0; + postScalingStrokeValue = strokeWidth; } else { - dimensions = this._getNonTransformedDimensions(); - dimX = dimensions.x; - dimY = dimensions.y; + preScalingStrokeValue = strokeWidth; + postScalingStrokeValue = 0; } + var dimX = options.width + preScalingStrokeValue, + dimY = options.height + preScalingStrokeValue, + finalDimensions, + noSkew = options.skewX === 0 && options.skewY === 0; if (noSkew) { - return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); + finalDimensions = new fabric.Point(dimX * options.scaleX, dimY * options.scaleY); + } + else { + var bbox = util.sizeAfterTransform(dimX, dimY, options); + finalDimensions = new fabric.Point(bbox.x, bbox.y); } - var bbox = util.sizeAfterTransform(dimX, dimY, { - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: skewX, - skewY: skewY, - }); - return this._finalizeDimensions(bbox.x, bbox.y); - }, - /* - * Calculate object bounding box dimensions from its properties scale, skew. - * @param Number width width of the bbox - * @param Number height height of the bbox - * @private - * @return {Object} .x finalized width dimension - * @return {Object} .y finalized height dimension - */ - _finalizeDimensions: function(width, height) { - return this.strokeUniform ? - { x: width + this.strokeWidth, y: height + this.strokeWidth } - : - { x: width, y: height }; + return finalDimensions.scalarAddEquals(postScalingStrokeValue); }, - /* + /** * Calculate object dimensions for controls box, including padding and canvas zoom. * and active selection - * private + * @private + * @returns {fabric.Point} dimensions */ _calculateCurrentDimensions: function() { var vpt = this.getViewportTransform(), @@ -17275,6 +17205,130 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati })(); +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Checks if object is decendant of target + * Should be used instead of @link {fabric.Collection.contains} for performance reasons + * @param {fabric.Object|fabric.StaticCanvas} target + * @returns {boolean} + */ + isDescendantOf: function (target) { + var parent = this.group || this.canvas; + while (parent) { + if (target === parent) { + return true; + } + else if (parent instanceof fabric.StaticCanvas) { + // happens after all parents were traversed through without a match + return false; + } + parent = parent.group || parent.canvas; + } + return false; + }, + + /** + * + * @typedef {fabric.Object[] | [...fabric.Object[], fabric.StaticCanvas]} Ancestors + * + * @param {boolean} [strict] returns only ancestors that are objects (without canvas) + * @returns {Ancestors} ancestors from bottom to top + */ + getAncestors: function (strict) { + var ancestors = []; + var parent = this.group || (strict ? undefined : this.canvas); + while (parent) { + ancestors.push(parent); + parent = parent.group || (strict ? undefined : parent.canvas); + } + return ancestors; + }, + + /** + * Returns an object that represent the ancestry situation. + * + * @typedef {object} AncestryComparison + * @property {Ancestors} common ancestors of `this` and `other` (may include `this` | `other`) + * @property {Ancestors} fork ancestors that are of `this` only + * @property {Ancestors} otherFork ancestors that are of `other` only + * + * @param {fabric.Object} other + * @param {boolean} [strict] finds only ancestors that are objects (without canvas) + * @returns {AncestryComparison | undefined} + * + */ + findCommonAncestors: function (other, strict) { + if (this === other) { + return { + fork: [], + otherFork: [], + common: [this].concat(this.getAncestors(strict)) + }; + } + else if (!other) { + // meh, warn and inform, and not my issue. + // the argument is NOT optional, we can't end up here. + return undefined; + } + var ancestors = this.getAncestors(strict); + var otherAncestors = other.getAncestors(strict); + // if `this` has no ancestors and `this` is top ancestor of `other` we must handle the following case + if (ancestors.length === 0 && otherAncestors.length > 0 && this === otherAncestors[otherAncestors.length - 1]) { + return { + fork: [], + otherFork: [other].concat(otherAncestors.slice(0, otherAncestors.length - 1)), + common: [this] + }; + } + // compare ancestors + for (var i = 0, ancestor; i < ancestors.length; i++) { + ancestor = ancestors[i]; + if (ancestor === other) { + return { + fork: [this].concat(ancestors.slice(0, i)), + otherFork: [], + common: ancestors.slice(i) + }; + } + for (var j = 0; j < otherAncestors.length; j++) { + if (this === otherAncestors[j]) { + return { + fork: [], + otherFork: [other].concat(otherAncestors.slice(0, j)), + common: [this].concat(ancestors) + }; + } + if (ancestor === otherAncestors[j]) { + return { + fork: [this].concat(ancestors.slice(0, i)), + otherFork: [other].concat(otherAncestors.slice(0, j)), + common: ancestors.slice(i) + }; + } + } + } + // nothing shared + return { + fork: [this].concat(ancestors), + otherFork: [other].concat(otherAncestors), + common: [] + }; + }, + + /** + * + * @param {fabric.Object} other + * @param {boolean} [strict] checks only ancestors that are objects (without canvas) + * @returns {boolean} + */ + hasCommonAncestors: function (other, strict) { + var commonAncestors = this.findCommonAncestors(other, strict); + return commonAncestors && !!commonAncestors.ancestors.length; + } +}); + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** @@ -17353,6 +17407,36 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this.canvas.moveTo(this, index); } return this; + }, + + /** + * + * @param {fabric.Object} other object to compare against + * @returns {boolean | undefined} if objects do not share a common ancestor or they are strictly equal it is impossible to determine which is in front of the other; in such cases the function returns `undefined` + */ + isInFrontOf: function (other) { + if (this === other) { + return undefined; + } + var ancestorData = this.findCommonAncestors(other); + if (!ancestorData) { + return undefined; + } + if (ancestorData.fork.includes(other)) { + return true; + } + if (ancestorData.otherFork.includes(this)) { + return false; + } + var firstCommonAncestor = ancestorData.common[0]; + if (!firstCommonAncestor) { + return undefined; + } + var headOfFork = ancestorData.fork.pop(), + headOfOtherFork = ancestorData.otherFork.pop(), + thisIndex = firstCommonAncestor._objects.indexOf(headOfFork), + otherIndex = firstCommonAncestor._objects.indexOf(headOfOtherFork); + return thisIndex > -1 && thisIndex > otherIndex; } }); @@ -17738,15 +17822,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ _findTargetCorner: function(pointer, forTouch) { - // objects in group, anykind, are not self modificable, - // must not return an hovered corner. - if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { + if (!this.hasControls || (!this.canvas || this.canvas._activeObject !== this)) { return false; } - - var ex = pointer.x, - ey = pointer.y, - xPoints, + var xPoints, lines, keys = Object.keys(this.oCoords), j = keys.length - 1, i; this.__corner = 0; @@ -17773,7 +17852,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); + xPoints = this._findCrossPoints(pointer, lines); if (xPoints !== 0 && xPoints % 2 === 1) { this.__corner = i; return i; @@ -17829,7 +17908,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; } ctx.save(); - var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), + var center = this.getRelativeCenterPoint(), wh = this._calculateCurrentDimensions(), vpt = this.canvas.viewportTransform; ctx.translate(center.x, center.y); ctx.scale(1 / vpt[0], 1 / vpt[3]); @@ -17856,8 +17935,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = wh.x + strokeWidth, height = wh.y + strokeWidth, hasControls = typeof styleOverride.hasControls !== 'undefined' ? - styleOverride.hasControls : this.hasControls, - shouldStroke = false; + styleOverride.hasControls : this.hasControls; ctx.save(); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17869,26 +17947,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); + hasControls && this.drawControlsConnectingLines(ctx); - if (hasControls) { - ctx.beginPath(); - this.forEachControl(function(control, key, fabricObject) { - // in this moment, the ctx is centered on the object. - // width and height of the above function are the size of the bbox. - if (control.withConnection && control.getVisibility(fabricObject, key)) { - // reset movement for each control - shouldStroke = true; - ctx.moveTo(control.x * width, control.y * height); - ctx.lineTo( - control.x * width + control.offsetX, - control.y * height + control.offsetY - ); - } - }); - if (shouldStroke) { - ctx.stroke(); - } - } ctx.restore(); return this; }, @@ -17912,7 +17972,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, height = - bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor, + hasControls = typeof styleOverride.hasControls !== 'undefined' ? + styleOverride.hasControls : this.hasControls; ctx.save(); this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17922,11 +17984,46 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); + hasControls && this.drawControlsConnectingLines(ctx); ctx.restore(); return this; }, + /** + * Draws lines from a borders of an object's bounding box to controls that have `withConnection` property set. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawControlsConnectingLines: function (ctx) { + var wh = this._calculateCurrentDimensions(), + strokeWidth = this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth, + shouldStroke = false; + + ctx.beginPath(); + this.forEachControl(function (control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + shouldStroke && ctx.stroke(); + + return this; + }, + /** * Draws corners of an object's bounding box. * Requires public properties: width, height @@ -17939,7 +18036,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot drawControls: function(ctx, styleOverride) { styleOverride = styleOverride || {}; ctx.save(); - var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; + var retinaScaling = this.canvas.getRetinaScaling(), p; ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; if (!this.transparentCorners) { @@ -17947,20 +18044,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); this.setCoords(); - if (this.group) { - // fabricJS does not really support drawing controls inside groups, - // this piece of code here helps having at least the control in places. - // If an application needs to show some objects as selected because of some UI state - // can still call Object._renderControls() on any object they desire, independently of groups. - // using no padding, circular controls and hiding the rotating cursor is higly suggested, - matrix = this.group.calcTransformMatrix(); - } this.forEachControl(function(control, key, fabricObject) { - p = fabricObject.oCoords[key]; if (control.getVisibility(fabricObject, key)) { - if (matrix) { - p = fabric.util.transformPoint(p, matrix); - } + p = fabricObject.oCoords[key]; control.render(ctx, p.x, p.y, styleOverride, fabricObject); } }); @@ -18069,11 +18155,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, - startValue: object.left, - endValue: this.getCenter().left, + startValue: object.getX(), + endValue: this.getCenterPoint().x, duration: this.FX_DURATION, onChange: function(value) { - object.set('left', value); + object.setX(value); _this.requestRenderAll(); onChange(); }, @@ -18102,11 +18188,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, - startValue: object.top, - endValue: this.getCenter().top, + startValue: object.getY(), + endValue: this.getCenterPoint().y, duration: this.FX_DURATION, onChange: function(value) { - object.set('top', value); + object.setY(value); _this.requestRenderAll(); onChange(); }, @@ -18561,16 +18647,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Line * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument + * @returns {Promise} */ - fabric.Line.fromObject = function(object, callback) { - function _callback(instance) { - delete instance.points; - callback && callback(instance); - }; + fabric.Line.fromObject = function(object) { var options = clone(object, true); options.points = [object.x1, object.y1, object.x2, object.y2]; - fabric.Object._fromObject('Line', options, _callback, 'points'); + return fabric.Object._fromObject(fabric.Line, options, 'points').then(function(fabricLine) { + delete fabricLine.points; + return fabricLine; + }); }; /** @@ -18803,11 +18888,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Circle * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - * @return {void} + * @returns {Promise} */ - fabric.Circle.fromObject = function(object, callback) { - fabric.Object._fromObject('Circle', object, callback); + fabric.Circle.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Circle, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -18899,10 +18983,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Triangle * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument + * @returns {Promise} */ - fabric.Triangle.fromObject = function(object, callback) { - return fabric.Object._fromObject('Triangle', object, callback); + fabric.Triangle.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Triangle, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19081,11 +19165,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Ellipse * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as first argument - * @return {void} + * @returns {Promise} */ - fabric.Ellipse.fromObject = function(object, callback) { - fabric.Object._fromObject('Ellipse', object, callback); + fabric.Ellipse.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Ellipse, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19271,10 +19354,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Rect * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + * @returns {Promise} */ - fabric.Rect.fromObject = function(object, callback) { - return fabric.Object._fromObject('Rect', object, callback); + fabric.Rect.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Rect, object); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19365,6 +19448,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, _setPositionDimensions: function(options) { + options || (options = {}); var calcDim = this._calcDimensions(options), correctLeftTop, correctSize = this.exactBoundingBox ? this.strokeWidth : 0; this.width = calcDim.width - correctSize; @@ -19541,10 +19625,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polyline * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @returns {Promise} */ - fabric.Polyline.fromObject = function(object, callback) { - return fabric.Object._fromObject('Polyline', object, callback, 'points'); + fabric.Polyline.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Polyline, object, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19623,11 +19707,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polygon * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - * @return {void} + * @returns {Promise} */ - fabric.Polygon.fromObject = function(object, callback) { - fabric.Object._fromObject('Polygon', object, callback, 'points'); + fabric.Polygon.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Polygon, object, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19642,7 +19725,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot max = fabric.util.array.max, extend = fabric.util.object.extend, clone = fabric.util.object.clone, - _toString = Object.prototype.toString, toFixed = fabric.util.toFixed; if (fabric.Path) { @@ -19696,10 +19778,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Object} [options] Options object */ _setPath: function (path, options) { - var fromArray = _toString.call(path) === '[object Array]'; - this.path = fabric.util.makePathSimpler( - fromArray ? path : fabric.util.parsePath(path) + Array.isArray(path) ? path : fabric.util.parsePath(path) ); fabric.Polyline.prototype._setPositionDimensions.call(this, options || {}); @@ -19970,20 +20050,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Path * @param {Object} object - * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - */ - fabric.Path.fromObject = function(object, callback) { - if (typeof object.sourcePath === 'string') { - var pathUrl = object.sourcePath; - fabric.loadSVGFromURL(pathUrl, function (elements) { - var path = elements[0]; - path.setOptions(object); - callback && callback(path); - }); - } - else { - fabric.Object._fromObject('Path', object, callback, 'path'); - } + * @returns {Promise} + */ + fabric.Path.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Path, object, 'path'); }; /* _FROM_SVG_START_ */ @@ -20014,15 +20084,21 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot })(typeof exports !== 'undefined' ? exports : this); -(function(global) { +(function (global) { 'use strict'; - var fabric = global.fabric || (global.fabric = { }), - min = fabric.util.array.min, - max = fabric.util.array.max; + var fabric = global.fabric || (global.fabric = {}), + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, + invertTransform = fabric.util.invertTransform, + transformPoint = fabric.util.transformPoint, + applyTransformToObject = fabric.util.applyTransformToObject, + degreesToRadians = fabric.util.degreesToRadians, + clone = fabric.util.object.clone, + extend = fabric.util.object.extend; if (fabric.Group) { + fabric.warn('fabric.Group is already defined'); return; } @@ -20031,280 +20107,351 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Group * @extends fabric.Object * @mixes fabric.Collection - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @fires layout once layout completes * @see {@link fabric.Group#initialize} for constructor definition */ fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { /** * Type of an object - * @type String + * @type string * @default */ type: 'group', + /** + * Specifies the **layout strategy** for instance + * Used by `getLayoutStrategyResult` to calculate layout + * `fit-content`, `fit-content-lazy`, `fixed`, `clip-path` are supported out of the box + * @type string + * @default + */ + layout: 'fit-content', + /** * Width of stroke * @type Number - * @default */ strokeWidth: 0, /** - * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets - * @type Boolean + * List of properties to consider when checking if state + * of an object is changed (fabric.Object#hasStateChanged) + * as well as for history (undo/redo) purposes + * @type string[] + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('layout'), + + /** + * Used to optimize performance + * set to `false` if you don't need contained objects to be targets of events * @default + * @type boolean */ subTargetCheck: false, /** - * Groups are container, do not render anything on theyr own, ence no cache properties - * @type Array + * Used to allow targeting of object inside groups. + * set to true if you want to select an object inside a group.\ + * **REQUIRES** `subTargetCheck` set to true * @default + * @type boolean */ - cacheProperties: [], + interactive: false, /** - * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still - * available setting this boolean to true. - * @type Boolean - * @since 2.0.0 - * @default + * Used internally to optimize performance + * Once an object is selected, instance is rendered without the selected object. + * This way instance is cached only once for the entire interaction with the selected object. + * @private */ - useSetOnGroup: false, + _activeObjects: undefined, /** * Constructor - * @param {Object} objects Group objects + * + * @param {fabric.Object[]} [objects] instance objects * @param {Object} [options] Options object - * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. - * @return {Object} thisArg + * @param {boolean} [objectsRelativeToGroup] true if objects exist in group coordinate plane + * @return {fabric.Group} thisArg */ - initialize: function(objects, options, isAlreadyGrouped) { - options = options || {}; - this._objects = []; - // if objects enclosed in a group have been grouped already, - // we cannot change properties of objects. - // Thus we need to set options to group without objects, - isAlreadyGrouped && this.callSuper('initialize', options); + initialize: function (objects, options, objectsRelativeToGroup) { this._objects = objects || []; - for (var i = this._objects.length; i--; ) { - this._objects[i].group = this; - } + this._activeObjects = []; + this.__objectMonitor = this.__objectMonitor.bind(this); + this.__objectSelectionTracker = this.__objectSelectionMonitor.bind(this, true); + this.__objectSelectionDisposer = this.__objectSelectionMonitor.bind(this, false); + this._firstLayoutDone = false; + // setting angle, skewX, skewY must occur after initial layout + this.callSuper('initialize', Object.assign({}, options, { angle: 0, skewX: 0, skewY: 0 })); + this.forEachObject(function (object) { + this.enterGroup(object, false); + object.fire('added:initialized', { target: this }); + }, this); + this._applyLayoutStrategy({ + type: 'initialization', + options: options, + objectsRelativeToGroup: objectsRelativeToGroup + }); + }, - if (!isAlreadyGrouped) { - var center = options && options.centerPoint; - // we want to set origins before calculating the bounding box. - // so that the topleft can be set with that in mind. - // if specific top and left are passed, are overwritten later - // with the callSuper('initialize', options) - if (options.originX !== undefined) { - this.originX = options.originX; - } - if (options.originY !== undefined) { - this.originY = options.originY; - } - // if coming from svg i do not want to calc bounds. - // i assume width and height are passed along options - center || this._calcBounds(); - this._updateObjectsCoords(center); - delete options.centerPoint; - this.callSuper('initialize', options); + /** + * @private + * @param {string} key + * @param {*} value + */ + _set: function (key, value) { + var prev = this[key]; + this.callSuper('_set', key, value); + if (key === 'canvas' && prev !== value) { + this.forEachObject(function (object) { + object._set(key, value); + }); } - else { - this._updateObjectsACoords(); + if (key === 'layout' && prev !== value) { + this._applyLayoutStrategy({ type: 'layout_change', layout: value, prevLayout: prev }); } - - this.setCoords(); + if (key === 'interactive') { + this.forEachObject(this._watchObject.bind(this, value)); + } + return this; }, /** * @private */ - _updateObjectsACoords: function() { - var skipControls = true; - for (var i = this._objects.length; i--; ){ - this._objects[i].setCoords(skipControls); - } + _shouldSetNestedCoords: function () { + return this.subTargetCheck; }, /** - * @private - * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + * Add objects + * @param {...fabric.Object} objects */ - _updateObjectsCoords: function(center) { - var center = center || this.getCenterPoint(); - for (var i = this._objects.length; i--; ){ - this._updateObjectCoords(this._objects[i], center); - } + add: function () { + fabric.Collection.add.call(this, arguments, this._onObjectAdded); + this._onAfterObjectsChange('added', Array.from(arguments)); }, /** - * @private - * @param {Object} object - * @param {fabric.Point} center, current center of group. + * Inserts an object into collection at specified index + * @param {fabric.Object} objects Object to insert + * @param {Number} index Index to insert object at */ - _updateObjectCoords: function(object, center) { - var objectLeft = object.left, - objectTop = object.top, - skipControls = true; + insertAt: function (objects, index) { + fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); + this._onAfterObjectsChange('added', Array.isArray(objects) ? objects : [objects]); + }, - object.set({ - left: objectLeft - center.x, - top: objectTop - center.y - }); - object.group = this; - object.setCoords(skipControls); + /** + * Remove objects + * @param {...fabric.Object} objects + * @returns {fabric.Object[]} removed objects + */ + remove: function () { + var removed = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); + this._onAfterObjectsChange('removed', removed); + return removed; }, /** - * Returns string represenation of a group - * @return {String} + * Remove all objects + * @returns {fabric.Object[]} removed objects */ - toString: function() { - return '#'; + removeAll: function () { + this._activeObjects = []; + return this.remove.apply(this, this._objects.slice()); }, /** - * Adds an object to a group; Then recalculates group's dimension, position. - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable + * invalidates layout on object modified + * @private */ - addWithUpdate: function(object) { - var nested = !!this.group; - this._restoreObjectsState(); - fabric.util.resetObjectTransform(this); - if (object) { - if (nested) { - // if this group is inside another group, we need to pre transform the object - fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); - } - this._objects.push(object); - object.group = this; - object._set('canvas', this.canvas); - } - this._calcBounds(); - this._updateObjectsCoords(); - this.dirty = true; - if (nested) { - this.group.addWithUpdate(); + __objectMonitor: function (opt) { + this._applyLayoutStrategy(extend(clone(opt), { + type: 'object_modified' + })); + this._set('dirty', true); + }, + + /** + * keeps track of the selected objects + * @private + */ + __objectSelectionMonitor: function (selected, opt) { + var object = opt.target; + if (selected) { + this._activeObjects.push(object); + this._set('dirty', true); } - else { - this.setCoords(); + else if (this._activeObjects.length > 0) { + var index = this._activeObjects.indexOf(object); + if (index > -1) { + this._activeObjects.splice(index, 1); + this._set('dirty', true); + } } - return this; }, /** - * Removes an object from a group; Then recalculates group's dimension, position. - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable + * @private + * @param {boolean} watch + * @param {fabric.Object} object */ - removeWithUpdate: function(object) { - this._restoreObjectsState(); - fabric.util.resetObjectTransform(this); + _watchObject: function (watch, object) { + var directive = watch ? 'on' : 'off'; + // make sure we listen only once + watch && this._watchObject(false, object); + object[directive]('changed', this.__objectMonitor); + object[directive]('modified', this.__objectMonitor); + object[directive]('selected', this.__objectSelectionTracker); + object[directive]('deselected', this.__objectSelectionDisposer); + }, - this.remove(object); - this._calcBounds(); - this._updateObjectsCoords(); - this.setCoords(); - this.dirty = true; - return this; + /** + * Checks if object can enter group and logs relevant warnings + * @private + * @param {fabric.Object} object + * @returns + */ + canEnter: function (object) { + if (object === this || this.isDescendantOf(object)) { + /* _DEV_MODE_START_ */ + console.warn('fabric.Group: trying to add group to itself, this call has no effect'); + /* _DEV_MODE_END_ */ + return false; + } + else if (object.group && object.group === this) { + /* _DEV_MODE_START_ */ + console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); + /* _DEV_MODE_END_ */ + return false; + } + else if (object.group) { + /* _DEV_MODE_START_ */ + console.warn('fabric.Group: object is about to enter group and leave another'); + /* _DEV_MODE_END_ */ + } + return true; }, /** * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + * @returns {boolean} true if object entered group */ - _onObjectAdded: function(object) { - this.dirty = true; - object.group = this; + enterGroup: function (object, removeParentTransform) { + if (!this.canEnter(object)) { + return false; + } + if (object.group) { + object.group.remove(object); + } + this._enterGroup(object, removeParentTransform); + return true; + }, + + /** + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + */ + _enterGroup: function (object, removeParentTransform) { + if (removeParentTransform) { + // can this be converted to utils (sendObjectToPlane)? + applyTransformToObject( + object, + multiplyTransformMatrices( + invertTransform(this.calcTransformMatrix()), + object.calcTransformMatrix() + ) + ); + } + this._shouldSetNestedCoords() && object.setCoords(); + object._set('group', this); object._set('canvas', this.canvas); + this.interactive && this._watchObject(true, object); + var activeObject = this.canvas && this.canvas.getActiveObject && this.canvas.getActiveObject(); + // if we are adding the activeObject in a group + if (activeObject && (activeObject === object || object.isDescendantOf(activeObject))) { + this._activeObjects.push(object); + } }, /** * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it */ - _onObjectRemoved: function(object) { - this.dirty = true; - delete object.group; + exitGroup: function (object, removeParentTransform) { + this._exitGroup(object, removeParentTransform); + object._set('canvas', undefined); }, /** * @private - */ - _set: function(key, value) { - var i = this._objects.length; - if (this.useSetOnGroup) { - while (i--) { - this._objects[i].setOnGroup(key, value); - } + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it + */ + _exitGroup: function (object, removeParentTransform) { + object._set('group', undefined); + if (!removeParentTransform) { + applyTransformToObject( + object, + multiplyTransformMatrices( + this.calcTransformMatrix(), + object.calcTransformMatrix() + ) + ); + object.setCoords(); } - if (key === 'canvas') { - while (i--) { - this._objects[i]._set(key, value); - } + this._watchObject(false, object); + var index = this._activeObjects.length > 0 ? this._activeObjects.indexOf(object) : -1; + if (index > -1) { + this._activeObjects.splice(index, 1); } - fabric.Object.prototype._set.call(this, key, value); }, /** - * Returns 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 + * @private + * @param {'added'|'removed'} type + * @param {fabric.Object[]} targets */ - toObject: function(propertiesToInclude) { - var _includeDefaultValues = this.includeDefaultValues; - var objsToObject = this._objects - .filter(function (obj) { - return !obj.excludeFromExport; - }) - .map(function (obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = _includeDefaultValues; - var _obj = obj.toObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); - obj.objects = objsToObject; - return obj; + _onAfterObjectsChange: function (type, targets) { + this._applyLayoutStrategy({ + type: type, + targets: targets + }); + this._set('dirty', true); }, /** - * Returns object representation of an instance, in dataless mode. - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance + * @private + * @param {fabric.Object} object */ - toDatalessObject: function(propertiesToInclude) { - var objsToObject, sourcePath = this.sourcePath; - if (sourcePath) { - objsToObject = sourcePath; - } - else { - var _includeDefaultValues = this.includeDefaultValues; - objsToObject = this._objects.map(function(obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = _includeDefaultValues; - var _obj = obj.toDatalessObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - } - var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); - obj.objects = objsToObject; - return obj; + _onObjectAdded: function (object) { + this.enterGroup(object, true); + object.fire('added', { target: this }); }, /** - * Renders instance on a given context - * @param {CanvasRenderingContext2D} ctx context to render instance on + * @private + * @param {fabric.Object} object */ - render: function(ctx) { - this._transformDone = true; - this.callSuper('render', ctx); - this._transformDone = false; + _onRelativeObjectAdded: function (object) { + this.enterGroup(object, false); + object.fire('added', { target: this }); + }, + + /** + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it + */ + _onObjectRemoved: function (object, removeParentTransform) { + this.exitGroup(object, removeParentTransform); + object.fire('removed', { target: this }); }, /** @@ -20317,7 +20464,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot shouldCache: function() { var ownCache = fabric.Object.prototype.shouldCache.call(this); if (ownCache) { - for (var i = 0, len = this._objects.length; i < len; i++) { + for (var i = 0; i < this._objects.length; i++) { if (this._objects[i].willDrawShadow()) { this.ownCaching = false; return false; @@ -20335,7 +20482,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (fabric.Object.prototype.willDrawShadow.call(this)) { return true; } - for (var i = 0, len = this._objects.length; i < len; i++) { + for (var i = 0; i < this._objects.length; i++) { if (this._objects[i].willDrawShadow()) { return true; } @@ -20344,11 +20491,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, /** - * Check if this group or its parent group are caching, recursively up + * Check if instance or its group are caching, recursively up * @return {Boolean} */ - isOnACache: function() { - return this.ownCaching || (this.group && this.group.isOnACache()); + isOnACache: function () { + return this.ownCaching || (!!this.group && this.group.isOnACache()); }, /** @@ -20356,7 +20503,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {CanvasRenderingContext2D} ctx Context to render on */ drawObject: function(ctx) { - for (var i = 0, len = this._objects.length; i < len; i++) { + this._renderBackground(ctx); + for (var i = 0; i < this._objects.length; i++) { this._objects[i].render(ctx); } this._drawClipPath(ctx, this.clipPath); @@ -20372,7 +20520,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (!this.statefullCache) { return false; } - for (var i = 0, len = this._objects.length; i < len; i++) { + for (var i = 0; i < this._objects.length; i++) { if (this._objects[i].isCacheDirty(true)) { if (this._cacheCanvas) { // if this group has not a cache canvas there is nothing to clean @@ -20386,216 +20534,832 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, /** - * Restores original state of each of group objects (original state is that which was before group was created). - * if the nested boolean is true, the original state will be restored just for the - * first group and not for all the group chain - * @private - * @param {Boolean} nested tell the function to restore object state up to the parent group and not more - * @return {fabric.Group} thisArg - * @chainable + * @override + * @return {Boolean} + */ + setCoords: function () { + this.callSuper('setCoords'); + this._shouldSetNestedCoords() && this.forEachObject(function (object) { + object.setCoords(); + }); + }, + + /** + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function (ctx) { + // used to inform objects not to double opacity + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; + }, + + /** + * @public + * @param {Partial & { layout?: string }} [context] pass values to use for layout calculations + */ + triggerLayout: function (context) { + if (context && context.layout) { + context.prevLayout = this.layout; + this.layout = context.layout; + } + this._applyLayoutStrategy({ type: 'imperative', context: context }); + }, + + /** + * @private + * @param {fabric.Object} object + * @param {fabric.Point} diff + * @param {boolean} [setCoords] perf enhancement, instead of iterating over objects again + */ + _adjustObjectPosition: function (object, diff, setCoords) { + // layer doesn't need coords so we don't set them + if (object instanceof fabric.Layer) { + object.forEachObject(function (obj) { + this._adjustObjectPosition(obj, diff, setCoords); + }.bind(this)); + } + else { + object.set({ + left: object.left + diff.x, + top: object.top + diff.y, + }); + setCoords && object.setCoords(); + } + }, + + /** + * initial layout logic: + * calculate bbox of objects (if necessary) and translate it according to options received from the constructor (left, top, width, height) + * so it is placed in the center of the bbox received from the constructor + * + * @private + * @param {LayoutContext} context + */ + _applyLayoutStrategy: function (context) { + var isFirstLayout = context.type === 'initialization'; + if (!isFirstLayout && !this._firstLayoutDone) { + // reject layout requests before initialization layout + return; + } + var options = isFirstLayout && context.options; + var initialTransform = options && { + angle: options.angle || 0, + skewX: options.skewX || 0, + skewY: options.skewY || 0, + }; + var center = this.getRelativeCenterPoint(); + var result = this.getLayoutStrategyResult(this.layout, this._objects.concat(), context); + if (result) { + // handle positioning + var newCenter = new fabric.Point(result.centerX, result.centerY); + var vector = center.subtract(newCenter).add(new fabric.Point(result.correctionX || 0, result.correctionY || 0)); + var diff = transformPoint(vector, invertTransform(this.calcOwnMatrix()), true); + var objectsSetCoords = false; + // set dimensions + this.set({ width: result.width, height: result.height }); + if (!newCenter.eq(center) || initialTransform) { + // set position + this.setPositionByOrigin(newCenter, 'center', 'center'); + initialTransform && this.set(initialTransform); + // perf: avoid iterating over objects twice by setting coords only on instance + // and delegating the task to `_adjustObjectPosition` + this.callSuper('setCoords'); + objectsSetCoords = this.subTargetCheck; + } + // adjust objects to account for new center + !context.objectsRelativeToGroup && this.forEachObject(function (object) { + this._adjustObjectPosition(object, diff, objectsSetCoords); + }, this); + // clip path as well + !isFirstLayout && this.layout !== 'clip-path' && this.clipPath && !this.clipPath.absolutePositioned + && this._adjustObjectPosition(this.clipPath, diff, objectsSetCoords); + } + else if (isFirstLayout) { + // fill `result` with initial values for the layout hook + result = { + centerX: center.x, + centerY: center.y, + width: this.width, + height: this.height, + }; + initialTransform && this.set(initialTransform); + } + else { + // no `result` so we return + return; + } + // flag for next layouts + this._firstLayoutDone = true; + // fire layout hook and event (event will fire only for layouts after initialization layout) + this.onLayout(context, result); + this.fire('layout', { + context: context, + result: result, + diff: diff + }); + this._bubbleLayout(context); + }, + + + /** + * bubble layout recursive up + * @private + */ + _bubbleLayout: function (context) { + if (this.group && this.group._applyLayoutStrategy) { + // append the path recursion to context + if (!context.path) { + context.path = []; + } + context.path.push(this); + // all parents should invalidate their layout + this.group._applyLayoutStrategy(context); + } + }, + + /** + * Override this method to customize layout. + * If you need to run logic once layout completes use `onLayout` + * @public + * + * @typedef {'initialization'|'object_modified'|'added'|'removed'|'layout_change'|'imperative'} LayoutContextType + * + * @typedef LayoutContext context object with data regarding what triggered the call + * @property {LayoutContextType} type + * @property {fabric.Object[]} [path] array of objects starting from the object that triggered the call to the current one + * + * @typedef LayoutResult positioning and layout data **relative** to instance's parent + * @property {number} centerX new centerX as measured by the containing plane (same as `left` with `originX` set to `center`) + * @property {number} centerY new centerY as measured by the containing plane (same as `top` with `originY` set to `center`) + * @property {number} [correctionX] correctionX to translate objects by, measured as `centerX` + * @property {number} [correctionY] correctionY to translate objects by, measured as `centerY` + * @property {number} width + * @property {number} height + * + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {LayoutContext} context + * @returns {LayoutResult | undefined} + */ + getLayoutStrategyResult: function (layoutDirective, objects, context) { // eslint-disable-line no-unused-vars + // `fit-content-lazy` performance enhancement + // skip if instance had no objects before the `added` event because it may have kept layout after removing all previous objects + if (layoutDirective === 'fit-content-lazy' + && context.type === 'added' && objects.length > context.targets.length) { + // calculate added objects' bbox with existing bbox + var addedObjects = context.targets.concat(this); + return this.prepareBoundingBox(layoutDirective, addedObjects, context); + } + else if (layoutDirective === 'fit-content' || layoutDirective === 'fit-content-lazy' + || (layoutDirective === 'fixed' && context.type === 'initialization')) { + return this.prepareBoundingBox(layoutDirective, objects, context); + } + else if (layoutDirective === 'clip-path' && this.clipPath) { + var clipPath = this.clipPath; + var clipPathSizeAfter = clipPath._getTransformedDimensions(); + if (clipPath.absolutePositioned && (context.type === 'initialization' || context.type === 'layout_change')) { + // we want the center point to exist in group's containing plane + var clipPathCenter = clipPath.getCenterPoint(); + if (this.group) { + // send point from canvas plane to group's containing plane + var inv = invertTransform(this.group.calcTransformMatrix()); + clipPathCenter = transformPoint(clipPathCenter, inv); + } + return { + centerX: clipPathCenter.x, + centerY: clipPathCenter.y, + width: clipPathSizeAfter.x, + height: clipPathSizeAfter.y, + }; + } + else if (!clipPath.absolutePositioned) { + var center; + var clipPathRelativeCenter = clipPath.getRelativeCenterPoint(), + // we want the center point to exist in group's containing plane, so we send it upwards + clipPathCenter = transformPoint(clipPathRelativeCenter, this.calcOwnMatrix(), true); + if (context.type === 'initialization' || context.type === 'layout_change') { + var bbox = this.prepareBoundingBox(layoutDirective, objects, context) || {}; + center = new fabric.Point(bbox.centerX || 0, bbox.centerY || 0); + return { + centerX: center.x + clipPathCenter.x, + centerY: center.y + clipPathCenter.y, + correctionX: bbox.correctionX - clipPathCenter.x, + correctionY: bbox.correctionY - clipPathCenter.y, + width: clipPath.width, + height: clipPath.height, + }; + } + else { + center = this.getRelativeCenterPoint(); + return { + centerX: center.x + clipPathCenter.x, + centerY: center.y + clipPathCenter.y, + width: clipPathSizeAfter.x, + height: clipPathSizeAfter.y, + }; + } + } + } + else if (layoutDirective === 'svg' && context.type === 'initialization') { + var bbox = this.getObjectsBoundingBox(objects, true) || {}; + return Object.assign(bbox, { + correctionX: -bbox.offsetX || 0, + correctionY: -bbox.offsetY || 0, + }); + } + }, + + /** + * Override this method to customize layout. + * A wrapper around {@link fabric.Group#getObjectsBoundingBox} + * @public + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {LayoutContext} context + * @returns {LayoutResult | undefined} + */ + prepareBoundingBox: function (layoutDirective, objects, context) { + if (context.type === 'initialization') { + return this.prepareInitialBoundingBox(layoutDirective, objects, context); + } + else if (context.type === 'imperative' && context.context) { + return Object.assign( + this.getObjectsBoundingBox(objects) || {}, + context.context + ); + } + else { + return this.getObjectsBoundingBox(objects); + } + }, + + /** + * Calculates center taking into account originX, originY while not being sure that width/height are initialized + * @public + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {LayoutContext} context + * @returns {LayoutResult | undefined} + */ + prepareInitialBoundingBox: function (layoutDirective, objects, context) { + var options = context.options || {}, + hasX = typeof options.left === 'number', + hasY = typeof options.top === 'number', + hasWidth = typeof options.width === 'number', + hasHeight = typeof options.height === 'number'; + + // performance enhancement + // skip layout calculation if bbox is defined + if ((hasX && hasY && hasWidth && hasHeight && context.objectsRelativeToGroup) || objects.length === 0) { + // return nothing to skip layout + return; + } + + var bbox = this.getObjectsBoundingBox(objects) || {}; + var width = hasWidth ? this.width : (bbox.width || 0), + height = hasHeight ? this.height : (bbox.height || 0), + calculatedCenter = new fabric.Point(bbox.centerX || 0, bbox.centerY || 0), + origin = new fabric.Point(this.resolveOriginX(this.originX), this.resolveOriginY(this.originY)), + size = new fabric.Point(width, height), + strokeWidthVector = this._getTransformedDimensions({ width: 0, height: 0 }), + sizeAfter = this._getTransformedDimensions({ + width: width, + height: height, + strokeWidth: 0 + }), + bboxSizeAfter = this._getTransformedDimensions({ + width: bbox.width, + height: bbox.height, + strokeWidth: 0 + }), + rotationCorrection = new fabric.Point(0, 0); + + if (this.angle) { + var rad = degreesToRadians(this.angle), + sin = Math.abs(fabric.util.sin(rad)), + cos = Math.abs(fabric.util.cos(rad)); + sizeAfter.setXY( + sizeAfter.x * cos + sizeAfter.y * sin, + sizeAfter.x * sin + sizeAfter.y * cos + ); + bboxSizeAfter.setXY( + bboxSizeAfter.x * cos + bboxSizeAfter.y * sin, + bboxSizeAfter.x * sin + bboxSizeAfter.y * cos + ); + strokeWidthVector = fabric.util.rotateVector(strokeWidthVector, rad); + // correct center after rotating + var strokeCorrection = strokeWidthVector.multiply(origin.scalarAdd(-0.5).scalarDivide(-2)); + rotationCorrection = sizeAfter.subtract(size).scalarDivide(2).add(strokeCorrection); + calculatedCenter.addEquals(rotationCorrection); + } + // calculate center and correction + var originT = origin.scalarAdd(0.5); + var originCorrection = sizeAfter.multiply(originT); + var centerCorrection = new fabric.Point( + hasWidth ? bboxSizeAfter.x / 2 : originCorrection.x, + hasHeight ? bboxSizeAfter.y / 2 : originCorrection.y + ); + var center = new fabric.Point( + hasX ? this.left - (sizeAfter.x + strokeWidthVector.x) * origin.x : calculatedCenter.x - centerCorrection.x, + hasY ? this.top - (sizeAfter.y + strokeWidthVector.y) * origin.y : calculatedCenter.y - centerCorrection.y + ); + var offsetCorrection = new fabric.Point( + hasX ? + center.x - calculatedCenter.x + bboxSizeAfter.x * (hasWidth ? 0.5 : 0) : + -(hasWidth ? (sizeAfter.x - strokeWidthVector.x) * 0.5 : sizeAfter.x * originT.x), + hasY ? + center.y - calculatedCenter.y + bboxSizeAfter.y * (hasHeight ? 0.5 : 0) : + -(hasHeight ? (sizeAfter.y - strokeWidthVector.y) * 0.5 : sizeAfter.y * originT.y) + ).add(rotationCorrection); + var correction = new fabric.Point( + hasWidth ? -sizeAfter.x / 2 : 0, + hasHeight ? -sizeAfter.y / 2 : 0 + ).add(offsetCorrection); + + return { + centerX: center.x, + centerY: center.y, + correctionX: correction.x, + correctionY: correction.y, + width: size.x, + height: size.y, + }; + }, + + /** + * Calculate the bbox of objects relative to instance's containing plane + * @public + * @param {fabric.Object[]} objects + * @returns {LayoutResult | null} bounding box + */ + getObjectsBoundingBox: function (objects, ignoreOffset) { + if (objects.length === 0) { + return null; + } + var objCenter, sizeVector, min = new fabric.Point(0, 0), max = new fabric.Point(0, 0), a, b; + objects.forEach(function (object, i) { + if (object instanceof fabric.Layer) { + var bbox = object.getObjectsBoundingBox(object._objects.slice(0)); + if (!bbox) { + return; + } + sizeVector = object._getTransformedDimensions({ + width: bbox.width, + height: bbox.height + }).scalarDivideEquals(2); + objCenter = new fabric.Point(bbox.centerX, bbox.centerY); + } + else { + sizeVector = object._getTransformedDimensions().scalarDivideEquals(2); + objCenter = object.getRelativeCenterPoint(); + } + if (object.angle) { + var rad = degreesToRadians(object.angle), + sin = Math.abs(fabric.util.sin(rad)), + cos = Math.abs(fabric.util.cos(rad)), + rx = sizeVector.x * cos + sizeVector.y * sin, + ry = sizeVector.x * sin + sizeVector.y * cos; + sizeVector = new fabric.Point(rx, ry); + } + a = objCenter.subtract(sizeVector); + b = objCenter.add(sizeVector); + if (i === 0) { + min = new fabric.Point(Math.min(a.x, b.x), Math.min(a.y, b.y)); + max = new fabric.Point(Math.max(a.x, b.x), Math.max(a.y, b.y)); + } + else { + min.setXY(Math.min(min.x, a.x, b.x), Math.min(min.y, a.y, b.y)); + max.setXY(Math.max(max.x, a.x, b.x), Math.max(max.y, a.y, b.y)); + } + }); + + var size = max.subtract(min), + relativeCenter = ignoreOffset ? size.scalarDivide(2) : min.midPointFrom(max), + // we send `relativeCenter` up to group's containing plane + offset = transformPoint(min, this.calcOwnMatrix()), + center = transformPoint(relativeCenter, this.calcOwnMatrix()); + + return { + offsetX: offset.x, + offsetY: offset.y, + centerX: center.x, + centerY: center.y, + width: size.x, + height: size.y, + }; + }, + + /** + * Hook that is called once layout has completed. + * Provided for layout customization, override if necessary. + * Complements `getLayoutStrategyResult`, which is called at the beginning of layout. + * @public + * @param {LayoutContext} context layout context + * @param {LayoutResult} result layout result + */ + onLayout: function (/* context, result */) { + // override by subclass + }, + + /** + * + * @private + * @param {'toObject'|'toDatalessObject'} [method] + * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @returns {fabric.Object[]} serialized objects + */ + __serializeObjects: function (method, propertiesToInclude) { + var _includeDefaultValues = this.includeDefaultValues; + return this._objects + .filter(function (obj) { + return !obj.excludeFromExport; + }) + .map(function (obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var data = obj[method || 'toObject'](propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + //delete data.version; + return data; + }); + }, + + /** + * Returns object representation of an instance + * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function (propertiesToInclude) { + var obj = this.callSuper('toObject', ['layout', 'subTargetCheck', 'interactive'].concat(propertiesToInclude)); + obj.objects = this.__serializeObjects('toObject', propertiesToInclude); + return obj; + }, + + toString: function () { + return '#'; + }, + + dispose: function () { + this._activeObjects = []; + this.forEachObject(function (object) { + this._watchObject(false, object); + object.dispose && object.dispose(); + }, this); + }, + + /* _TO_SVG_START_ */ + + /** + * @private + */ + _createSVGBgRect: function (reviver) { + if (!this.backgroundColor) { + return ''; + } + var fillStroke = fabric.Rect.prototype._toSVG.call(this, reviver); + var commons = fillStroke.indexOf('COMMON_PARTS'); + fillStroke[commons] = 'for="group" '; + return fillStroke.join(''); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function (reviver) { + var svgString = ['\n']; + var bg = this._createSVGBgRect(reviver); + bg && svgString.push('\t\t', bg); + for (var i = 0; i < this._objects.length; i++) { + svgString.push('\t\t', this._objects[i].toSVG(reviver)); + } + svgString.push('\n'); + return svgString; + }, + + /** + * Returns styles-string for svg-export, specific version for group + * @return {String} + */ + getSvgStyles: function() { + var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? + 'opacity: ' + this.opacity + ';' : '', + visibility = this.visible ? '' : ' visibility: hidden;'; + return [ + opacity, + this.getSvgFilter(), + visibility + ].join(''); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function (reviver) { + var svgString = []; + var bg = this._createSVGBgRect(reviver); + bg && svgString.push('\t', bg); + for (var i = 0; i < this._objects.length; i++) { + svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); + } + return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); + }, + /* _TO_SVG_END_ */ + }); + + /** + * @todo support loading from svg + * @private + * @static + * @memberOf fabric.Group + * @param {Object} object Object to create a group from + * @returns {Promise} + */ + fabric.Group.fromObject = function(object) { + var objects = object.objects || [], + options = clone(object, true); + delete options.objects; + return Promise.all([ + fabric.util.enlivenObjects(objects), + fabric.util.enlivenObjectEnlivables(options) + ]).then(function (enlivened) { + return new fabric.Group(enlivened[0], Object.assign(options, enlivened[1]), true); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function (global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = {}); + + if (fabric.Layer) { + fabric.warn('fabric.Layer is already defined'); + return; + } + + /** + * Layer class + * @class fabric.Layer + * @extends fabric.Group + * @see {@link fabric.Layer#initialize} for constructor definition + */ + fabric.Layer = fabric.util.createClass(fabric.Group, /** @lends fabric.Layer.prototype */ { + + /** + * @default + * @type string + */ + type: 'layer', + + /** + * @override + * @default + */ + layout: 'auto', + + /** + * @override + * @default + */ + objectCaching: false, + + /** + * @override + * @default + */ + strokeWidth: 0, + + /** + * @override + * @default + */ + hasControls: false, + + /** + * @override + * @default + */ + hasBorders: false, + + /** + * @override + * @default + */ + lockMovementX: true, + + /** + * @override + * @default + */ + lockMovementY: true, + + /** + * @default + * @override + */ + originX: 'center', + + /** + * @default + * @override + */ + originY: 'center', + + /** + * we don't want to int with the layer, only with it's objects + * this makes group selection possible over a layer + * @override + * @default */ - _restoreObjectsState: function() { - var groupMatrix = this.calcOwnMatrix(); - this._objects.forEach(function(object) { - // instead of using _this = this; - fabric.util.addTransformToObject(object, groupMatrix); - delete object.group; - object.setCoords(); - }); - return this; - }, + selectable: false, /** - * Destroys a group (restoring state of its objects) + * Constructor + * + * @param {fabric.Object[]} [objects] instance objects + * @param {Object} [options] Options object * @return {fabric.Group} thisArg - * @chainable */ - destroy: function() { - // when group is destroyed objects needs to get a repaint to be eventually - // displayed on canvas. - this._objects.forEach(function(object) { - object.set('dirty', true); - }); - return this._restoreObjectsState(); + initialize: function (objects, options) { + this.callSuper('initialize', objects, options); + this.__canvasMonitor = this.__canvasMonitor.bind(this); + this.__groupMonitor = this.__groupMonitor.bind(this); + this.__onAdded = this._watchParent.bind(this, true); + this.__onRemoved = this._watchParent.bind(this, false); + this.on('added:initialized', this.__onAdded); + this.on('added', this.__onAdded); + this.on('removed', this.__onRemoved); + // trigger layout in case parent is passed in options + var parent = this.group || this.canvas; + parent && this.__onAdded({ target: parent }); }, - dispose: function () { - this.callSuper('dispose'); - this.forEachObject(function (object) { - object.dispose && object.dispose(); - }); - this._objects = []; + /** + * @override we want instance to fill parent so we disregard transformations + * @param {CanvasRenderingContext2D} ctx Context + */ + transform: function (ctx) { + var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); + ctx.transform(1, 0, 0, 1, m[4], m[5]); }, /** - * make a group an active selection, remove the group from canvas - * the group has to be on canvas for this to work. - * @return {fabric.ActiveSelection} thisArg - * @chainable + * @override apply instance's transformations on objects + * @param {CanvasRenderingContext2D} ctx Context to render on */ - toActiveSelection: function() { - if (!this.canvas) { - return; + drawObject: function (ctx) { + this._renderBackground(ctx); + ctx.save(); + var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); + ctx.transform(m[0], m[1], m[2], m[3], 0, 0); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].render(ctx); } - var objects = this._objects, canvas = this.canvas; - this._objects = []; - var options = this.toObject(); - delete options.objects; - var activeSelection = new fabric.ActiveSelection([]); - activeSelection.set(options); - activeSelection.type = 'activeSelection'; - canvas.remove(this); - objects.forEach(function(object) { - object.group = activeSelection; - object.dirty = true; - canvas.add(object); - }); - activeSelection.canvas = canvas; - activeSelection._objects = objects; - canvas._activeObject = activeSelection; - activeSelection.setCoords(); - return activeSelection; + ctx.restore(); + this._drawClipPath(ctx, this.clipPath); }, /** - * Destroys a group (restoring state of its objects) - * @return {fabric.Group} thisArg - * @chainable + * @private + * @override we want instance to fill parent so we disregard transformations + * @returns {fabric.Point} dimensions */ - ungroupOnCanvas: function() { - return this._restoreObjectsState(); + _getTransformedDimensions: function () { + return this.callSuper('_getTransformedDimensions', { + scaleX: 1, + scaleY: 1, + skewX: 0, + skewY: 0, + width: this.width, + height: this.height, + strokeWidth: 0 + }); }, /** - * Sets coordinates of all objects inside group - * @return {fabric.Group} thisArg - * @chainable + * we need to invalidate instance's group if objects have changed + * @override + * @private */ - setObjectsCoords: function() { - var skipControls = true; - this.forEachObject(function(object) { - object.setCoords(skipControls); - }); - return this; + __objectMonitor: function (opt) { + this.group && this.group.__objectMonitor(opt); }, /** * @private + * @param {boolean} watch + * @param {{target:fabric.Group|fabric.Canvas}} [opt] */ - _calcBounds: function(onlyWidthHeight) { - var aX = [], - aY = [], - o, prop, coords, - props = ['tr', 'br', 'bl', 'tl'], - i = 0, iLen = this._objects.length, - j, jLen = props.length; - - for ( ; i < iLen; ++i) { - o = this._objects[i]; - coords = o.calcACoords(); - for (j = 0; j < jLen; j++) { - prop = props[j]; - aX.push(coords[prop].x); - aY.push(coords[prop].y); - } - o.aCoords = coords; + _watchParent: function (watch, opt) { + var target = opt && opt.target; + // make sure we listen only once + this.canvas && this.canvas.off('resize', this.__canvasMonitor); + this.group && this.group.off('layout', this.__groupMonitor); + if (!watch) { + return; + } + else if (target instanceof fabric.Group) { + this._applyLayoutStrategy({ type: 'group' }); + this.group.on('layout', this.__groupMonitor); + } + else if (target instanceof fabric.StaticCanvas) { + this._applyLayoutStrategy({ type: 'canvas' }); + this.canvas.on('resize', this.__canvasMonitor); } - - this._getBounds(aX, aY, onlyWidthHeight); }, /** * @private */ - _getBounds: function(aX, aY, onlyWidthHeight) { - var minXY = new fabric.Point(min(aX), min(aY)), - maxXY = new fabric.Point(max(aX), max(aY)), - top = minXY.y || 0, left = minXY.x || 0, - width = (maxXY.x - minXY.x) || 0, - height = (maxXY.y - minXY.y) || 0; - this.width = width; - this.height = height; - if (!onlyWidthHeight) { - // the bounding box always finds the topleft most corner. - // whatever is the group origin, we set up here the left/top position. - this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); - } + __canvasMonitor: function () { + this._applyLayoutStrategy({ type: 'canvas_resize' }); }, - /* _TO_SVG_START_ */ /** - * Returns svg representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance + * @private */ - _toSVG: function(reviver) { - var svgString = ['\n']; - - for (var i = 0, len = this._objects.length; i < len; i++) { - svgString.push('\t\t', this._objects[i].toSVG(reviver)); - } - svgString.push('\n'); - return svgString; + __groupMonitor: function (context) { + this._applyLayoutStrategy(Object.assign({}, context, { type: 'group_layout' })); }, /** - * Returns styles-string for svg-export, specific version for group - * @return {String} + * @private + * @override we do not want to bubble layout */ - getSvgStyles: function() { - var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? - 'opacity: ' + this.opacity + ';' : '', - visibility = this.visible ? '' : ' visibility: hidden;'; - return [ - opacity, - this.getSvgFilter(), - visibility - ].join(''); + _bubbleLayout: function () { + // noop }, /** - * Returns svg clipPath representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance + * Layer will layout itself once it is added to a canvas/group and by listening to it's parent `resize`/`layout` events respectively + * Override this method to customize layout + * @public + * @param {string} layoutDirective + * @param {fabric.Object[]} objects + * @param {object} context object with data regarding what triggered the call + * @param {'initializion'|'canvas'|'canvas_resize'|'layout_change'} context.type + * @param {fabric.Object[]} context.path array of objects starting from the object that triggered the call to the current one + * @returns {Object} options object */ - toClipPathSVG: function(reviver) { - var svgString = []; - - for (var i = 0, len = this._objects.length; i < len; i++) { - svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); + getLayoutStrategyResult: function (layoutDirective, objects, context) { // eslint-disable-line no-unused-vars + if ((context.type === 'canvas' || context.type === 'canvas_resize') && this.canvas && !this.group) { + return { + centerX: this.canvas.width / 2, + centerY: this.canvas.height / 2, + width: this.canvas.width, + height: this.canvas.height + }; + } + else if ((context.type === 'group' || context.type === 'group_layout') && this.group) { + var w = this.group.width, h = this.group.height; + return { + centerX: 0, + centerY: 0, + width: w, + height: h + }; } + }, - return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); + toString: function () { + return '#'; }, - /* _TO_SVG_END_ */ + + dispose: function () { + this.off('added:initialized', this.__onAdded); + this.off('added', this.__onAdded); + this.off('removed', this.__onRemoved); + this._watchParent(false); + this.callSuper('dispose'); + } + }); /** - * Returns {@link fabric.Group} instance from an object representation + * Returns fabric.Layer instance from an object representation * @static - * @memberOf fabric.Group - * @param {Object} object Object to create a group from - * @param {Function} [callback] Callback to invoke when an group instance is created + * @memberOf fabric.Layer + * @param {Object} object Object to create an instance from + * @returns {Promise} */ - fabric.Group.fromObject = function(object, callback) { - var objects = object.objects, + fabric.Layer.fromObject = function (object) { + var objects = object.objects || [], options = fabric.util.object.clone(object, true); delete options.objects; - if (typeof objects === 'string') { - // it has to be an url or something went wrong. - fabric.loadSVGFromURL(objects, function (elements) { - var group = fabric.util.groupSVGElements(elements, object, objects); - group.set(options); - callback && callback(group); - }); - return; - } - 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.Group(enlivenedObjects, options, true)); - }); + return Promise.all([ + fabric.util.enlivenObjects(objects), + fabric.util.enlivenObjectEnlivables(options) + ]).then(function (enlivened) { + return new fabric.Layer(enlivened[0], Object.assign(options, enlivened[1]), true); }); }; @@ -20628,58 +21392,99 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ type: 'activeSelection', + /** + * @override + */ + layout: 'fit-content', + + /** + * @override + */ + subTargetCheck: false, + + /** + * @override + */ + interactive: false, + /** * Constructor - * @param {Object} objects ActiveSelection objects + * + * @param {fabric.Object[]} [objects] instance objects * @param {Object} [options] Options object - * @return {Object} thisArg + * @param {boolean} [objectsRelativeToGroup] true if objects exist in group coordinate plane + * @return {fabric.ActiveSelection} thisArg */ - initialize: function(objects, options) { - options = options || {}; - this._objects = objects || []; - for (var i = this._objects.length; i--; ) { - this._objects[i].group = this; - } + initialize: function (objects, options, objectsRelativeToGroup) { + this.callSuper('initialize', objects, options, objectsRelativeToGroup); + this.setCoords(); + }, - if (options.originX) { - this.originX = options.originX; + /** + * @private + */ + _shouldSetNestedCoords: function () { + return true; + }, + + /** + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane + * @returns {boolean} true if object entered group + */ + enterGroup: function (object, removeParentTransform) { + if (!this.canEnter(object)) { + return false; } - if (options.originY) { - this.originY = options.originY; + if (object.group) { + // save ref to group for later in order to return to it + var parent = object.group; + parent._exitGroup(object); + object.__owningGroup = parent; } - this._calcBounds(); - this._updateObjectsCoords(); - fabric.Object.prototype.initialize.call(this, options); - this.setCoords(); + this._enterGroup(object, removeParentTransform); + return true; }, /** - * Change te activeSelection to a normal group, - * High level function that automatically adds it to canvas as - * active object. no events fired. - * @since 2.0.0 - * @return {fabric.Group} + * we want objects to retain their canvas ref when exiting instance + * @private + * @param {fabric.Object} object + * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it */ - toGroup: function() { - var objects = this._objects.concat(); - this._objects = []; - var options = fabric.Object.prototype.toObject.call(this); - var newGroup = new fabric.Group([]); - delete options.type; - newGroup.set(options); - objects.forEach(function(object) { - object.canvas.remove(object); - object.group = newGroup; + exitGroup: function (object, removeParentTransform) { + this._exitGroup(object, removeParentTransform); + var parent = object.__owningGroup; + if (parent) { + // return to owning group + parent.enterGroup(object); + delete object.__owningGroup; + } + }, + + /** + * @private + * @param {'added'|'removed'} type + * @param {fabric.Object[]} targets + */ + _onAfterObjectsChange: function (type, targets) { + var groups = []; + targets.forEach(function (object) { + object.group && !groups.includes(object.group) && groups.push(object.group); }); - newGroup._objects = objects; - if (!this.canvas) { - return newGroup; + if (type === 'removed') { + // invalidate groups' layout and mark as dirty + groups.forEach(function (group) { + group._onAfterObjectsChange('added', targets); + }); + } + else { + // mark groups as dirty + groups.forEach(function (group) { + group._set('dirty', true); + }); } - var canvas = this.canvas; - canvas.add(newGroup); - canvas._activeObject = newGroup; - newGroup.setCoords(); - return newGroup; }, /** @@ -20688,7 +21493,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Boolean} [cancel] */ onDeselect: function() { - this.destroy(); + this.removeAll(); return false; }, @@ -20735,7 +21540,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot childrenOverride.hasControls = false; } childrenOverride.forActiveSelection = true; - for (var i = 0, len = this._objects.length; i < len; i++) { + for (var i = 0; i < this._objects.length; i++) { this._objects[i]._renderControls(ctx, childrenOverride); } ctx.restore(); @@ -20747,12 +21552,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.ActiveSelection * @param {Object} object Object to create a group from - * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created + * @returns {Promise} */ - fabric.ActiveSelection.fromObject = function(object, callback) { - fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { - delete object.objects; - callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); + fabric.ActiveSelection.fromObject = function(object) { + var objects = object.objects, + options = fabric.util.object.clone(object, true); + delete options.objects; + return fabric.util.enlivenObjects(objects).then(function(enlivenedObjects) { + return new fabric.ActiveSelection(enlivenedObjects, object, true); }); }; @@ -20903,7 +21710,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Please check video element events for seeking. * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element * @param {Object} [options] Options object - * @param {function} [callback] callback function to call after eventual filters applied. * @return {fabric.Image} thisArg */ initialize: function(element, options) { @@ -21131,20 +21937,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Sets source of an image * @param {String} src Source string (URL) - * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) * @param {Object} [options] Options object * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @return {fabric.Image} thisArg - * @chainable + * @return {Promise} thisArg */ - setSrc: function(src, callback, options) { - fabric.util.loadImage(src, function(img, isError) { - this.setElement(img, options); - this._setWidthHeight(); - callback && callback(this, isError); - }, this, options && options.crossOrigin); - return this; + setSrc: function(src, options) { + var _this = this; + return fabric.util.loadImage(src, options).then(function(img) { + _this.setElement(img, options); + _this._setWidthHeight(); + return _this; + }); }, /** @@ -21159,8 +21963,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot var filter = this.resizeFilter, minimumScale = this.minimumScaleTrigger, objectScale = this.getTotalObjectScaling(), - scaleX = objectScale.scaleX, - scaleY = objectScale.scaleY, + scaleX = objectScale.x, + scaleY = objectScale.y, elementToFilter = this._filteredEl || this._originalElement; if (this.group) { this.set('dirty', true); @@ -21316,7 +22120,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _needsResize: function() { var scale = this.getTotalObjectScaling(); - return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); + return (scale.x !== this._lastScaleX || scale.y !== this._lastScaleY); }, /** @@ -21348,22 +22152,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._setWidthHeight(options); }, - /** - * @private - * @param {Array} filters to be initialized - * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created - */ - _initFilters: function(filters, callback) { - if (filters && filters.length) { - fabric.util.enlivenObjects(filters, function(enlivenedObjects) { - callback && callback(enlivenedObjects); - }, 'fabric.Image.filters'); - } - else { - callback && callback(); - } - }, - /** * @private * Set the width and the height of the image object, using the element or the @@ -21461,39 +22249,39 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Creates an instance of fabric.Image from its object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} callback Callback to invoke when an image instance is created - */ - fabric.Image.fromObject = function(_object, callback) { - var object = fabric.util.object.clone(_object); - fabric.util.loadImage(object.src, function(img, isError) { - if (isError) { - callback && callback(null, true); - return; - } - fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { - object.filters = filters || []; - fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { - object.resizeFilter = resizeFilters[0]; - fabric.util.enlivenObjectEnlivables(object, object, function () { - var image = new fabric.Image(img, object); - callback(image, false); - }); - }); + * @returns {Promise} + */ + fabric.Image.fromObject = function(_object) { + var object = fabric.util.object.clone(_object), + filters = object.filters, + resizeFilter = object.resizeFilter; + // the generic enliving will fail on filters for now + delete object.resizeFilter; + delete object.filters; + return Promise.all([ + fabric.util.loadImage(object.src, { crossOrigin: _object.crossOrigin }), + filters && fabric.util.enlivenObjects(filters, 'fabric.Image.filters'), + resizeFilter && fabric.util.enlivenObjects([resizeFilter], 'fabric.Image.filters'), + fabric.util.enlivenObjectEnlivables(object), + ]) + .then(function(imgAndFilters) { + object.filters = imgAndFilters[1] || []; + object.resizeFilter = imgAndFilters[2] && imgAndFilters[2][0]; + return new fabric.Image(imgAndFilters[0], Object.assign(object, imgAndFilters[3])); }); - }, null, object.crossOrigin); }; /** * Creates an instance of fabric.Image from an URL string * @static * @param {String} url URL to create an image from - * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. * @param {Object} [imgOptions] Options object + * @returns {Promise} */ - fabric.Image.fromURL = function(url, callback, imgOptions) { - fabric.util.loadImage(url, function(img, isError) { - callback && callback(new fabric.Image(img, imgOptions), isError); - }, null, imgOptions && imgOptions.crossOrigin); + fabric.Image.fromURL = function(url, imgOptions) { + return fabric.util.loadImage(url, imgOptions || {}).then(function(img) { + return new fabric.Image(img, imgOptions); + }); }; /* _FROM_SVG_START_ */ @@ -21517,8 +22305,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ fabric.Image.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); - fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, - extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + fabric.Image.fromURL(parsedAttributes['xlink:href'], Object.assign({ }, options || { }, parsedAttributes)) + .then(function(fabricImage) { + callback(fabricImage); + }); }; /* _FROM_SVG_END_ */ @@ -22428,10 +23218,14 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag } }); -fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { - var filter = new fabric.Image.filters[object.type](object); - callback && callback(filter); - return filter; +/** + * Create filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @returns {Promise} + */ +fabric.Image.filters.BaseFilter.fromObject = function(object) { + return Promise.resolve(new fabric.Image.filters[object.type](object)); }; @@ -22586,11 +23380,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] function to invoke after filter creation - * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix + * @returns {Promise} */ fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; })(typeof exports !== 'undefined' ? exports : this); @@ -22700,11 +23493,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + * @returns {Promise} */ fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23054,11 +23846,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + * @returns {Promise} */ fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23210,11 +24001,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + * @returns {Promise} */ fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23322,11 +24112,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + * @returns {Promise} */ fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23459,11 +24248,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + * @returns {Promise} */ fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23598,11 +24386,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + * @returns {Promise} */ fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23773,11 +24560,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite + * @returns {Promise} */ fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24113,11 +24899,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor + * @returns {Promise} */ fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24356,17 +25141,16 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} callback to be invoked after filter creation - * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage + * @returns {Promise} */ - fabric.Image.filters.BlendImage.fromObject = function(object, callback) { - fabric.Image.fromObject(object.image, function(image) { + fabric.Image.filters.BlendImage.fromObject = function(object) { + return fabric.Image.fromObject(object.image).then(function(image) { var options = fabric.util.object.clone(object); options.image = image; - callback(new fabric.Image.filters.BlendImage(options)); + return new fabric.Image.filters.BlendImage(options); }); }; @@ -24854,11 +25638,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize + * @returns {Promise} */ fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24969,11 +25752,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast + * @returns {Promise} */ fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25029,7 +25811,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * Saturation value, from -1 to 1. * Increases/decreases the color saturation. * A value of 0 has no effect. - * + * * @param {Number} saturation * @default */ @@ -25090,11 +25872,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate + * @returns {Promise} */ fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25151,7 +25932,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * Vibrance value, from -1 to 1. * Increases/decreases the saturation of more muted colors with less effect on saturated colors. * A value of 0 has no effect. - * + * * @param {Number} vibrance * @default */ @@ -25214,11 +25995,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance + * @returns {Promise} */ fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25437,7 +26217,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Deserialize a JSON definition of a BlurFilter into a concrete instance. + * Create filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @returns {Promise} */ filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25571,11 +26354,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma + * @returns {Promise} */ fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25644,14 +26426,13 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Deserialize a JSON definition of a ComposedFilter into a concrete instance. */ - fabric.Image.filters.Composed.fromObject = function(object, callback) { - var filters = object.subFilters || [], - subFilters = filters.map(function(filter) { - return new fabric.Image.filters[filter.type](filter); - }), - instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); - callback && callback(instance); - return instance; + fabric.Image.filters.Composed.fromObject = function(object) { + var filters = object.subFilters || []; + return Promise.all(filters.map(function(filter) { + return fabric.Image.filters[filter.type].fromObject(filter); + })).then(function(enlivedFilters) { + return new fabric.Image.filters.Composed({ subFilters: enlivedFilters }); + }); }; })(typeof exports !== 'undefined' ? exports : this); @@ -25754,11 +26535,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }); /** - * Returns filter instance from an object representation + * Create filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation + * @returns {Promise} */ fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -26649,11 +27429,20 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Measure and return the info of a single grapheme. * needs the the info of previous graphemes already filled - * @private + * Override to customize measuring + * + * @typedef {object} GraphemeBBox + * @property {number} width + * @property {number} height + * @property {number} kernedWidth + * @property {number} left + * @property {number} deltaY + * * @param {String} grapheme to be measured * @param {Number} lineIndex index of the line where the char is * @param {Number} charIndex position in the line * @param {String} [prevGrapheme] character preceding the one to be measured + * @returns {GraphemeBBox} grapheme bbox */ _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), @@ -26811,7 +27600,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); + // this was changed in the PR #7674 + // currentDirection = ctx.canvas.getAttribute('dir'); + drawingLeft, currentDirection = ctx.direction; ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); @@ -27073,7 +27864,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { leftOffset = lineDiff; } if (direction === 'rtl') { - leftOffset -= lineDiff; + if (textAlign === 'right' || textAlign === 'justify' || textAlign === 'justify-right') { + leftOffset = 0; + } + else if (textAlign === 'left' || textAlign === 'justify-left') { + leftOffset = -lineDiff; + } + else if (textAlign === 'center' || textAlign === 'justify-center') { + leftOffset = -lineDiff / 2; + } } return leftOffset; }, @@ -27279,6 +28078,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { this.callSuper('render', ctx); }, + /** + * Override this method to customize grapheme splitting + * @param {string} value + * @returns {string[]} array of graphemes + */ + graphemeSplit: function (value) { + return fabric.util.string.graphemeSplit(value); + }, + /** * Returns the text as an array of lines. * @param {String} text text to split @@ -27290,7 +28098,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { newLine = ['\n'], newText = []; for (var i = 0; i < lines.length; i++) { - newLines[i] = fabric.util.string.graphemeSplit(lines[i]); + newLines[i] = this.graphemeSplit(lines[i]); newText = newText.concat(newLines[i], newLine); } newText.pop(); @@ -27466,22 +28274,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @memberOf fabric.Text * @param {Object} object plain js Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created + * @returns {Promise} */ - fabric.Text.fromObject = function(object, callback) { - var objectCopy = clone(object), path = object.path; - delete objectCopy.path; - return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { - if (path) { - fabric.Object._fromObject('Path', path, function(pathInstance) { - textInstance.set('path', pathInstance); - callback(textInstance); - }, 'path'); - } - else { - callback(textInstance); - } - }, 'text'); + fabric.Text.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Text, object, 'text'); }; fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; @@ -27818,16 +28614,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { (function() { - - function parseDecoration(object) { - if (object.textDecoration) { - object.textDecoration.indexOf('underline') > -1 && (object.underline = true); - object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); - object.textDecoration.indexOf('overline') > -1 && (object.overline = true); - delete object.textDecoration; - } - } - /** * IText class (introduced in v1.4) Events are also fired with "text:" * prefix when observing canvas. @@ -28015,6 +28801,21 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { this.initBehavior(); }, + /** + * While editing handle differently + * @private + * @param {string} key + * @param {*} value + */ + _set: function (key, value) { + if (this.isEditing && this._savedProps && key in this._savedProps) { + this._savedProps[key] = value; + } + else { + this.callSuper('_set', key, value); + } + }, + /** * Sets selection start (left boundary of a selection) * @param {Number} index Index to set selection start to @@ -28185,7 +28986,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - boundaries.left *= -1; + if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { + boundaries.left *= -1; + } + else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + } + else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { + boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); + } } this.cursorOffsetCache = boundaries; return this.cursorOffsetCache; @@ -28274,7 +29083,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - drawStart = this.width - drawStart - drawWidth; + if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { + drawStart = this.width - drawStart - drawWidth; + } + else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { + drawStart = boundaries.left + lineOffset - boxEnd; + } + else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { + drawStart = boundaries.left + lineOffset - boxEnd; + } } ctx.fillRect( drawStart, @@ -28326,18 +29143,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @memberOf fabric.IText * @param {Object} object Object to create an instance from - * @param {function} [callback] invoked with new instance as argument + * @returns {Promise} */ - fabric.IText.fromObject = function(object, callback) { - parseDecoration(object); - if (object.styles) { - for (var i in object.styles) { - for (var j in object.styles[i]) { - parseDecoration(object.styles[i][j]); - } - } - } - fabric.Object._fromObject('IText', object, callback, 'text'); + fabric.IText.fromObject = function(object) { + return fabric.Object._fromObject(fabric.IText, object, 'text'); }; })(); @@ -28369,8 +29178,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ initAddedHandler: function() { var _this = this; - this.on('added', function() { - var canvas = _this.canvas; + this.on('added', function (opt) { + // make sure we listen to the canvas added event + var canvas = opt.target; if (canvas) { if (!canvas._hasITextHandlers) { canvas._hasITextHandlers = true; @@ -28384,8 +29194,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { initRemovedHandler: function() { var _this = this; - this.on('removed', function() { - var canvas = _this.canvas; + this.on('removed', function (opt) { + // make sure we listen to the canvas removed event + var canvas = opt.target; if (canvas) { canvas._iTextInstances = canvas._iTextInstances || []; fabric.util.removeFromArray(canvas._iTextInstances, _this); @@ -28485,9 +29296,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { this.abortCursorAnimation(); this._currentCursorOpacity = 1; - this._cursorTimeout2 = setTimeout(function() { - _this._tick(); - }, delay); + if (delay) { + this._cursorTimeout2 = setTimeout(function () { + _this._tick(); + }, delay); + } + else { + this._tick(); + } }, /** @@ -28776,12 +29592,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ fromStringToGraphemeSelection: function(start, end, text) { var smallerTextStart = text.slice(0, start), - graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length; + graphemeStart = this.graphemeSplit(smallerTextStart).length; if (start === end) { return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; } var smallerTextEnd = text.slice(start, end), - graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length; + graphemeEnd = this.graphemeSplit(smallerTextEnd).length; return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; }, @@ -28936,6 +29752,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { this.canvas.defaultCursor = this._savedProps.defaultCursor; this.canvas.moveCursor = this._savedProps.moveCursor; } + + delete this._savedProps; }, /** @@ -29435,7 +30253,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ mouseUpHandler: function(options) { this.__isMousedown = false; - if (!this.editable || this.group || + if (!this.editable || + (this.group && !this.group.interactive) || (options.transform && options.transform.actionPerformed) || (options.e.button && options.e.button !== 1)) { return; @@ -29513,7 +30332,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot break; } } - lineLeftOffset = this._getLineLeftOffset(lineIndex); + lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); width = lineLeftOffset * this.scaleX; line = this._textLines[lineIndex]; // handling of RTL: in order to get things work correctly, @@ -29521,7 +30340,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // so in position detection we mirror the X offset, and when is time // of rendering it, we mirror it again. if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; + mouseOffset.x = this.width * this.scaleX - mouseOffset.x; } for (var j = 0, jlen = line.length; j < jlen; j++) { prevWidth = width; @@ -29579,7 +30398,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + - ' paddingーtop: ' + style.fontSize + ';'; + ' padding-top: ' + style.fontSize + ';'; if (this.hiddenTextareaContainer) { this.hiddenTextareaContainer.appendChild(this.hiddenTextarea); @@ -29588,6 +30407,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot fabric.document.body.appendChild(this.hiddenTextarea); } + fabric.util.addListener(this.hiddenTextarea, 'blur', this.blur.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); @@ -29661,6 +30481,13 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this.hiddenTextarea && this.hiddenTextarea.focus(); }, + /** + * Override this method to customize cursor behavior on textbox blur + */ + blur: function () { + this.abortCursorAnimation(); + }, + /** * Handles keydown event * only used for arrows and combination of modifier keys. @@ -30242,7 +31069,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (end > start) { this.removeStyleFromTo(start, end); } - var graphemes = fabric.util.string.graphemeSplit(text); + var graphemes = this.graphemeSplit(text); this.insertNewStyleBlock(graphemes, start, style); this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end)); this.text = this._text.join(''); @@ -30312,6 +31139,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''), (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''), (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''), + (this.direction === 'rtl' ? 'direction="' + this.direction + '" ' : ''), 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textAndBg.textSpans.join(''), '\n' @@ -30334,6 +31162,9 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); + if (this.direction === 'rtl') { + lineOffset += this.width; + } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } @@ -30408,7 +31239,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); charsToRender = ''; actualStyle = nextStyle; - textLeftOffset += boxWidth; + if (this.direction === 'rtl') { + textLeftOffset -= boxWidth; + } + else { + textLeftOffset += boxWidth; + } boxWidth = 0; } } @@ -30773,7 +31609,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot var wrapped = [], i; this.isWrapping = true; for (i = 0; i < lines.length; i++) { - wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth)); + wrapped.push.apply(wrapped, this._wrapLine(lines[i], i, desiredWidth)); } this.isWrapping = false; return wrapped; @@ -30781,13 +31617,15 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * Helper function to measure a string of text, given its lineIndex and charIndex offset - * it gets called when charBounds are not available yet. + * It gets called when charBounds are not available yet. + * Override if necessary + * Use with {@link fabric.Textbox#wordSplit} + * * @param {CanvasRenderingContext2D} ctx * @param {String} text * @param {number} lineIndex * @param {number} charOffset * @returns {number} - * @private */ _measureWord: function(word, lineIndex, charOffset) { var width = 0, prevGrapheme, skipLeft = true; @@ -30800,6 +31638,16 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot return width; }, + /** + * Override this method to customize word splitting + * Use with {@link fabric.Textbox#_measureWord} + * @param {string} value + * @returns {string[]} array of words + */ + wordSplit: function (value) { + return value.split(this._wordJoiners); + }, + /** * Wraps a line of text using the width of the Textbox and a context. * @param {Array} line The grapheme array that represent the line @@ -30815,7 +31663,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot graphemeLines = [], line = [], // spaces in different languages? - words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), + words = splitByGrapheme ? this.graphemeSplit(_line) : this.wordSplit(_line), word = '', offset = 0, infix = splitByGrapheme ? '' : ' ', @@ -30830,14 +31678,25 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot words.push([]); } desiredWidth -= reservedSpace; - for (var i = 0; i < words.length; i++) { + // measure words + var data = words.map(function (word) { // if using splitByGrapheme words are already in graphemes. - word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]); - wordWidth = this._measureWord(word, lineIndex, offset); + word = splitByGrapheme ? word : this.graphemeSplit(word); + var width = this._measureWord(word, lineIndex, offset); + largestWordWidth = Math.max(width, largestWordWidth); + offset += word.length + 1; + return { word: word, width: width }; + }.bind(this)); + var maxWidth = Math.max(desiredWidth, largestWordWidth, this.dynamicMinWidth); + // layout words + offset = 0; + for (var i = 0; i < words.length; i++) { + word = data[i].word; + wordWidth = data[i].width; offset += word.length; lineWidth += infixWidth + wordWidth - additionalSpace; - if (lineWidth > desiredWidth && !lineJustStarted) { + if (lineWidth > maxWidth && !lineJustStarted) { graphemeLines.push(line); line = []; lineWidth = wordWidth; @@ -30855,10 +31714,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); offset++; lineJustStarted = false; - // keep track of largest word - if (wordWidth > largestWordWidth) { - largestWordWidth = wordWidth; - } } i && graphemeLines.push(line); @@ -30952,10 +31807,10 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @static * @memberOf fabric.Textbox * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created + * @returns {Promise} */ - fabric.Textbox.fromObject = function(object, callback) { - return fabric.Object._fromObject('Textbox', object, callback, 'text'); + fabric.Textbox.fromObject = function(object) { + return fabric.Object._fromObject(fabric.Textbox, object, 'text'); }; })(typeof exports !== 'undefined' ? exports : this); diff --git a/src/mixins/object_ancestry.mixin.js b/src/mixins/object_ancestry.mixin.js index f066433fd07..8c74ea48d2b 100644 --- a/src/mixins/object_ancestry.mixin.js +++ b/src/mixins/object_ancestry.mixin.js @@ -40,16 +40,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Returns an object that represent the ancestry situation. - * + * * @typedef {object} AncestryComparison * @property {Ancestors} common ancestors of `this` and `other` (may include `this` | `other`) * @property {Ancestors} fork ancestors that are of `this` only * @property {Ancestors} otherFork ancestors that are of `other` only - * + * * @param {fabric.Object} other * @param {boolean} [strict] finds only ancestors that are objects (without canvas) * @returns {AncestryComparison | undefined} - * + * */ findCommonAncestors: function (other, strict) { if (this === other) { From 53027cb2fcb5883ec9df66bce6497805f50bb61c Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 1 May 2022 00:18:23 +0200 Subject: [PATCH 69/77] remove whitespace --- dist/fabric.js | 4351 +++++++++++++++++++----------------------------- 1 file changed, 1748 insertions(+), 2603 deletions(-) diff --git a/dist/fabric.js b/dist/fabric.js index d832d46591d..9b7bf655d2a 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -358,68 +358,79 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') { */ fabric.Collection = { - /** - * @type {fabric.Object[]} - */ _objects: [], /** * Adds objects to collection, Canvas or Group, then renders canvas * (if `renderOnAddRemove` is not `false`). + * in case of Group no changes to bounding box are made. * Objects should be instances of (or inherit from) fabric.Object - * @private - * @param {fabric.Object[]} objects to add - * @param {(object:fabric.Object) => any} [callback] - * @returns {number} new array length + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the add method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable */ - add: function (objects, callback) { - var size = this._objects.push.apply(this._objects, objects); - if (callback) { - for (var i = 0; i < objects.length; i++) { - callback.call(this, objects[i]); + add: function () { + this._objects.push.apply(this._objects, arguments); + if (this._onObjectAdded) { + for (var i = 0, length = arguments.length; i < length; i++) { + this._onObjectAdded(arguments[i]); } } - return size; + this.renderOnAddRemove && this.requestRenderAll(); + return this; }, /** * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) * An object should be an instance of (or inherit from) fabric.Object - * @private - * @param {fabric.Object|fabric.Object[]} objects Object(s) to insert + * Use of this function is highly discouraged for groups. + * you can add a bunch of objects with the insertAt method but then you NEED + * to run a addWithUpdate call for the Group class or position/bbox will be wrong. + * @param {Object} object Object to insert * @param {Number} index Index to insert object at - * @param {(object:fabric.Object) => any} [callback] + * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs + * @return {Self} thisArg + * @chainable */ - insertAt: function (objects, index, callback) { - var args = [index, 0].concat(objects); - this._objects.splice.apply(this._objects, args); - if (callback) { - for (var i = 2; i < args.length; i++) { - callback.call(this, args[i]); - } + insertAt: function (object, index, nonSplicing) { + var objects = this._objects; + if (nonSplicing) { + objects[index] = object; + } + else { + objects.splice(index, 0, object); } + this._onObjectAdded && this._onObjectAdded(object); + this.renderOnAddRemove && this.requestRenderAll(); + return this; }, /** * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) - * @private - * @param {fabric.Object[]} objectsToRemove objects to remove - * @param {(object:fabric.Object) => any} [callback] function to call for each object removed - * @returns {fabric.Object[]} removed objects - */ - remove: function(objectsToRemove, callback) { - var objects = this._objects, removed = []; - for (var i = 0, object, index; i < objectsToRemove.length; i++) { - object = objectsToRemove[i]; - index = objects.indexOf(object); + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + remove: function() { + var objects = this._objects, + index, somethingRemoved = false; + + for (var i = 0, length = arguments.length; i < length; i++) { + index = objects.indexOf(arguments[i]); + // only call onObjectRemoved if an object was actually removed if (index !== -1) { + somethingRemoved = true; objects.splice(index, 1); - removed.push(object); - callback && callback.call(this, object); + this._onObjectRemoved && this._onObjectRemoved(arguments[i]); } } - return removed; + + this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); + return this; }, /** @@ -436,7 +447,7 @@ fabric.Collection = { */ forEachObject: function(callback, context) { var objects = this.getObjects(); - for (var i = 0; i < objects.length; i++) { + for (var i = 0, len = objects.length; i < len; i++) { callback.call(context, objects[i], i, objects); } return this; @@ -444,16 +455,17 @@ fabric.Collection = { /** * Returns an array of children objects of this instance - * @param {...String} [types] When specified, only objects of these types are returned + * Type parameter introduced in 1.3.10 + * since 2.3.5 this method return always a COPY of the array; + * @param {String} [type] When specified, only objects of this type are returned * @return {Array} */ - getObjects: function() { - if (arguments.length === 0) { + getObjects: function(type) { + if (typeof type === 'undefined') { return this._objects.concat(); } - var types = Array.from(arguments); - return this._objects.filter(function (o) { - return types.indexOf(o.type) > -1; + return this._objects.filter(function(o) { + return o.type === type; }); }, @@ -483,9 +495,7 @@ fabric.Collection = { }, /** - * Returns true if collection contains an object.\ - * **Prefer using {@link `fabric.Object#isDescendantOf`} for performance reasons** - * instead of a.contains(b) use b.isDescendantOf(a) + * Returns true if collection contains an object * @param {Object} object Object to check against * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` * @return {Boolean} `true` if collection contains an object @@ -530,6 +540,32 @@ fabric.CommonMethods = { } }, + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Gradient to + */ + _initGradient: function(filler, property) { + if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { + this.set(property, new fabric.Gradient(filler)); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Pattern to + * @param {Function} [callback] callback to invoke after pattern load + */ + _initPattern: function(filler, property, callback) { + if (filler && filler.source && !(filler instanceof fabric.Pattern)) { + this.set(property, new fabric.Pattern(filler, callback)); + } + else { + callback && callback(); + } + }, + /** * @private */ @@ -593,10 +629,6 @@ fabric.CommonMethods = { PiBy180 = Math.PI / 180, PiBy2 = Math.PI / 2; - /** - * @typedef {[number,number,number,number,number,number]} Matrix - */ - /** * @namespace fabric.util */ @@ -708,7 +740,7 @@ fabric.CommonMethods = { rotatePoint: function(point, origin, radians) { var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), v = fabric.util.rotateVector(newPoint, radians); - return v.addEquals(origin); + return new fabric.Point(v.x, v.y).addEquals(origin); }, /** @@ -717,14 +749,17 @@ fabric.CommonMethods = { * @memberOf fabric.util * @param {Object} vector The vector to rotate (x and y) * @param {Number} radians The radians of the angle for the rotation - * @return {fabric.Point} The new rotated point + * @return {Object} The new rotated point */ rotateVector: function(vector, radians) { var sin = fabric.util.sin(radians), cos = fabric.util.cos(radians), rx = vector.x * cos - vector.y * sin, ry = vector.x * sin + vector.y * cos; - return new fabric.Point(rx, ry); + return { + x: rx, + y: ry + }; }, /** @@ -763,7 +798,7 @@ fabric.CommonMethods = { * @returns {Point} vector representing the unit vector of pointing to the direction of `v` */ getHatVector: function (v) { - return new fabric.Point(v.x, v.y).scalarMultiply(1 / Math.hypot(v.x, v.y)); + return new fabric.Point(v.x, v.y).multiply(1 / Math.hypot(v.x, v.y)); }, /** @@ -878,70 +913,8 @@ fabric.CommonMethods = { ); }, - /** - * Sends a point from the source coordinate plane to the destination coordinate plane.\ - * From the canvas/viewer's perspective the point remains unchanged. - * - * @example Send point from canvas plane to group plane - * var obj = new fabric.Rect({ left: 20, top: 20, width: 60, height: 60, strokeWidth: 0 }); - * var group = new fabric.Group([obj], { strokeWidth: 0 }); - * var sentPoint1 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), null, group.calcTransformMatrix()); - * var sentPoint2 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), fabric.iMatrix, group.calcTransformMatrix()); - * console.log(sentPoint1, sentPoint2) // both points print (0,0) which is the center of group - * - * @static - * @memberOf fabric.util - * @see {fabric.util.transformPointRelativeToCanvas} for transforming relative to canvas - * @param {fabric.Point} point - * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `point` exists in the canvas coordinate plane. - * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `point` should be sent to the canvas coordinate plane. - * @returns {fabric.Point} transformed point - */ - sendPointToPlane: function (point, from, to) { - // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) - // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) - var inv = fabric.util.invertTransform(to || fabric.iMatrix); - var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); - return fabric.util.transformPoint(point, t); - }, - - /** - * Transform point relative to canvas. - * From the viewport/viewer's perspective the point remains unchanged. - * - * `child` relation means `point` exists in the coordinate plane created by `canvas`. - * In other words point is measured acoording to canvas' top left corner - * meaning that if `point` is equal to (0,0) it is positioned at canvas' top left corner. - * - * `sibling` relation means `point` exists in the same coordinate plane as canvas. - * In other words they both relate to the same (0,0) and agree on every point, which is how an event relates to canvas. - * - * @static - * @memberOf fabric.util - * @param {fabric.Point} point - * @param {fabric.StaticCanvas} canvas - * @param {'sibling'|'child'} relationBefore current relation of point to canvas - * @param {'sibling'|'child'} relationAfter desired relation of point to canvas - * @returns {fabric.Point} transformed point - */ - transformPointRelativeToCanvas: function (point, canvas, relationBefore, relationAfter) { - if (relationBefore !== 'child' && relationBefore !== 'sibling') { - throw new Error('fabric.js: received bad argument ' + relationBefore); - } - if (relationAfter !== 'child' && relationAfter !== 'sibling') { - throw new Error('fabric.js: received bad argument ' + relationAfter); - } - if (relationBefore === relationAfter) { - return point; - } - var t = canvas.viewportTransform; - return fabric.util.transformPoint(point, relationAfter === 'child' ? fabric.util.invertTransform(t) : t); - }, - /** * Returns coordinates of points's bounding rectangle (left, top, width, height) - * @static - * @memberOf fabric.util * @param {Array} points 4 points array * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix * @return {Object} Object with left, top, width, height properties @@ -1107,84 +1080,185 @@ fabric.CommonMethods = { }, /** - * Loads image element from given url and resolve it, or catch. + * Loads image element from given url and passes it to a callback * @memberOf fabric.util * @param {String} url URL representing an image - * @param {Object} [options] image loading options - * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous - * @param {Promise} img the loaded image. - */ - loadImage: function(url, options) { - return new Promise(function(resolve, reject) { - var img = fabric.util.createImage(); - var done = function() { - img.onload = img.onerror = null; - resolve(img); - }; - if (!url) { - done(); - } - else { - img.onload = done; - img.onerror = function () { - reject(new Error('Error loading ' + img.src)); - }; - options && options.crossOrigin && (img.crossOrigin = options.crossOrigin); - img.src = url; - } - }); + * @param {Function} callback Callback; invoked with loaded image + * @param {*} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } + + var img = fabric.util.createImage(); + + /** @ignore */ + var onLoadCallback = function () { + callback && callback.call(context, img, false); + img = img.onload = img.onerror = null; + }; + + img.onload = onLoadCallback; + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + // crossOrigin null is the same as not set. + if (url.indexOf('data') !== 0 && + crossOrigin !== undefined && + crossOrigin !== null) { + img.crossOrigin = crossOrigin; + } + + // IE10 / IE11-Fix: SVG contents from data: URI + // will only be available if the IMG is present + // in the DOM (and visible) + if (url.substring(0,14) === 'data:image/svg') { + img.onload = null; + fabric.util.loadImageInDom(img, onLoadCallback); + } + + img.src = url; + }, + + /** + * Attaches SVG image with data: URL to the dom + * @memberOf fabric.util + * @param {Object} img Image object with data:image/svg src + * @param {Function} callback Callback; invoked with loaded image + * @return {Object} DOM element (div containing the SVG image) + */ + loadImageInDom: function(img, onLoadCallback) { + var div = fabric.document.createElement('div'); + div.style.width = div.style.height = '1px'; + div.style.left = div.style.top = '-100%'; + div.style.position = 'absolute'; + div.appendChild(img); + fabric.document.querySelector('body').appendChild(div); + /** + * Wrap in function to: + * 1. Call existing callback + * 2. Cleanup DOM + */ + img.onload = function () { + onLoadCallback(); + div.parentNode.removeChild(div); + div = null; + }; }, /** * Creates corresponding fabric instances from their object representations * @static * @memberOf fabric.util - * @param {Object[]} objects Objects to enliven + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created * @param {String} namespace Namespace to get klass "Class" object from * @param {Function} reviver Method for further parsing of object elements, * called after each fabric object created. */ - enlivenObjects: function(objects, namespace, reviver) { - return Promise.all(objects.map(function(obj) { - var klass = fabric.util.getKlass(obj.type, namespace); - return klass.fromObject(obj).then(function(fabricInstance) { - reviver && reviver(obj, fabricInstance); - return fabricInstance; + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || []; + + var enlivenedObjects = [], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects.filter(function(obj) { + // filter out undefined objects (objects that gave error) + return obj; + })); + } + } + + if (!numTotalObjects) { + callback && callback(enlivenedObjects); + return; + } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { + onLoaded(); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + klass.fromObject(o, function (obj, error) { + error || (enlivenedObjects[index] = obj); + reviver && reviver(o, obj, error); + onLoaded(); }); - })); + }); }, /** * Creates corresponding fabric instances residing in an object, e.g. `clipPath` - * @param {Object} object with properties to enlive ( fill, stroke, clipPath, path ) - * @returns {Promise} the input object with enlived values + * @see {@link fabric.Object.ENLIVEN_PROPS} + * @param {Object} object + * @param {Object} [context] assign enlived props to this object (pass null to skip this) + * @param {(objects:fabric.Object[]) => void} callback + */ + enlivenObjectEnlivables: function (object, context, callback) { + var enlivenProps = fabric.Object.ENLIVEN_PROPS.filter(function (key) { return !!object[key]; }); + fabric.util.enlivenObjects(enlivenProps.map(function (key) { return object[key]; }), function (enlivedProps) { + var objects = {}; + enlivenProps.forEach(function (key, index) { + objects[key] = enlivedProps[index]; + context && (context[key] = enlivedProps[index]); + }); + callback && callback(objects); + }); + }, + + /** + * Create and wait for loading of patterns + * @static + * @memberOf fabric.util + * @param {Array} patterns Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * called after each fabric object created. */ + enlivenPatterns: function(patterns, callback) { + patterns = patterns || []; - enlivenObjectEnlivables: function (serializedObject) { - // enlive every possible property - var promises = Object.values(serializedObject).map(function(value) { - if (!value) { - return value; + function onLoaded() { + if (++numLoadedPatterns === numPatterns) { + callback && callback(enlivenedPatterns); } - if (value.colorStops) { - return new fabric.Gradient(value); - } - if (value.type) { - return fabric.util.enlivenObjects([value]).then(function (enlived) { - return enlived[0]; + } + + var enlivenedPatterns = [], + numLoadedPatterns = 0, + numPatterns = patterns.length; + + if (!numPatterns) { + callback && callback(enlivenedPatterns); + return; + } + + patterns.forEach(function (p, index) { + if (p && p.source) { + new fabric.Pattern(p, function(pattern) { + enlivenedPatterns[index] = pattern; + onLoaded(); }); } - if (value.source) { - return fabric.Pattern.fromObject(value); + else { + enlivenedPatterns[index] = p; + onLoaded(); } - return value; - }); - var keys = Object.keys(serializedObject); - return Promise.all(promises).then(function(enlived) { - return enlived.reduce(function(acc, instance, index) { - acc[keys[index]] = instance; - return acc; - }, {}); }); }, @@ -1193,13 +1267,32 @@ fabric.CommonMethods = { * @static * @memberOf fabric.util * @param {Array} elements SVG elements to group + * @param {Object} [options] Options object + * @param {String} path Value to set sourcePath to * @return {fabric.Object|fabric.Group} */ - groupSVGElements: function(elements) { + groupSVGElements: function(elements, options, path) { + var object; if (elements && elements.length === 1) { return elements[0]; } - return new fabric.Group(elements); + if (options) { + if (options.width && options.height) { + options.centerPoint = { + x: options.width / 2, + y: options.height / 2 + }; + } + else { + delete options.width; + delete options.height; + } + } + object = new fabric.Group(elements, options); + if (typeof path !== 'undefined') { + object.sourcePath = path; + } + return object; }, /** @@ -1211,7 +1304,7 @@ fabric.CommonMethods = { * @return {Array} properties Properties names to include */ populateWithProperties: function(source, destination, properties) { - if (properties && Array.isArray(properties)) { + if (properties && Object.prototype.toString.call(properties) === '[object Array]') { for (var i = 0, len = properties.length; i < len; i++) { if (properties[i] in source) { destination[properties[i]] = source[properties[i]]; @@ -1612,7 +1705,7 @@ fabric.CommonMethods = { * this is equivalent to remove from that object that transformation, so that * added in a space with the removed transform, the object will be the same as before. * Removing from an object a transform that scale by 2 is like scaling it by 1/2. - * Removing from an object a transform that rotate by 30deg is like rotating by 30deg + * Removing from an object a transfrom that rotate by 30deg is like rotating by 30deg * in the opposite direction. * This util is used to add objects inside transformed groups or nested groups. * @memberOf fabric.util @@ -1660,50 +1753,6 @@ fabric.CommonMethods = { object.setPositionByOrigin(center, 'center', 'center'); }, - /** - * - * A util that abstracts applying transform to objects.\ - * Sends `object` to the destination coordinate plane by applying the relevant transformations.\ - * Changes the space/plane where `object` is drawn.\ - * From the canvas/viewer's perspective `object` remains unchanged. - * - * @example Move clip path from one object to another while preserving it's appearance as viewed by canvas/viewer - * let obj, obj2; - * let clipPath = new fabric.Circle({ radius: 50 }); - * obj.clipPath = clipPath; - * // render - * fabric.util.sendObjectToPlane(clipPath, obj.calcTransformMatrix(), obj2.calcTransformMatrix()); - * obj.clipPath = undefined; - * obj2.clipPath = clipPath; - * // render, clipPath now clips obj2 but seems unchanged from the eyes of the viewer - * - * @example Clip an object's clip path with an existing object - * let obj, existingObj; - * let clipPath = new fabric.Circle({ radius: 50 }); - * obj.clipPath = clipPath; - * let transformTo = fabric.util.multiplyTransformMatrices(obj.calcTransformMatrix(), clipPath.calcTransformMatrix()); - * fabric.util.sendObjectToPlane(existingObj, existingObj.group?.calcTransformMatrix(), transformTo); - * clipPath.clipPath = existingObj; - * - * @static - * @memberof fabric.util - * @param {fabric.Object} object - * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `object` is a direct child of canvas. - * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `object` should be sent to the canvas coordinate plane. - * @returns {Matrix} the transform matrix that was applied to `object` - */ - sendObjectToPlane: function (object, from, to) { - // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping) - // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from) - var inv = fabric.util.invertTransform(to || fabric.iMatrix); - var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix); - fabric.util.applyTransformToObject( - object, - fabric.util.multiplyTransformMatrices(t, object.calcOwnMatrix()) - ); - return t; - }, - /** * given a width and height, return the size of the bounding box * that can contains the box with width/height with applied transform @@ -2776,7 +2825,7 @@ fabric.CommonMethods = { /** * Creates an empty object and copies all enumerable properties of another object to it - * This method is mostly for internal use, and not intended for duplicating shapes in canvas. + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. * @memberOf fabric.util.object * @param {Object} object Object to clone * @param {Boolean} [deep] Whether to clone nested objects @@ -2785,7 +2834,7 @@ fabric.CommonMethods = { //TODO: this function return an empty object if you try to clone null function clone(object, deep) { - return deep ? extend({ }, object, deep) : Object.assign({}, object); + return extend({ }, object, deep); } /** @namespace fabric.util.object */ @@ -3463,7 +3512,6 @@ fabric.CommonMethods = { /** * Cross-browser abstraction for sending XMLHttpRequest * @memberOf fabric.util - * @deprecated this has to go away, we can use a modern browser method to do the same. * @param {String} url URL to send XMLHttpRequest to * @param {Object} [options] Options object * @param {String} [options.method="GET"] @@ -3528,18 +3576,30 @@ fabric.warn = console.warn; clone = fabric.util.object.clone; /** - * * @typedef {Object} AnimationOptions * Animation of a value or list of values. + * When using lists, think of something like this: + * fabric.util.animate({ + * startValue: [1, 2, 3], + * endValue: [2, 4, 6], + * onChange: function([a, b, c]) { + * canvas.zoomToPoint({x: b, y: c}, a) + * canvas.renderAll() + * } + * }); + * @example * @property {Function} [onChange] Callback; invoked on every value change * @property {Function} [onComplete] Callback; invoked when value change is completed + * @example + * // Note: startValue, endValue, and byValue must match the type + * var animationOptions = { startValue: 0, endValue: 1, byValue: 0.25 } + * var animationOptions = { startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] } * @property {number | number[]} [startValue=0] Starting value * @property {number | number[]} [endValue=100] Ending value * @property {number | number[]} [byValue=100] Value to modify the property by * @property {Function} [easing] Easing function - * @property {number} [duration=500] Duration of change (in ms) + * @property {Number} [duration=500] Duration of change (in ms) * @property {Function} [abort] Additional function with logic. If returns true, animation aborts. - * @property {number} [delay] Delay of animation start (in ms) * * @typedef {() => void} CancelFunction * @@ -3649,27 +3709,10 @@ fabric.warn = console.warn; * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. * @memberOf fabric.util * @param {AnimationOptions} [options] Animation options - * When using lists, think of something like this: - * @example - * fabric.util.animate({ - * startValue: [1, 2, 3], - * endValue: [2, 4, 6], - * onChange: function([x, y, zoom]) { - * canvas.zoomToPoint(new fabric.Point(x, y), zoom); - * canvas.requestRenderAll(); - * } - * }); - * * @example - * fabric.util.animate({ - * startValue: 1, - * endValue: 0, - * onChange: function(v) { - * obj.set('opacity', v); - * canvas.requestRenderAll(); - * } - * }); - * + * // Note: startValue, endValue, and byValue must match the type + * fabric.util.animate({ startValue: 0, endValue: 1, byValue: 0.25 }) + * fabric.util.animate({ startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] }) * @returns {CancelFunction} cancel function */ function animate(options) { @@ -3692,7 +3735,7 @@ fabric.warn = console.warn; }); fabric.runningAnimations.push(context); - var runner = function (timestamp) { + requestAnimFrame(function(timestamp) { var start = timestamp || +new Date(), duration = options.duration || 500, finish = start + duration, time, @@ -3745,16 +3788,7 @@ fabric.warn = console.warn; requestAnimFrame(tick); } })(start); - }; - - if (options.delay) { - setTimeout(function () { - requestAnimFrame(runner); - }, options.delay); - } - else { - requestAnimFrame(runner); - } + }); return context.cancel; } @@ -4348,7 +4382,8 @@ fabric.warn = console.warn; } function normalizeValue(attr, value, parentAttributes, fontSize) { - var isArray = Array.isArray(value), parsed; + var isArray = Object.prototype.toString.call(value) === '[object Array]', + parsed; if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; @@ -4746,7 +4781,7 @@ fabric.warn = console.warn; return; } - var xlink = xlinkAttribute.slice(1), + var xlink = xlinkAttribute.substr(1), x = el.getAttribute('x') || 0, y = el.getAttribute('y') || 0, el2 = elementById(doc, xlink).cloneNode(true), @@ -5018,7 +5053,7 @@ fabric.warn = console.warn; function recursivelyParseGradientsXlink(doc, gradient) { var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], xlinkAttr = 'xlink:href', - xLink = gradient.getAttribute(xlinkAttr).slice(1), + xLink = gradient.getAttribute(xlinkAttr).substr(1), referencedGradient = elementById(doc, xLink); if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { recursivelyParseGradientsXlink(doc, referencedGradient); @@ -5633,61 +5668,47 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, - /** - * Multiplies this point by another value and returns a new one - * @param {fabric.Point} that - * @return {fabric.Point} - */ - multiply: function (that) { - return new Point(this.x * that.x, this.y * that.y); - }, - /** * Multiplies this point by a value and returns a new one + * TODO: rename in scalarMultiply in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - scalarMultiply: function (scalar) { + multiply: function (scalar) { return new Point(this.x * scalar, this.y * scalar); }, /** * Multiplies this point by a value + * TODO: rename in scalarMultiplyEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - scalarMultiplyEquals: function (scalar) { + multiplyEquals: function (scalar) { this.x *= scalar; this.y *= scalar; return this; }, - /** - * Divides this point by another and returns a new one - * @param {fabric.Point} that - * @return {fabric.Point} - */ - divide: function (that) { - return new Point(this.x / that.x, this.y / that.y); - }, - /** * Divides this point by a value and returns a new one + * TODO: rename in scalarDivide in 2.0 * @param {Number} scalar * @return {fabric.Point} */ - scalarDivide: function (scalar) { + divide: function (scalar) { return new Point(this.x / scalar, this.y / scalar); }, /** * Divides this point by a value + * TODO: rename in scalarDivideEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg * @chainable */ - scalarDivideEquals: function (scalar) { + divideEquals: function (scalar) { this.x /= scalar; this.y /= scalar; return this; @@ -6705,9 +6726,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @return {Number} 0 - 7 a quadrant number */ function findCornerQuadrant(fabricObject, control) { - // angle is relative to canvas plane - var angle = fabricObject.getTotalAngle(); - var cornerAngle = angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; return Math.round((cornerAngle % 360) / 45); } @@ -6876,7 +6895,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ function wrapWithFixedAnchor(actionHandler) { return function(eventData, transform, x, y) { - var target = transform.target, centerPoint = target.getRelativeCenterPoint(), + var target = transform.target, centerPoint = target.getCenterPoint(), constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), actionPerformed = actionHandler(eventData, transform, x, y); target.setPositionByOrigin(constraint, transform.originX, transform.originY); @@ -6914,7 +6933,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp control = target.controls[transform.corner], zoom = target.canvas.getZoom(), padding = target.padding / zoom, - localPoint = target.normalizePoint(new fabric.Point(x, y), originX, originY); + localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); if (localPoint.x >= padding) { localPoint.x -= padding; } @@ -6960,7 +6979,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectX(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions({ skewX: 0, skewY: target.skewY }), + dimNoSkew = target._getTransformedDimensions(0, target.skewY), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7003,7 +7022,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function skewObjectY(eventData, transform, x, y) { var target = transform.target, // find how big the object would be, if there was no skewX. takes in account scaling - dimNoSkew = target._getTransformedDimensions({ skewX: target.skewX, skewY: 0 }), + dimNoSkew = target._getTransformedDimensions(target.skewX, 0), localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), // the mouse is in the center of the object, and we want it to stay there. // so the object will grow twice as much as the mouse. @@ -7152,7 +7171,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp function rotationWithSnapping(eventData, transform, x, y) { var t = transform, target = t.target, - pivotPoint = target.translateToOriginPoint(target.getRelativeCenterPoint(), t.originX, t.originY); + pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); if (target.lockRotation) { return false; @@ -7371,10 +7390,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), multiplier = isTransformCentered(transform) ? 2 : 1, oldWidth = target.width, - newWidth = Math.ceil(Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding); + newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; target.set('width', Math.max(newWidth, 0)); - // check against actual target width in case `newWidth` was rejected - return oldWidth !== target.width; + return oldWidth !== newWidth; } /** @@ -7508,9 +7526,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp // this is still wrong ctx.lineWidth = 1; ctx.translate(left, top); - // angle is relative to canvas plane - var angle = fabricObject.getTotalAngle(); - ctx.rotate(degreesToRadians(angle)); + ctx.rotate(degreesToRadians(fabricObject.angle)); // this does not work, and fixed with ( && ) does not make sense. // to have real transparent corners we need the controls on upperCanvas // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); @@ -8413,18 +8429,30 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ patternTransform: null, - type: 'pattern', - /** * Constructor * @param {Object} [options] Options object - * @param {option.source} [source] the pattern source, eventually empty or a drawable + * @param {Function} [callback] function to invoke after callback init. * @return {fabric.Pattern} thisArg */ - initialize: function(options) { + initialize: function(options, callback) { options || (options = { }); + this.id = fabric.Object.__uid++; this.setOptions(options); + if (!options.source || (options.source && typeof options.source !== 'string')) { + callback && callback(this); + return; + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img, isError) { + _this.source = img; + callback && callback(_this, isError); + }, null, this.crossOrigin); + } }, /** @@ -8536,15 +8564,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return ctx.createPattern(source, this.repeat); } }); - - fabric.Pattern.fromObject = function(object) { - var patternOptions = Object.assign({}, object); - return fabric.util.loadImage(object.source, { crossOrigin: object.crossOrigin }) - .then(function(img) { - patternOptions.source = img; - return new fabric.Pattern(patternOptions); - }); - }; })(); @@ -8779,8 +8798,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @fires object:added * @fires object:removed */ - // eslint-disable-next-line max-len - fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, fabric.Collection, /** @lends fabric.StaticCanvas.prototype */ { + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { /** * Constructor @@ -8797,6 +8815,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. * @type {(String|fabric.Pattern)} * @default */ @@ -8814,6 +8833,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp /** * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} * @since 1.3.9 * @type {(String|fabric.Pattern)} * @default @@ -8950,6 +8970,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @param {Object} [options] Options object */ _initStatic: function(el, options) { + var cb = this.requestRenderAllBound; this._objects = []; this._createLowerCanvas(el); this._initOptions(options); @@ -8957,6 +8978,19 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp if (!this.interactive) { this._initRetinaScaling(); } + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, cb); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, cb); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, cb); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, cb); + } this.calcOffset(); }, @@ -9007,6 +9041,202 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp return this; }, + /** + * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} + * @example Normal overlayImage with left/top = 0 + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage loaded from cross-origin + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage loaded from cross-origin + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + // TODO: fix stretched examples + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to + * @param {Function} callback Callback to invoke when foreground color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} + * @example Normal backgroundColor - color value + * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setBackgroundColor: function(backgroundColor, callback) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img, isError) { + if (img) { + var instance = new fabric.Image(img, options); + this[property] = instance; + instance.canvas = this; + } + callback && callback(img, isError); + }, this, options && options.crossOrigin); + } + else { + options && image.setOptions(options); + this[property] = image; + image && (image.canvas = this); + callback && callback(image, false); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String|null)} color Object with pattern information, color value or null + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + this[property] = color; + this._initGradient(color, property); + this._initPattern(color, property, callback); + return this; + }, + /** * @private */ @@ -9061,15 +9291,10 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp else { this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); } - if (this.lowerCanvasEl.hasAttribute('data-fabric')) { - /* _DEV_MODE_START_ */ - throw new Error('fabric.js: trying to initialize a canvas that has already been initialized'); - /* _DEV_MODE_END_ */ - } + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); - this.lowerCanvasEl.setAttribute('data-fabric', 'main'); + this._originalCanvasStyle = this.lowerCanvasEl.style; if (this.interactive) { - this._originalCanvasStyle = this.lowerCanvasEl.style.cssText; this._applyCanvasStyle(this.lowerCanvasEl); } @@ -9152,7 +9377,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp } this._initRetinaScaling(); this.calcOffset(); - this.fire('resize', dimensions); if (!options.cssOnly) { this.requestRenderAll(); @@ -9313,75 +9537,31 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }, /** - * @param {...fabric.Object} objects to add - * @return {Self} thisArg - * @chainable + * @private + * @param {fabric.Object} obj Object that was added */ - add: function () { - fabric.Collection.add.call(this, arguments, this._onObjectAdded); - arguments.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); - return this; + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj._set('canvas', this); + obj.setCoords(); + this.fire('object:added', { target: obj }); + obj.fire('added'); }, /** - * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) - * An object should be an instance of (or inherit from) fabric.Object - * @param {fabric.Object|fabric.Object[]} objects Object(s) to insert - * @param {Number} index Index to insert object at - * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs - * @return {Self} thisArg - * @chainable + * @private + * @param {fabric.Object} obj Object that was removed */ - insertAt: function (objects, index) { - fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); - this.renderOnAddRemove && this.requestRenderAll(); - return this; + _onObjectRemoved: function(obj) { + this.fire('object:removed', { target: obj }); + obj.fire('removed'); + delete obj.canvas; }, /** - * @param {...fabric.Object} objects to remove - * @return {Self} thisArg - * @chainable - */ - remove: function () { - var removed = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); - removed.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * @private - * @param {fabric.Object} obj Object that was added - */ - _onObjectAdded: function(obj) { - this.stateful && obj.setupState(); - if (obj.canvas && obj.canvas !== this) { - /* _DEV_MODE_START_ */ - console.warn('fabric.Canvas: trying to add an object that belongs to a different canvas.\n' + - 'Resulting to default behavior: removing object from previous canvas and adding to new canvas'); - /* _DEV_MODE_END_ */ - obj.canvas.remove(obj); - } - obj._set('canvas', this); - obj.setCoords(); - this.fire('object:added', { target: obj }); - obj.fire('added', { target: this }); - }, - - /** - * @private - * @param {fabric.Object} obj Object that was removed - */ - _onObjectRemoved: function(obj) { - this.fire('object:removed', { target: obj }); - obj.fire('removed', { target: this }); - obj._set('canvas', undefined); - }, - - /** - * Clears specified context of canvas element - * @param {CanvasRenderingContext2D} ctx Context to clear - * @return {fabric.Canvas} thisArg + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg * @chainable */ clearContext: function(ctx) { @@ -9613,7 +9793,6 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * Returns coordinates of a center of canvas. * Returned value is an object with top and left properties * @return {Object} object with "top" and "left" number values - * @deprecated migrate to `getCenterPoint` */ getCenter: function () { return { @@ -9622,21 +9801,13 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }; }, - /** - * Returns coordinates of a center of canvas. - * @return {fabric.Point} - */ - getCenterPoint: function () { - return new fabric.Point(this.width / 2, this.height / 2); - }, - /** * Centers object horizontally in the canvas * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { - return this._centerObject(object, new fabric.Point(this.getCenterPoint().x, object.getCenterPoint().y)); + return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); }, /** @@ -9646,7 +9817,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObjectV: function (object) { - return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenterPoint().y)); + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); }, /** @@ -9656,8 +9827,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ centerObject: function(object) { - var center = this.getCenterPoint(); - return this._centerObject(object, center); + var center = this.getCenter(); + + return this._centerObject(object, new fabric.Point(center.left, center.top)); }, /** @@ -9668,6 +9840,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp */ viewportCenterObject: function(object) { var vpCenter = this.getVpCenter(); + return this._centerObject(object, vpCenter); }, @@ -9701,9 +9874,9 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ getVpCenter: function() { - var center = this.getCenterPoint(), + var center = this.getCenter(), iVpt = invertTransform(this.viewportTransform); - return transformPoint(center, iVpt); + return transformPoint({ x: center.left, y: center.top }, iVpt); }, /** @@ -9714,7 +9887,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp * @chainable */ _centerObject: function(object, center) { - object.setXY(center, 'center', 'center'); + object.setPositionByOrigin(center, 'center', 'center'); object.setCoords(); this.renderOnAddRemove && this.requestRenderAll(); return this; @@ -10367,13 +10540,10 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp this.overlayImage = null; this._iTextInstances = null; this.contextContainer = null; - // restore canvas style and attributes + // restore canvas style this.lowerCanvasEl.classList.remove('lower-canvas'); - this.lowerCanvasEl.removeAttribute('data-fabric'); - if (this.interactive) { - this.lowerCanvasEl.style.cssText = this._originalCanvasStyle; - delete this._originalCanvasStyle; - } + fabric.util.setStyle(this.lowerCanvasEl, this._originalCanvasStyle); + delete this._originalCanvasStyle; // restore canvas size to original size in case retina scaling was applied this.lowerCanvasEl.setAttribute('width', this.width); this.lowerCanvasEl.setAttribute('height', this.height); @@ -10393,6 +10563,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp }); extend(fabric.StaticCanvas.prototype, fabric.Observable); + extend(fabric.StaticCanvas.prototype, fabric.Collection); extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { @@ -11183,12 +11354,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric rects = this._getOptimizedRects(rects); } - var group = new fabric.Group(rects, { - objectCaching: true, - layout: 'fixed', - subTargetCheck: false, - interactive: false - }); + var group = new fabric.Group(rects); this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); this.canvas.fire('before:path:created', { path: group }); this.canvas.add(group); @@ -11402,28 +11568,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @fires after:render at the end of the render process, receives the context in the callback * @fires before:render at start the render process, receives the context in the callback * - * @fires contextmenu:before - * @fires contextmenu - * @example - * let handler; - * targets.forEach(target => { - * target.on('contextmenu:before', opt => { - * // decide which target should handle the event before canvas hijacks it - * if (someCaseHappens && opt.targets.includes(target)) { - * handler = target; - * } - * }); - * target.on('contextmenu', opt => { - * // do something fantastic - * }); - * }); - * canvas.on('contextmenu', opt => { - * if (!handler) { - * // no one takes responsibility, it's always left to me - * // let's show them how it's done! - * } - * }); - * */ fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { @@ -11734,13 +11878,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _hoveredTargets: [], - /** - * hold the list of objects to render - * @type fabric.Object[] - * @private - */ - _objectsToRender: undefined, - /** * @private */ @@ -11758,23 +11895,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); }, - /** - * @private - * @param {fabric.Object} obj Object that was added - */ - _onObjectAdded: function (obj) { - this._objectsToRender = undefined; - this.callSuper('_onObjectAdded', obj); - }, - - /** - * @private - * @param {fabric.Object} obj Object that was removed - */ - _onObjectRemoved: function (obj) { - this._objectsToRender = undefined; - this.callSuper('_onObjectRemoved', obj); - }, /** * Divides objects in two groups, one to render immediately * and one to render as activeGroup. @@ -11784,7 +11904,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var activeObjects = this.getActiveObjects(), object, objsToRender, activeGroupObjects; - if (!this.preserveObjectStacking && activeObjects.length > 1) { + if (activeObjects.length > 0 && !this.preserveObjectStacking) { objsToRender = []; activeGroupObjects = []; for (var i = 0, length = this._objects.length; i < length; i++) { @@ -11801,15 +11921,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } objsToRender.push.apply(objsToRender, activeGroupObjects); } - // in case a single object is selected render it's entire parent above the other objects - else if (!this.preserveObjectStacking && activeObjects.length === 1) { - var target = activeObjects[0], ancestors = target.getAncestors(true); - var topAncestor = ancestors.length === 0 ? target : ancestors.pop(); - objsToRender = this._objects.slice(); - var index = objsToRender.indexOf(topAncestor); - index > -1 && objsToRender.splice(objsToRender.indexOf(topAncestor), 1); - objsToRender.push(topAncestor); - } else { objsToRender = this._objects; } @@ -11831,8 +11942,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.hasLostContext = false; } var canvasToDrawOn = this.contextContainer; - !this._objectsToRender && (this._objectsToRender = this._chooseObjectsToRender()); - this.renderCanvas(canvasToDrawOn, this._objectsToRender); + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); return this; }, @@ -11923,7 +12033,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _isSelectionKeyPressed: function(e) { var selectionKeyPressed = false; - if (Array.isArray(this.selectionKey)) { + if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); } else { @@ -12038,22 +12148,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (!target) { return; } - var pointer = this.getPointer(e); - if (target.group) { - // transform pointer to target's containing coordinate plane - pointer = fabric.util.transformPoint(pointer, fabric.util.invertTransform(target.group.calcTransformMatrix())); - } - var corner = target.__corner, + + var pointer = this.getPointer(e), corner = target.__corner, control = target.controls[corner], actionHandler = (alreadySelected && corner) ? control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, action = this._getActionFromCorner(alreadySelected, corner, e, target), origin = this._getOriginFromCorner(target, corner), altKey = e[this.centeredKey], - /** - * relative to target's containing coordinate plane - * both agree on every point - **/ transform = { target: target, action: action, @@ -12063,6 +12165,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, + // used by transation offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, @@ -12071,7 +12174,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ey: pointer.y, lastX: pointer.x, lastY: pointer.y, + // unsure they are useful anymore. + // left: target.left, + // top: target.top, theta: degreesToRadians(target.angle), + // end of unsure width: target.width * target.scaleX, shiftKey: e.shiftKey, altKey: altKey, @@ -12164,12 +12271,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { return activeObject; } - if (aObjects.length > 1 && activeObject.type === 'activeSelection' - && !skipGroup && this.searchPossibleTargets([activeObject], pointer)) { + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { return activeObject; } if (aObjects.length === 1 && - activeObject === this.searchPossibleTargets([activeObject], pointer)) { + activeObject === this._searchPossibleTargets([activeObject], pointer)) { if (!this.preserveObjectStacking) { return activeObject; } @@ -12179,7 +12285,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.targets = []; } } - var target = this.searchPossibleTargets(this._objects, pointer); + var target = this._searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; this.targets = activeTargetSubs; @@ -12216,10 +12322,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Internal Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted * @param {Array} [objects] objects array to look into * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {fabric.Object} **top most object from given `objects`** that contains pointer + * @return {fabric.Object} object that contains pointer * @private */ _searchPossibleTargets: function(objects, pointer) { @@ -12233,7 +12339,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._normalizePointer(objToCheck.group, pointer) : pointer; if (this._checkTarget(pointerToUse, objToCheck, pointer)) { target = objects[i]; - if (target.subTargetCheck && Array.isArray(target._objects)) { + if (target.subTargetCheck && target instanceof fabric.Group) { subTarget = this._searchPossibleTargets(target._objects, pointer); subTarget && this.targets.push(subTarget); } @@ -12243,18 +12349,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return target; }, - /** - * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted - * @see {@link fabric.Canvas#_searchPossibleTargets} - * @param {Array} [objects] objects array to look into - * @param {Object} [pointer] x,y object of point coordinates we want to check. - * @return {fabric.Object} **top most object on screen** that contains pointer - */ - searchPossibleTargets: function (objects, pointer) { - var target = this._searchPossibleTargets(objects, pointer); - return target && target.interactive && this.targets[0] ? this.targets[0] : target; - }, - /** * Returns pointer coordinates without the effect of the viewport * @param {Object} pointer with "x" and "y" number values @@ -12270,27 +12364,27 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * Returns pointer coordinates relative to canvas. * Can return coordinates with or without viewportTransform. - * ignoreVpt false gives back coordinates that represent + * ignoreZoom false gives back coordinates that represent * the point clicked on canvas element. - * ignoreVpt true gives back coordinates after being processed + * ignoreZoom true gives back coordinates after being processed * by the viewportTransform ( sort of coordinates of what is displayed * on the canvas where you are clicking. - * ignoreVpt true = HTMLElement coordinates relative to top,left - * ignoreVpt false, default = fabric space coordinates, the same used for shape position - * To interact with your shapes top and left you want to use ignoreVpt true - * most of the time, while ignoreVpt false will give you coordinates + * ignoreZoom true = HTMLElement coordinates relative to top,left + * ignoreZoom false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreZoom true + * most of the time, while ignoreZoom false will give you coordinates * compatible with the object.oCoords system. * of the time. * @param {Event} e - * @param {Boolean} ignoreVpt + * @param {Boolean} ignoreZoom * @return {Object} object with "x" and "y" number values */ - getPointer: function (e, ignoreVpt) { + getPointer: function (e, ignoreZoom) { // return cached values if we are in the event processing chain - if (this._absolutePointer && !ignoreVpt) { + if (this._absolutePointer && !ignoreZoom) { return this._absolutePointer; } - if (this._pointer && ignoreVpt) { + if (this._pointer && ignoreZoom) { return this._pointer; } @@ -12313,7 +12407,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.calcOffset(); pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; - if (!ignoreVpt) { + if (!ignoreZoom) { pointer = this.restorePointerVpt(pointer); } @@ -12357,7 +12451,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.upperCanvasEl = upperCanvasEl; } fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); - this.upperCanvasEl.setAttribute('data-fabric', 'top'); + this.wrapperEl.appendChild(upperCanvasEl); this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); @@ -12379,13 +12473,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @private */ _initWrapperElement: function () { - if (this.wrapperEl) { - return; - } this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { 'class': this.containerClass }); - this.wrapperEl.setAttribute('data-fabric', 'wrapper'); fabric.util.setStyle(this.wrapperEl, { width: this.width + 'px', height: this.height + 'px', @@ -12426,17 +12516,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab toEl.style.cssText = fromEl.style.cssText; }, - /** - * Returns context of top canvas where interactions are drawn - * @returns {CanvasRenderingContext2D} - */ - getTopContext: function () { - return this.contextTop; - }, - /** * Returns context of canvas where object selection is drawn - * @alias * @return {CanvasRenderingContext2D} */ getSelectionContext: function() { @@ -12502,7 +12583,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _fireSelectionEvents: function(oldObjects, e) { var somethingChanged = false, objects = this.getActiveObjects(), - added = [], removed = [], invalidate = false; + added = [], removed = []; oldObjects.forEach(function(oldObject) { if (objects.indexOf(oldObject) === -1) { somethingChanged = true; @@ -12524,7 +12605,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } }); if (oldObjects.length > 0 && objects.length > 0) { - invalidate = true; somethingChanged && this.fire('selection:updated', { e: e, selected: added, @@ -12532,20 +12612,17 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }); } else if (objects.length > 0) { - invalidate = true; this.fire('selection:created', { e: e, selected: added, }); } else if (oldObjects.length > 0) { - invalidate = true; this.fire('selection:cleared', { e: e, deselected: removed, }); } - invalidate && (this._objectsToRender = undefined); }, /** @@ -12633,24 +12710,21 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ dispose: function () { - var wrapperEl = this.wrapperEl, - lowerCanvasEl = this.lowerCanvasEl, - upperCanvasEl = this.upperCanvasEl, - cacheCanvasEl = this.cacheCanvasEl; + var wrapper = this.wrapperEl; this.removeListeners(); - this.callSuper('dispose'); - wrapperEl.removeChild(upperCanvasEl); - wrapperEl.removeChild(lowerCanvasEl); + wrapper.removeChild(this.upperCanvasEl); + wrapper.removeChild(this.lowerCanvasEl); this.contextCache = null; this.contextTop = null; - fabric.util.cleanUpJsdomNode(upperCanvasEl); - this.upperCanvasEl = undefined; - fabric.util.cleanUpJsdomNode(cacheCanvasEl); - this.cacheCanvasEl = undefined; - if (wrapperEl.parentNode) { - wrapperEl.parentNode.replaceChild(lowerCanvasEl, wrapperEl); + ['upperCanvasEl', 'cacheCanvasEl'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + if (wrapper.parentNode) { + wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); } delete this.wrapperEl; + fabric.StaticCanvas.prototype.dispose.call(this); return this; }, @@ -12689,7 +12763,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var originalProperties = this._realizeGroupTransformOnObject(instance), object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); //Undo the damage we did by changing all of its properties - originalProperties && instance.set(originalProperties); + this._unwindGroupTransformOnObject(instance, originalProperties); return object; }, @@ -12715,6 +12789,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } }, + /** + * Restores the changed properties of instance + * @private + * @param {fabric.Object} [instance] the object to un-transform (gets mutated) + * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject + */ + _unwindGroupTransformOnObject: function(instance, originalValues) { + if (originalValues) { + instance.set(originalValues); + } + }, + /** * @private */ @@ -12723,7 +12809,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab //object when the group is deselected var originalProperties = this._realizeGroupTransformOnObject(instance); this.callSuper('_setSVGObject', markup, instance, reviver); - originalProperties && instance.set(originalProperties); + this._unwindGroupTransformOnObject(instance, originalProperties); }, setViewportTransform: function (vpt) { @@ -12981,12 +13067,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mousedown */ _onContextMenu: function (e) { - this._simpleEventHandler('contextmenu:before', e); if (this.stopContextMenu) { e.stopPropagation(); e.preventDefault(); } - this._simpleEventHandler('contextmenu', e); return false; }, @@ -13129,9 +13213,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * @private */ - _onResize: function (e) { + _onResize: function () { this.calcOffset(); - this.fire('window:resize', { e: e }); }, /** @@ -13459,13 +13542,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } } - var invalidate = shouldRender || shouldGroup; - // we clear `_objectsToRender` in case of a change in order to repopulate it at rendering - // run before firing the `down` event to give the dev a chance to populate it themselves - invalidate && (this._objectsToRender = undefined); this._handleEvent(e, 'down'); // we must renderAll so that we update the visuals - invalidate && this.requestRenderAll(); + (shouldRender || shouldGroup) && this.requestRenderAll(); }, /** @@ -13651,19 +13730,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _transformObject: function(e) { var pointer = this.getPointer(e), - transform = this._currentTransform, - target = transform.target, - // transform pointer to target's containing coordinate plane - // both pointer and object should agree on every point - localPointer = target.group ? - fabric.util.sendPointToPlane(pointer, null, target.group.calcTransformMatrix()) : - pointer; + transform = this._currentTransform; transform.reset = false; transform.shiftKey = e.shiftKey; transform.altKey = e[this.centeredKey]; - this._performTransformAction(e, transform, localPointer); + this._performTransformAction(e, transform, pointer); transform.actionPerformed && this.requestRenderAll(); }, @@ -13756,19 +13829,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _shouldGroup: function(e, target) { var activeObject = this._activeObject; - // check if an active object exists on canvas and if the user is pressing the `selectionKey` while canvas supports multi selection. - return !!activeObject && this._isSelectionKeyPressed(e) && this.selection - // on top of that the user also has to hit a target that is selectable. - && !!target && target.selectable - // if all pre-requisite pass, the target is either something different from the current - // activeObject or if an activeSelection already exists - // TODO at time of writing why `activeObject.type === 'activeSelection'` matter is unclear. - // is a very old condition uncertain if still valid. - && (activeObject !== target || activeObject.type === 'activeSelection') - // make sure `activeObject` and `target` aren't ancestors of each other - && !target.isDescendantOf(activeObject) && !activeObject.isDescendantOf(target) - // target accepts selection - && !target.onSelect({ e: e }); + return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && + (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); }, /** @@ -13804,8 +13866,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _updateActiveSelection: function(target, e) { var activeSelection = this._activeObject, currentActiveObjects = activeSelection._objects.slice(0); - if (target.group === activeSelection) { - activeSelection.remove(target); + if (activeSelection.contains(target)) { + activeSelection.removeWithUpdate(target); this._hoveredTarget = target; this._hoveredTargets = this.targets.concat(); if (activeSelection.size() === 1) { @@ -13814,7 +13876,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } else { - activeSelection.add(target); + activeSelection.addWithUpdate(target); this._hoveredTarget = activeSelection; this._hoveredTargets = this.targets.concat(); } @@ -13834,19 +13896,17 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._fireSelectionEvents(currentActives, e); }, - /** * @private * @param {Object} target - * @returns {fabric.ActiveSelection} */ _createGroup: function(target) { - var activeObject = this._activeObject; - var groupObjects = target.isInFrontOf(activeObject) ? - [activeObject, target] : - [target, activeObject]; - activeObject.isEditing && activeObject.exitEditing(); - // handle case: target is nested + var objects = this._objects, + isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), + groupObjects = isActiveLower + ? [this._activeObject, target] + : [target, this._activeObject]; + this._activeObject.isEditing && this._activeObject.exitEditing(); return new fabric.ActiveSelection(groupObjects, { canvas: this }); @@ -13947,9 +14007,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 - * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format - * @see {@link https://jsfiddle.net/xsjua1rd/ demo} + * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} * @example Generate jpeg dataURL with lower quality * var dataURL = canvas.toDataURL({ * format: 'jpeg', @@ -13968,11 +14027,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * format: 'png', * multiplier: 2 * }); - * @example Generate dataURL with objects that overlap a specified object - * var myObject; - * var dataURL = canvas.toDataURL({ - * filter: (object) => object.isContainedWithinObject(myObject) || object.intersectsWithObject(myObject) - * }); */ toDataURL: function (options) { options || (options = { }); @@ -13991,31 +14045,29 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * This is an intermediary step used to get to a dataUrl but also it is useful to * create quick image copies of a canvas without passing for the dataUrl string * @param {Number} [multiplier] a zoom factor. - * @param {Object} [options] Cropping informations - * @param {Number} [options.left] Cropping left offset. - * @param {Number} [options.top] Cropping top offset. - * @param {Number} [options.width] Cropping width. - * @param {Number} [options.height] Cropping height. - * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. - */ - toCanvasElement: function (multiplier, options) { + * @param {Object} [cropping] Cropping informations + * @param {Number} [cropping.left] Cropping left offset. + * @param {Number} [cropping.top] Cropping top offset. + * @param {Number} [cropping.width] Cropping width. + * @param {Number} [cropping.height] Cropping height. + */ + toCanvasElement: function(multiplier, cropping) { multiplier = multiplier || 1; - options = options || { }; - var scaledWidth = (options.width || this.width) * multiplier, - scaledHeight = (options.height || this.height) * multiplier, + cropping = cropping || { }; + var scaledWidth = (cropping.width || this.width) * multiplier, + scaledHeight = (cropping.height || this.height) * multiplier, zoom = this.getZoom(), originalWidth = this.width, originalHeight = this.height, newZoom = zoom * multiplier, vp = this.viewportTransform, - translateX = (vp[4] - (options.left || 0)) * multiplier, - translateY = (vp[5] - (options.top || 0)) * multiplier, + translateX = (vp[4] - (cropping.left || 0)) * multiplier, + translateY = (vp[5] - (cropping.top || 0)) * multiplier, originalInteractive = this.interactive, newVp = [newZoom, 0, 0, newZoom, translateX, translateY], originalRetina = this.enableRetinaScaling, canvasEl = fabric.util.createCanvasElement(), - originalContextTop = this.contextTop, - objectsToRender = options.filter ? this._objects.filter(options.filter) : this._objects; + originalContextTop = this.contextTop; canvasEl.width = scaledWidth; canvasEl.height = scaledHeight; this.contextTop = null; @@ -14025,7 +14077,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.width = scaledWidth; this.height = scaledHeight; this.calcViewportBoundaries(); - this.renderCanvas(canvasEl.getContext('2d'), objectsToRender); + this.renderCanvas(canvasEl.getContext('2d'), this._objects); this.viewportTransform = vp; this.width = originalWidth; this.height = originalHeight; @@ -14045,23 +14097,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. - * @return {Promise} instance + * @return {fabric.Canvas} instance * @chainable * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} * @example loadFromJSON - * canvas.loadFromJSON(json).then((canvas) => canvas.requestRenderAll()); + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); * @example loadFromJSON with reviver - * canvas.loadFromJSON(json, function(o, object) { + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { * // `o` = json object * // `object` = fabric.Object instance * // ... do some stuff ... - * }).then((canvas) => { - * ... canvas is restored, add your code. * }); */ - loadFromJSON: function (json, reviver) { + loadFromJSON: function (json, callback, reviver) { if (!json) { return; } @@ -14072,35 +14125,38 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati : fabric.util.object.clone(json); var _this = this, + clipPath = serialized.clipPath, renderOnAddRemove = this.renderOnAddRemove; this.renderOnAddRemove = false; - return fabric.util.enlivenObjects(serialized.objects || [], '', reviver) - .then(function(enlived) { - _this.clear(); - return fabric.util.enlivenObjectEnlivables({ - backgroundImage: serialized.backgroundImage, - backgroundColor: serialized.background, - overlayImage: serialized.overlayImage, - overlayColor: serialized.overlay, - clipPath: serialized.clipPath, - }) - .then(function(enlivedMap) { - _this.__setupCanvas(serialized, enlived, renderOnAddRemove); - _this.set(enlivedMap); - return _this; + delete serialized.clipPath; + + this._enlivenObjects(serialized.objects, function (enlivenedObjects) { + _this.clear(); + _this._setBgOverlay(serialized, function () { + if (clipPath) { + _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { + _this.clipPath = enlivenedCanvasClip[0]; + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); }); + } + else { + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + } }); + }, reviver); + return this; }, /** * @private * @param {Object} serialized Object with background and overlay information - * @param {Array} enlivenedObjects canvas objects - * @param {boolean} renderOnAddRemove renderOnAddRemove setting for the canvas + * @param {Array} restored canvas objects + * @param {Function} cached renderOnAddRemove callback + * @param {Function} callback Invoked after all background and overlay images/patterns loaded */ - __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove) { + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { var _this = this; enlivenedObjects.forEach(function(obj, index) { // we splice the array just in case some custom classes restored from JSON @@ -14119,17 +14175,122 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // create the Object instance. Here the Canvas is // already an instance and we are just loading things over it this._setOptions(serialized); + this.renderAll(); + callback && callback(); + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { + var loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } + + var cbIfLoaded = function () { + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + callback && callback(); + } + }; + + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); + }, + + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; + + if (!value) { + loaded[property] = true; + callback && callback(); + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.util.enlivenObjects([value], function(enlivedObject){ + _this[property] = enlivedObject[0]; + loaded[property] = true; + callback && callback(); + }); + } + else { + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); + }); + } + }, + + /** + * @private + * @param {Array} objects + * @param {Function} callback + * @param {Function} [reviver] + */ + _enlivenObjects: function (objects, callback, reviver) { + if (!objects || objects.length === 0) { + callback && callback([]); + return; + } + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, null, reviver); + }, + + /** + * @private + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); }, /** * Clones canvas instance + * @param {Object} [callback] Receives cloned instance as a first argument * @param {Array} [properties] Array of properties to include in the cloned canvas and children - * @returns {Promise} */ - clone: function (properties) { + clone: function (callback, properties) { var data = JSON.stringify(this.toJSON(properties)); - return this.cloneWithoutData().then(function(clone) { - return clone.loadFromJSON(data); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); }); }, @@ -14137,23 +14298,26 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Clones canvas instance without cloning existing data. * This essentially copies canvas dimensions, clipping properties, etc. * but leaves data empty (so that you can populate it with your own) - * @returns {Promise} + * @param {Object} [callback] Receives cloned instance as a first argument */ - cloneWithoutData: function() { + cloneWithoutData: function(callback) { var el = fabric.util.createCanvasElement(); el.width = this.width; el.height = this.height; - // this seems wrong. either Canvas or StaticCanvas + var clone = new fabric.Canvas(el); - var data = {}; if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.toObject(); + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; } - if (this.backgroundColor) { - data.background = this.backgroundColor.toObject ? this.backgroundColor.toObject() : this.backgroundColor; + else { + callback && callback(clone); } - return clone.loadFromJSON(data); } }); @@ -14887,17 +15051,17 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _getCacheCanvasDimensions: function() { var objectScale = this.getTotalObjectScaling(), // caculate dimensions without skewing - dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }), - neededX = dim.x * objectScale.x / this.scaleX, - neededY = dim.y * objectScale.y / this.scaleY; + dim = this._getTransformedDimensions(0, 0), + neededX = dim.x * objectScale.scaleX / this.scaleX, + neededY = dim.y * objectScale.scaleY / this.scaleY; return { // for sure this ALIASING_LIMIT is slightly creating problem // in situation in which the cache canvas gets an upper limit // also objectScale contains already scaleX and scaleY width: neededX + ALIASING_LIMIT, height: neededY + ALIASING_LIMIT, - zoomX: objectScale.x, - zoomY: objectScale.y, + zoomX: objectScale.scaleX, + zoomY: objectScale.scaleY, x: neededX, y: neededY }; @@ -14975,16 +15139,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ setOptions: function(options) { this._setOptions(options); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx - * @returns {boolean} true if object needs to fully transform ctx - */ - needsFullTransform: function (ctx) { - return (this.group && !this.group._transformDone) || - (this.group && this.canvas && ctx === this.canvas.contextTop); + this._initGradient(options.fill, 'fill'); + this._initGradient(options.stroke, 'stroke'); + this._initPattern(options.fill, 'fill'); + this._initPattern(options.stroke, 'stroke'); }, /** @@ -14992,7 +15150,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {CanvasRenderingContext2D} ctx Context */ transform: function(ctx) { - var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); + var needFullTransform = (this.group && !this.group._transformDone) || + (this.group && this.canvas && ctx === this.canvas.contextTop); + var m = this.calcTransformMatrix(!needFullTransform); ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); }, @@ -15067,17 +15227,20 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Object} object */ _removeDefaultValues: function(object) { - var prototype = fabric.util.getKlass(object.type).prototype; - Object.keys(object).forEach(function(prop) { - if (prop === 'left' || prop === 'top' || prop === 'type') { + var prototype = fabric.util.getKlass(object.type).prototype, + stateProperties = prototype.stateProperties; + stateProperties.forEach(function(prop) { + if (prop === 'left' || prop === 'top') { return; } if (object[prop] === prototype[prop]) { delete object[prop]; } + var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && + Object.prototype.toString.call(prototype[prop]) === '[object Array]'; + // basically a check for [] === [] - if (Array.isArray(object[prop]) && Array.isArray(prototype[prop]) - && object[prop].length === 0 && prototype[prop].length === 0) { + if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { delete object[prop]; } }); @@ -15095,7 +15258,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Return the object scale factor counting also the group scaling - * @return {fabric.Point} + * @return {Object} object with scaleX and scaleY properties */ getObjectScaling: function() { // if the object is a top level one, on the canvas, we go for simple aritmetic @@ -15103,11 +15266,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // and will likely kill the cache when not needed // https://github.com/fabricjs/fabric.js/issues/7157 if (!this.group) { - return new fabric.Point(Math.abs(this.scaleX), Math.abs(this.scaleY)); + return { + scaleX: this.scaleX, + scaleY: this.scaleY, + }; } // if we are inside a group total zoom calculation is complex, we defer to generic matrices var options = fabric.util.qrDecompose(this.calcTransformMatrix()); - return new fabric.Point(Math.abs(options.scaleX), Math.abs(options.scaleY)); + return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.scaleY) }; }, /** @@ -15115,13 +15281,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Object} object with scaleX and scaleY properties */ getTotalObjectScaling: function() { - var scale = this.getObjectScaling(); + var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; if (this.canvas) { var zoom = this.canvas.getZoom(); var retina = this.canvas.getRetinaScaling(); - scale.scalarMultiplyEquals(zoom * retina); + scaleX *= zoom * retina; + scaleY *= zoom * retina; } - return scale; + return { scaleX: scaleX, scaleY: scaleY }; }, /** @@ -15136,16 +15303,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return opacity; }, - /** - * Returns the object angle relative to canvas counting also the group property - * @returns {number} - */ - getTotalAngle: function () { - return this.group ? - fabric.util.qrDecompose(this.calcTransformMatrix()).angle : - this.angle; - }, - /** * @private * @param {String} key @@ -15189,6 +15346,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this; }, + /** + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. + */ + setOnGroup: function() { + // implemented by sub-classes, as needed. + }, + /** * Retrieves viewportTransform from Object's canvas if possible * @method getViewportTransform @@ -15249,7 +15416,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati renderCache: function(options) { options = options || {}; - if (!this._cacheCanvas || !this._cacheContext) { + if (!this._cacheCanvas) { this._createCacheCanvas(); } if (this.isCacheDirty()) { @@ -15264,7 +15431,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _removeCacheCanvas: function() { this._cacheCanvas = null; - this._cacheContext = null; this.cacheWidth = 0; this.cacheHeight = 0; }, @@ -15337,7 +15503,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * Check if this object or a child object will cast a shadow * used by Group.shouldCache to know if child has a shadow recursively * @return {Boolean} - * @deprecated */ willDrawShadow: function() { return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); @@ -15424,7 +15589,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (this.isNotVisible()) { return false; } - if (this._cacheCanvas && this._cacheContext && !skipCanvas && this._updateCacheCanvas()) { + if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { // in this case the context is already cleared. return true; } @@ -15433,7 +15598,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati (this.clipPath && this.clipPath.absolutePositioned) || (this.statefullCache && this.hasStateChanged('cacheProperties')) ) { - if (this._cacheCanvas && this._cacheContext && !skipCanvas) { + if (this._cacheCanvas && !skipCanvas) { var width = this.cacheWidth / this.zoomX; var height = this.cacheHeight / this.zoomY; this._cacheContext.clearRect(-width / 2, -height / 2, width, height); @@ -15589,19 +15754,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return; } - var shadow = this.shadow, canvas = this.canvas, + var shadow = this.shadow, canvas = this.canvas, scaling, multX = (canvas && canvas.viewportTransform[0]) || 1, - multY = (canvas && canvas.viewportTransform[3]) || 1, - scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); + multY = (canvas && canvas.viewportTransform[3]) || 1; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } if (canvas && canvas._isRetinaScaling()) { multX *= fabric.devicePixelRatio; multY *= fabric.devicePixelRatio; } ctx.shadowColor = shadow.color; ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * - (multX + multY) * (scaling.x + scaling.y) / 4; - ctx.shadowOffsetX = shadow.offsetX * multX * scaling.x; - ctx.shadowOffsetY = shadow.offsetY * multY * scaling.y; + (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; }, /** @@ -15704,9 +15874,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati } ctx.save(); - if (this.strokeUniform) { + if (this.strokeUniform && this.group) { var scaling = this.getObjectScaling(); - ctx.scale(1 / scaling.x, 1 / scaling.y); + ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); + } + else if (this.strokeUniform) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); } this._setLineDash(ctx, this.strokeDashArray); this._setStrokeStyles(ctx, this); @@ -15808,13 +15981,18 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Clones an instance. + * Clones an instance, using a callback method will work for every object. + * @param {Function} callback Callback is invoked with a clone as a first argument * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @returns {Promise} */ - clone: function(propertiesToInclude) { + clone: function(callback, propertiesToInclude) { var objectForm = this.toObject(propertiesToInclude); - return this.constructor.fromObject(objectForm); + if (this.constructor.fromObject) { + this.constructor.fromObject(objectForm, callback); + } + else { + fabric.Object._fromObject('Object', objectForm, callback); + } }, /** @@ -15824,6 +16002,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * and format option. toCanvasElement is faster and produce no loss of quality. * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. * toCanvasElement and then toBlob from the obtained canvas is also a good option. + * This method is sync now, but still support the callback because we did not want to break. + * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. + * @param {Function} callback callback, invoked with an instance as a first argument * @param {Object} [options] for clone as image, passed to toDataURL * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 @@ -15833,11 +16014,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 - * @return {fabric.Image} Object cloned as image. + * @return {fabric.Object} thisArg */ - cloneAsImage: function(options) { + cloneAsImage: function(callback, options) { var canvasEl = this.toCanvasElement(options); - return new fabric.Image(canvasEl); + if (callback) { + callback(new fabric.Image(canvasEl)); + } + return this; }, /** @@ -15859,8 +16043,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var utils = fabric.util, origParams = utils.saveObjectTransform(this), originalGroup = this.group, originalShadow = this.shadow, abs = Math.abs, - retinaScaling = options.enableRetinaScaling ? Math.max(fabric.devicePixelRatio, 1) : 1, - multiplier = (options.multiplier || 1) * retinaScaling; + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); delete this.group; if (options.withoutTransform) { utils.resetObjectTransform(this); @@ -15872,15 +16055,21 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var el = fabric.util.createCanvasElement(), // skip canvas zoom and calculate with setCoords now. boundingRect = this.getBoundingRect(true, true), - shadow = this.shadow, shadowOffset = { x: 0, y: 0 }, + shadow = this.shadow, scaling, + shadowOffset = { x: 0, y: 0 }, shadowBlur, width, height; if (shadow) { - var shadowBlur = shadow.blur; - var scaling = shadow.nonScaling ? new fabric.Point(1, 1) : this.getObjectScaling(); + shadowBlur = shadow.blur; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } // consider non scaling shadow. - shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.x)); - shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.y)); + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); } width = boundingRect.width + shadowOffset.x; height = boundingRect.height + shadowOffset.y; @@ -15943,7 +16132,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Boolean} */ isType: function(type) { - return arguments.length > 1 ? Array.from(arguments).includes(this.type) : this.type === type; + return this.type === type; }, /** @@ -16053,13 +16242,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * This callback function is called by the parent group of an object every - * time a non-delegated property changes on the group. It is passed the key - * and value as parameters. Not adding in this function's signature to avoid - * Travis build error about unused variables. + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) */ - setOnGroup: function() { - // implemented by sub-classes, as needed. + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var pClicked = new fabric.Point(pointer.x, pointer.y), + objectLeftTop = this._getLeftTopCoords(); + if (this.angle) { + pClicked = fabric.util.rotatePoint( + pClicked, objectLeftTop, degreesToRadians(-this.angle)); + } + return { + x: pClicked.x - objectLeftTop.x, + y: pClicked.y - objectLeftTop.y + }; }, /** @@ -16105,19 +16304,25 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @constant * @type string[] */ + fabric.Object.ENLIVEN_PROPS = ['clipPath']; - fabric.Object._fromObject = function(klass, object, extraParam) { - var serializedObject = clone(object, true); - return fabric.util.enlivenObjectEnlivables(serializedObject).then(function(enlivedMap) { - var newObject = Object.assign(object, enlivedMap); - return extraParam ? new klass(object[extraParam], newObject) : new klass(newObject); + fabric.Object._fromObject = function(className, object, callback, extraParam) { + var klass = fabric[className]; + object = clone(object, true); + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + if (typeof patterns[0] !== 'undefined') { + object.fill = patterns[0]; + } + if (typeof patterns[1] !== 'undefined') { + object.stroke = patterns[1]; + } + fabric.util.enlivenObjectEnlivables(object, object, function () { + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + }); }); }; - fabric.Object.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Object, object); - }; - /** * Unique id used internally when creating SVG elements * @static @@ -16142,70 +16347,71 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati bottom: 0.5 }; - /** - * @typedef {number | 'left' | 'center' | 'right'} OriginX - * @typedef {number | 'top' | 'center' | 'bottom'} OriginY - */ - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - /** - * Resolves origin value relative to center - * @private - * @param {OriginX} originX - * @returns number - */ - resolveOriginX: function (originX) { - return typeof originX === 'string' ? - originXOffset[originX] : - originX - 0.5; - }, - - /** - * Resolves origin value relative to center - * @private - * @param {OriginY} originY - * @returns number - */ - resolveOriginY: function (originY) { - return typeof originY === 'string' ? - originYOffset[originY] : - originY - 0.5; - }, - /** * Translates the coordinates from a set of origin to another (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {OriginX} fromOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} fromOriginY Vertical origin: 'top', 'center' or 'bottom' - * @param {OriginX} toOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { var x = point.x, y = point.y, - dim, - offsetX = this.resolveOriginX(toOriginX) - this.resolveOriginX(fromOriginX), - offsetY = this.resolveOriginY(toOriginY) - this.resolveOriginY(fromOriginY); + offsetX, offsetY, dim; - if (offsetX || offsetY) { - dim = this._getTransformedDimensions(); - x = point.x + offsetX * dim.x; - y = point.y + offsetY * dim.y; + if (typeof fromOriginX === 'string') { + fromOriginX = originXOffset[fromOriginX]; + } + else { + fromOriginX -= 0.5; } - return new fabric.Point(x, y); - }, + if (typeof toOriginX === 'string') { + toOriginX = originXOffset[toOriginX]; + } + else { + toOriginX -= 0.5; + } - /** - * Translates the coordinates from origin to center coordinates (based on the object's dimensions) - * @param {fabric.Point} point The point which corresponds to the originX and originY params - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {fabric.Point} - */ - translateToCenterPoint: function(point, originX, originY) { + offsetX = toOriginX - fromOriginX; + + if (typeof fromOriginY === 'string') { + fromOriginY = originYOffset[fromOriginY]; + } + else { + fromOriginY -= 0.5; + } + + if (typeof toOriginY === 'string') { + toOriginY = originYOffset[toOriginY]; + } + else { + toOriginY -= 0.5; + } + + offsetY = toOriginY - fromOriginY; + + if (offsetX || offsetY) { + dim = this._getTransformedDimensions(); + x = point.x + offsetX * dim.x; + y = point.y + offsetY * dim.y; + } + + return new fabric.Point(x, y); + }, + + /** + * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToCenterPoint: function(point, originX, originY) { var p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); if (this.angle) { return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle)); @@ -16216,8 +16422,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Translates the coordinates from center to origin coordinates (based on the object's dimensions) * @param {fabric.Point} center The point which corresponds to center of the object - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { @@ -16229,30 +16435,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Returns the center coordinates of the object relative to canvas + * Returns the real center coordinates of the object * @return {fabric.Point} */ getCenterPoint: function() { - var relCenter = this.getRelativeCenterPoint(); - return this.group ? - fabric.util.transformPoint(relCenter, this.group.calcTransformMatrix()) : - relCenter; - }, - - /** - * Returns the center coordinates of the object relative to it's containing group or null - * @return {fabric.Point|null} point or null of object has no parent group - */ - getCenterPointRelativeToParent: function () { - return this.group ? this.getRelativeCenterPoint() : null; - }, - - /** - * Returns the center coordinates of the object relative to it's parent - * @return {fabric.Point} - */ - getRelativeCenterPoint: function () { - return this.translateToCenterPoint(new fabric.Point(this.left, this.top), this.originX, this.originY); + var leftTop = new fabric.Point(this.left, this.top); + return this.translateToCenterPoint(leftTop, this.originX, this.originY); }, /** @@ -16266,24 +16454,26 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Returns the coordinates of the object as if it has a different origin - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ getPointByOrigin: function(originX, originY) { - var center = this.getRelativeCenterPoint(); + var center = this.getCenterPoint(); return this.translateToOriginPoint(center, originX, originY); }, /** - * Returns the normalized point (rotated relative to center) in local coordinates - * @param {fabric.Point} point The point relative to instance coordinate system - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * Returns the point in local coordinates + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ - normalizePoint: function(point, originX, originY) { - var center = this.getRelativeCenterPoint(), p, p2; + toLocalPoint: function(point, originX, originY) { + var center = this.getCenterPoint(), + p, p2; + if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); } @@ -16298,20 +16488,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return p2.subtractEquals(p); }, - /** - * Returns coordinates of a pointer relative to object's top left corner in object's plane - * @param {Event} e Event to operate upon - * @param {Object} [pointer] Pointer to operate upon (instead of event) - * @return {Object} Coordinates of a pointer (x, y) - */ - getLocalPointer: function (e, pointer) { - pointer = pointer || this.canvas.getPointer(e); - return fabric.util.transformPoint( - new fabric.Point(pointer.x, pointer.y), - fabric.util.invertTransform(this.calcTransformMatrix()) - ).addEquals(new fabric.Point(this.width / 2, this.height / 2)); - }, - /** * Returns the point in global coordinates * @param {fabric.Point} The point relative to the local coordinate system @@ -16324,8 +16500,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Sets the position of the object taking into consideration the object's origin * @param {fabric.Point} pos The new position of the object - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {void} */ setPositionByOrigin: function(pos, originX, originY) { @@ -16373,7 +16549,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this._originalOriginX = this.originX; this._originalOriginY = this.originY; - var center = this.getRelativeCenterPoint(); + var center = this.getCenterPoint(); this.originX = 'center'; this.originY = 'center'; @@ -16389,7 +16565,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _resetOrigin: function() { var originPoint = this.translateToOriginPoint( - this.getRelativeCenterPoint(), + this.getCenterPoint(), this._originalOriginX, this._originalOriginY); @@ -16407,7 +16583,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @private */ _getLeftTopCoords: function() { - return this.translateToOriginPoint(this.getRelativeCenterPoint(), 'left', 'top'); + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); }, }); @@ -16482,113 +16658,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ controls: { }, - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane - */ - getX: function () { - return this.getXY().x; - }, - - /** - * @param {number} value x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane - */ - setX: function (value) { - this.setXY(this.getXY().setX(value)); - }, - - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link fabric.Object#getX} - */ - getRelativeX: function () { - return this.left; - }, - - /** - * @param {number} value x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ - * if parent is canvas then this method is identical to {@link fabric.Object#setX} - */ - setRelativeX: function (value) { - this.left = value; - }, - - /** - * @returns {number} y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane - */ - getY: function () { - return this.getXY().y; - }, - - /** - * @param {number} value y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane - */ - setY: function (value) { - this.setXY(this.getXY().setY(value)); - }, - - /** - * @returns {number} y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link fabric.Object#getY} - */ - getRelativeY: function () { - return this.top; - }, - - /** - * @param {number} value y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link fabric.Object#setY} - */ - setRelativeY: function (value) { - this.top = value; - }, - - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in canvas coordinate plane - */ - getXY: function () { - var relativePosition = this.getRelativeXY(); - return this.group ? - fabric.util.transformPoint(relativePosition, this.group.calcTransformMatrix()) : - relativePosition; - }, - - /** - * Set an object position to a particular point, the point is intended in absolute ( canvas ) coordinate. - * You can specify {@link fabric.Object#originX} and {@link fabric.Object#originY} values, - * that otherwise are the object's current values. - * @example Set object's bottom left corner to point (5,5) on canvas - * object.setXY(new fabric.Point(5, 5), 'left', 'bottom'). - * @param {fabric.Point} point position in canvas coordinate plane - * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' - * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' - */ - setXY: function (point, originX, originY) { - if (this.group) { - point = fabric.util.transformPoint( - point, - fabric.util.invertTransform(this.group.calcTransformMatrix()) - ); - } - this.setRelativeXY(point, originX, originY); - }, - - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane - */ - getRelativeXY: function () { - return new fabric.Point(this.left, this.top); - }, - - /** - * As {@link fabric.Object#setXY}, but in current parent's coordinate plane ( the current group if any or the canvas) - * @param {fabric.Point} point position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane - * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' - * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' - */ - setRelativeXY: function (point, originX, originY) { - this.setPositionByOrigin(point, originX || this.originX, originY || this.originY); - }, - /** * return correct set of coordinates for intersection * this will return either aCoords or lineCoords. @@ -16611,15 +16680,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * The coords are returned in an array. * @return {Array} [tl, tr, br, bl] of points */ - getCoords: function (absolute, calculate) { - var coords = arrayFromCoords(this._getCoords(absolute, calculate)); - if (this.group) { - var t = this.group.calcTransformMatrix(); - return coords.map(function (p) { - return util.transformPoint(p, t); - }); - } - return coords; + getCoords: function(absolute, calculate) { + return arrayFromCoords(this._getCoords(absolute, calculate)); }, /** @@ -16961,7 +17023,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati calcLineCoords: function() { var vpt = this.getViewportTransform(), - padding = this.padding, angle = degreesToRadians(this.getTotalAngle()), + padding = this.padding, angle = degreesToRadians(this.angle), cos = util.cos(angle), sin = util.sin(angle), cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); @@ -16991,8 +17053,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var rotateMatrix = this._calcRotateMatrix(), translateMatrix = this._calcTranslateMatrix(), vpt = this.getViewportTransform(), - startMatrix = this.group ? multiplyMatrices(vpt, this.group.calcTransformMatrix()) : vpt, - startMatrix = multiplyMatrices(startMatrix, translateMatrix), + startMatrix = multiplyMatrices(vpt, translateMatrix), finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), dim = this._calculateCurrentDimensions(), @@ -17002,18 +17063,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }); // debug code - /* - var canvas = this.canvas; - setTimeout(function () { - if (!canvas) return; - canvas.contextTop.clearRect(0, 0, 700, 700); - canvas.contextTop.fillStyle = 'green'; - Object.keys(coords).forEach(function(key) { - var control = coords[key]; - canvas.contextTop.fillRect(control.x, control.y, 3, 3); - }); - }, 50); - */ + // var canvas = this.canvas; + // setTimeout(function() { + // canvas.contextTop.clearRect(0, 0, 700, 700); + // canvas.contextTop.fillStyle = 'green'; + // Object.keys(coords).forEach(function(key) { + // var control = coords[key]; + // canvas.contextTop.fillRect(control.x, control.y, 3, 3); + // }); + // }, 50); return coords; }, @@ -17070,7 +17128,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Array} rotation matrix for the object */ _calcTranslateMatrix: function() { - var center = this.getRelativeCenterPoint(); + var center = this.getCenterPoint(); return [1, 0, 0, 1, center.x, center.y]; }, @@ -17135,65 +17193,77 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return cache.value; }, - /** + /* * Calculate object dimensions from its properties * @private - * @returns {fabric.Point} dimensions + * @return {Object} .x width dimension + * @return {Object} .y height dimension */ _getNonTransformedDimensions: function() { - return new fabric.Point(this.width, this.height).scalarAddEquals(this.strokeWidth); + var strokeWidth = this.strokeWidth, + w = this.width + strokeWidth, + h = this.height + strokeWidth; + return { x: w, y: h }; }, - /** + /* * Calculate object bounding box dimensions from its properties scale, skew. - * @param {Object} [options] - * @param {Number} [options.scaleX] - * @param {Number} [options.scaleY] - * @param {Number} [options.skewX] - * @param {Number} [options.skewY] + * @param {Number} skewX, a value to override current skewX + * @param {Number} skewY, a value to override current skewY * @private - * @returns {fabric.Point} dimensions + * @return {Object} .x width dimension + * @return {Object} .y height dimension */ - _getTransformedDimensions: function (options) { - options = Object.assign({ - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: this.skewX, - skewY: this.skewY, - width: this.width, - height: this.height, - strokeWidth: this.strokeWidth - }, options || {}); - // stroke is applied before/after transformations are applied according to `strokeUniform` - var preScalingStrokeValue, postScalingStrokeValue, strokeWidth = options.strokeWidth; + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; + } + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions, dimX, dimY, + noSkew = skewX === 0 && skewY === 0; + if (this.strokeUniform) { - preScalingStrokeValue = 0; - postScalingStrokeValue = strokeWidth; + dimX = this.width; + dimY = this.height; } else { - preScalingStrokeValue = strokeWidth; - postScalingStrokeValue = 0; + dimensions = this._getNonTransformedDimensions(); + dimX = dimensions.x; + dimY = dimensions.y; } - var dimX = options.width + preScalingStrokeValue, - dimY = options.height + preScalingStrokeValue, - finalDimensions, - noSkew = options.skewX === 0 && options.skewY === 0; if (noSkew) { - finalDimensions = new fabric.Point(dimX * options.scaleX, dimY * options.scaleY); - } - else { - var bbox = util.sizeAfterTransform(dimX, dimY, options); - finalDimensions = new fabric.Point(bbox.x, bbox.y); + return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); } + var bbox = util.sizeAfterTransform(dimX, dimY, { + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: skewX, + skewY: skewY, + }); + return this._finalizeDimensions(bbox.x, bbox.y); + }, - return finalDimensions.scalarAddEquals(postScalingStrokeValue); + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param Number width width of the bbox + * @param Number height height of the bbox + * @private + * @return {Object} .x finalized width dimension + * @return {Object} .y finalized height dimension + */ + _finalizeDimensions: function(width, height) { + return this.strokeUniform ? + { x: width + this.strokeWidth, y: height + this.strokeWidth } + : + { x: width, y: height }; }, - /** + /* * Calculate object dimensions for controls box, including padding and canvas zoom. * and active selection - * @private - * @returns {fabric.Point} dimensions + * private */ _calculateCurrentDimensions: function() { var vpt = this.getViewportTransform(), @@ -17205,130 +17275,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati })(); -fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { - - /** - * Checks if object is decendant of target - * Should be used instead of @link {fabric.Collection.contains} for performance reasons - * @param {fabric.Object|fabric.StaticCanvas} target - * @returns {boolean} - */ - isDescendantOf: function (target) { - var parent = this.group || this.canvas; - while (parent) { - if (target === parent) { - return true; - } - else if (parent instanceof fabric.StaticCanvas) { - // happens after all parents were traversed through without a match - return false; - } - parent = parent.group || parent.canvas; - } - return false; - }, - - /** - * - * @typedef {fabric.Object[] | [...fabric.Object[], fabric.StaticCanvas]} Ancestors - * - * @param {boolean} [strict] returns only ancestors that are objects (without canvas) - * @returns {Ancestors} ancestors from bottom to top - */ - getAncestors: function (strict) { - var ancestors = []; - var parent = this.group || (strict ? undefined : this.canvas); - while (parent) { - ancestors.push(parent); - parent = parent.group || (strict ? undefined : parent.canvas); - } - return ancestors; - }, - - /** - * Returns an object that represent the ancestry situation. - * - * @typedef {object} AncestryComparison - * @property {Ancestors} common ancestors of `this` and `other` (may include `this` | `other`) - * @property {Ancestors} fork ancestors that are of `this` only - * @property {Ancestors} otherFork ancestors that are of `other` only - * - * @param {fabric.Object} other - * @param {boolean} [strict] finds only ancestors that are objects (without canvas) - * @returns {AncestryComparison | undefined} - * - */ - findCommonAncestors: function (other, strict) { - if (this === other) { - return { - fork: [], - otherFork: [], - common: [this].concat(this.getAncestors(strict)) - }; - } - else if (!other) { - // meh, warn and inform, and not my issue. - // the argument is NOT optional, we can't end up here. - return undefined; - } - var ancestors = this.getAncestors(strict); - var otherAncestors = other.getAncestors(strict); - // if `this` has no ancestors and `this` is top ancestor of `other` we must handle the following case - if (ancestors.length === 0 && otherAncestors.length > 0 && this === otherAncestors[otherAncestors.length - 1]) { - return { - fork: [], - otherFork: [other].concat(otherAncestors.slice(0, otherAncestors.length - 1)), - common: [this] - }; - } - // compare ancestors - for (var i = 0, ancestor; i < ancestors.length; i++) { - ancestor = ancestors[i]; - if (ancestor === other) { - return { - fork: [this].concat(ancestors.slice(0, i)), - otherFork: [], - common: ancestors.slice(i) - }; - } - for (var j = 0; j < otherAncestors.length; j++) { - if (this === otherAncestors[j]) { - return { - fork: [], - otherFork: [other].concat(otherAncestors.slice(0, j)), - common: [this].concat(ancestors) - }; - } - if (ancestor === otherAncestors[j]) { - return { - fork: [this].concat(ancestors.slice(0, i)), - otherFork: [other].concat(otherAncestors.slice(0, j)), - common: ancestors.slice(i) - }; - } - } - } - // nothing shared - return { - fork: [this].concat(ancestors), - otherFork: [other].concat(otherAncestors), - common: [] - }; - }, - - /** - * - * @param {fabric.Object} other - * @param {boolean} [strict] checks only ancestors that are objects (without canvas) - * @returns {boolean} - */ - hasCommonAncestors: function (other, strict) { - var commonAncestors = this.findCommonAncestors(other, strict); - return commonAncestors && !!commonAncestors.ancestors.length; - } -}); - - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** @@ -17407,36 +17353,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this.canvas.moveTo(this, index); } return this; - }, - - /** - * - * @param {fabric.Object} other object to compare against - * @returns {boolean | undefined} if objects do not share a common ancestor or they are strictly equal it is impossible to determine which is in front of the other; in such cases the function returns `undefined` - */ - isInFrontOf: function (other) { - if (this === other) { - return undefined; - } - var ancestorData = this.findCommonAncestors(other); - if (!ancestorData) { - return undefined; - } - if (ancestorData.fork.includes(other)) { - return true; - } - if (ancestorData.otherFork.includes(this)) { - return false; - } - var firstCommonAncestor = ancestorData.common[0]; - if (!firstCommonAncestor) { - return undefined; - } - var headOfFork = ancestorData.fork.pop(), - headOfOtherFork = ancestorData.otherFork.pop(), - thisIndex = firstCommonAncestor._objects.indexOf(headOfFork), - otherIndex = firstCommonAncestor._objects.indexOf(headOfOtherFork); - return thisIndex > -1 && thisIndex > otherIndex; } }); @@ -17822,10 +17738,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ _findTargetCorner: function(pointer, forTouch) { - if (!this.hasControls || (!this.canvas || this.canvas._activeObject !== this)) { + // objects in group, anykind, are not self modificable, + // must not return an hovered corner. + if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { return false; } - var xPoints, + + var ex = pointer.x, + ey = pointer.y, + xPoints, lines, keys = Object.keys(this.oCoords), j = keys.length - 1, i; this.__corner = 0; @@ -17852,7 +17773,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - xPoints = this._findCrossPoints(pointer, lines); + xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); if (xPoints !== 0 && xPoints % 2 === 1) { this.__corner = i; return i; @@ -17908,7 +17829,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; } ctx.save(); - var center = this.getRelativeCenterPoint(), wh = this._calculateCurrentDimensions(), + var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), vpt = this.canvas.viewportTransform; ctx.translate(center.x, center.y); ctx.scale(1 / vpt[0], 1 / vpt[3]); @@ -17935,7 +17856,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = wh.x + strokeWidth, height = wh.y + strokeWidth, hasControls = typeof styleOverride.hasControls !== 'undefined' ? - styleOverride.hasControls : this.hasControls; + styleOverride.hasControls : this.hasControls, + shouldStroke = false; ctx.save(); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17947,8 +17869,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); - hasControls && this.drawControlsConnectingLines(ctx); + if (hasControls) { + ctx.beginPath(); + this.forEachControl(function(control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + if (shouldStroke) { + ctx.stroke(); + } + } ctx.restore(); return this; }, @@ -17972,9 +17912,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width = bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, height = - bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor, - hasControls = typeof styleOverride.hasControls !== 'undefined' ? - styleOverride.hasControls : this.hasControls; + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; ctx.save(); this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); ctx.strokeStyle = styleOverride.borderColor || this.borderColor; @@ -17984,46 +17922,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot width, height ); - hasControls && this.drawControlsConnectingLines(ctx); ctx.restore(); return this; }, - /** - * Draws lines from a borders of an object's bounding box to controls that have `withConnection` property set. - * Requires public properties: width, height - * Requires public options: padding, borderColor - * @param {CanvasRenderingContext2D} ctx Context to draw on - * @return {fabric.Object} thisArg - * @chainable - */ - drawControlsConnectingLines: function (ctx) { - var wh = this._calculateCurrentDimensions(), - strokeWidth = this.borderScaleFactor, - width = wh.x + strokeWidth, - height = wh.y + strokeWidth, - shouldStroke = false; - - ctx.beginPath(); - this.forEachControl(function (control, key, fabricObject) { - // in this moment, the ctx is centered on the object. - // width and height of the above function are the size of the bbox. - if (control.withConnection && control.getVisibility(fabricObject, key)) { - // reset movement for each control - shouldStroke = true; - ctx.moveTo(control.x * width, control.y * height); - ctx.lineTo( - control.x * width + control.offsetX, - control.y * height + control.offsetY - ); - } - }); - shouldStroke && ctx.stroke(); - - return this; - }, - /** * Draws corners of an object's bounding box. * Requires public properties: width, height @@ -18036,7 +17939,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot drawControls: function(ctx, styleOverride) { styleOverride = styleOverride || {}; ctx.save(); - var retinaScaling = this.canvas.getRetinaScaling(), p; + var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; if (!this.transparentCorners) { @@ -18044,9 +17947,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); this.setCoords(); + if (this.group) { + // fabricJS does not really support drawing controls inside groups, + // this piece of code here helps having at least the control in places. + // If an application needs to show some objects as selected because of some UI state + // can still call Object._renderControls() on any object they desire, independently of groups. + // using no padding, circular controls and hiding the rotating cursor is higly suggested, + matrix = this.group.calcTransformMatrix(); + } this.forEachControl(function(control, key, fabricObject) { + p = fabricObject.oCoords[key]; if (control.getVisibility(fabricObject, key)) { - p = fabricObject.oCoords[key]; + if (matrix) { + p = fabric.util.transformPoint(p, matrix); + } control.render(ctx, p.x, p.y, styleOverride, fabricObject); } }); @@ -18155,11 +18069,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, - startValue: object.getX(), - endValue: this.getCenterPoint().x, + startValue: object.left, + endValue: this.getCenter().left, duration: this.FX_DURATION, onChange: function(value) { - object.setX(value); + object.set('left', value); _this.requestRenderAll(); onChange(); }, @@ -18188,11 +18102,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return fabric.util.animate({ target: this, - startValue: object.getY(), - endValue: this.getCenterPoint().y, + startValue: object.top, + endValue: this.getCenter().top, duration: this.FX_DURATION, onChange: function(value) { - object.setY(value); + object.set('top', value); _this.requestRenderAll(); onChange(); }, @@ -18647,15 +18561,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Line * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument */ - fabric.Line.fromObject = function(object) { + fabric.Line.fromObject = function(object, callback) { + function _callback(instance) { + delete instance.points; + callback && callback(instance); + }; var options = clone(object, true); options.points = [object.x1, object.y1, object.x2, object.y2]; - return fabric.Object._fromObject(fabric.Line, options, 'points').then(function(fabricLine) { - delete fabricLine.points; - return fabricLine; - }); + fabric.Object._fromObject('Line', options, _callback, 'points'); }; /** @@ -18888,10 +18803,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Circle * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument + * @return {void} */ - fabric.Circle.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Circle, object); + fabric.Circle.fromObject = function(object, callback) { + fabric.Object._fromObject('Circle', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -18983,10 +18899,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Triangle * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument */ - fabric.Triangle.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Triangle, object); + fabric.Triangle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Triangle', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19165,10 +19081,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Ellipse * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as first argument + * @return {void} */ - fabric.Ellipse.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Ellipse, object); + fabric.Ellipse.fromObject = function(object, callback) { + fabric.Object._fromObject('Ellipse', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19354,10 +19271,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Rect * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created */ - fabric.Rect.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Rect, object); + fabric.Rect.fromObject = function(object, callback) { + return fabric.Object._fromObject('Rect', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19448,7 +19365,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, _setPositionDimensions: function(options) { - options || (options = {}); var calcDim = this._calcDimensions(options), correctLeftTop, correctSize = this.exactBoundingBox ? this.strokeWidth : 0; this.width = calcDim.width - correctSize; @@ -19625,10 +19541,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polyline * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created */ - fabric.Polyline.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Polyline, object, 'points'); + fabric.Polyline.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polyline', object, callback, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19707,10 +19623,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polygon * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @return {void} */ - fabric.Polygon.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Polygon, object, 'points'); + fabric.Polygon.fromObject = function(object, callback) { + fabric.Object._fromObject('Polygon', object, callback, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -19725,6 +19642,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot max = fabric.util.array.max, extend = fabric.util.object.extend, clone = fabric.util.object.clone, + _toString = Object.prototype.toString, toFixed = fabric.util.toFixed; if (fabric.Path) { @@ -19778,8 +19696,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Object} [options] Options object */ _setPath: function (path, options) { + var fromArray = _toString.call(path) === '[object Array]'; + this.path = fabric.util.makePathSimpler( - Array.isArray(path) ? path : fabric.util.parsePath(path) + fromArray ? path : fabric.util.parsePath(path) ); fabric.Polyline.prototype._setPositionDimensions.call(this, options || {}); @@ -20050,10 +19970,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Path * @param {Object} object - * @returns {Promise} - */ - fabric.Path.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Path, object, 'path'); + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Path.fromObject = function(object, callback) { + if (typeof object.sourcePath === 'string') { + var pathUrl = object.sourcePath; + fabric.loadSVGFromURL(pathUrl, function (elements) { + var path = elements[0]; + path.setOptions(object); + callback && callback(path); + }); + } + else { + fabric.Object._fromObject('Path', object, callback, 'path'); + } }; /* _FROM_SVG_START_ */ @@ -20084,21 +20014,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot })(typeof exports !== 'undefined' ? exports : this); -(function (global) { +(function(global) { 'use strict'; - var fabric = global.fabric || (global.fabric = {}), - multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, - invertTransform = fabric.util.invertTransform, - transformPoint = fabric.util.transformPoint, - applyTransformToObject = fabric.util.applyTransformToObject, - degreesToRadians = fabric.util.degreesToRadians, - clone = fabric.util.object.clone, - extend = fabric.util.object.extend; + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max; if (fabric.Group) { - fabric.warn('fabric.Group is already defined'); return; } @@ -20107,351 +20031,280 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @class fabric.Group * @extends fabric.Object * @mixes fabric.Collection - * @fires layout once layout completes + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} * @see {@link fabric.Group#initialize} for constructor definition */ fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { /** * Type of an object - * @type string + * @type String * @default */ type: 'group', - /** - * Specifies the **layout strategy** for instance - * Used by `getLayoutStrategyResult` to calculate layout - * `fit-content`, `fit-content-lazy`, `fixed`, `clip-path` are supported out of the box - * @type string - * @default - */ - layout: 'fit-content', - /** * Width of stroke * @type Number + * @default */ strokeWidth: 0, /** - * List of properties to consider when checking if state - * of an object is changed (fabric.Object#hasStateChanged) - * as well as for history (undo/redo) purposes - * @type string[] - */ - stateProperties: fabric.Object.prototype.stateProperties.concat('layout'), - - /** - * Used to optimize performance - * set to `false` if you don't need contained objects to be targets of events - * @default - * @type boolean + * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets + * @type Boolean + * @default */ subTargetCheck: false, /** - * Used to allow targeting of object inside groups. - * set to true if you want to select an object inside a group.\ - * **REQUIRES** `subTargetCheck` set to true + * Groups are container, do not render anything on theyr own, ence no cache properties + * @type Array * @default - * @type boolean */ - interactive: false, + cacheProperties: [], /** - * Used internally to optimize performance - * Once an object is selected, instance is rendered without the selected object. - * This way instance is cached only once for the entire interaction with the selected object. - * @private + * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still + * available setting this boolean to true. + * @type Boolean + * @since 2.0.0 + * @default */ - _activeObjects: undefined, + useSetOnGroup: false, /** * Constructor - * - * @param {fabric.Object[]} [objects] instance objects + * @param {Object} objects Group objects * @param {Object} [options] Options object - * @param {boolean} [objectsRelativeToGroup] true if objects exist in group coordinate plane - * @return {fabric.Group} thisArg + * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. + * @return {Object} thisArg */ - initialize: function (objects, options, objectsRelativeToGroup) { + initialize: function(objects, options, isAlreadyGrouped) { + options = options || {}; + this._objects = []; + // if objects enclosed in a group have been grouped already, + // we cannot change properties of objects. + // Thus we need to set options to group without objects, + isAlreadyGrouped && this.callSuper('initialize', options); this._objects = objects || []; - this._activeObjects = []; - this.__objectMonitor = this.__objectMonitor.bind(this); - this.__objectSelectionTracker = this.__objectSelectionMonitor.bind(this, true); - this.__objectSelectionDisposer = this.__objectSelectionMonitor.bind(this, false); - this._firstLayoutDone = false; - // setting angle, skewX, skewY must occur after initial layout - this.callSuper('initialize', Object.assign({}, options, { angle: 0, skewX: 0, skewY: 0 })); - this.forEachObject(function (object) { - this.enterGroup(object, false); - object.fire('added:initialized', { target: this }); - }, this); - this._applyLayoutStrategy({ - type: 'initialization', - options: options, - objectsRelativeToGroup: objectsRelativeToGroup - }); - }, - - /** - * @private - * @param {string} key - * @param {*} value - */ - _set: function (key, value) { - var prev = this[key]; - this.callSuper('_set', key, value); - if (key === 'canvas' && prev !== value) { - this.forEachObject(function (object) { - object._set(key, value); - }); + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; } - if (key === 'layout' && prev !== value) { - this._applyLayoutStrategy({ type: 'layout_change', layout: value, prevLayout: prev }); + + if (!isAlreadyGrouped) { + var center = options && options.centerPoint; + // we want to set origins before calculating the bounding box. + // so that the topleft can be set with that in mind. + // if specific top and left are passed, are overwritten later + // with the callSuper('initialize', options) + if (options.originX !== undefined) { + this.originX = options.originX; + } + if (options.originY !== undefined) { + this.originY = options.originY; + } + // if coming from svg i do not want to calc bounds. + // i assume width and height are passed along options + center || this._calcBounds(); + this._updateObjectsCoords(center); + delete options.centerPoint; + this.callSuper('initialize', options); } - if (key === 'interactive') { - this.forEachObject(this._watchObject.bind(this, value)); + else { + this._updateObjectsACoords(); } - return this; - }, - - /** - * @private - */ - _shouldSetNestedCoords: function () { - return this.subTargetCheck; - }, - /** - * Add objects - * @param {...fabric.Object} objects - */ - add: function () { - fabric.Collection.add.call(this, arguments, this._onObjectAdded); - this._onAfterObjectsChange('added', Array.from(arguments)); - }, - - /** - * Inserts an object into collection at specified index - * @param {fabric.Object} objects Object to insert - * @param {Number} index Index to insert object at - */ - insertAt: function (objects, index) { - fabric.Collection.insertAt.call(this, objects, index, this._onObjectAdded); - this._onAfterObjectsChange('added', Array.isArray(objects) ? objects : [objects]); - }, - - /** - * Remove objects - * @param {...fabric.Object} objects - * @returns {fabric.Object[]} removed objects - */ - remove: function () { - var removed = fabric.Collection.remove.call(this, arguments, this._onObjectRemoved); - this._onAfterObjectsChange('removed', removed); - return removed; + this.setCoords(); }, /** - * Remove all objects - * @returns {fabric.Object[]} removed objects + * @private */ - removeAll: function () { - this._activeObjects = []; - return this.remove.apply(this, this._objects.slice()); + _updateObjectsACoords: function() { + var skipControls = true; + for (var i = this._objects.length; i--; ){ + this._objects[i].setCoords(skipControls); + } }, /** - * invalidates layout on object modified * @private + * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change */ - __objectMonitor: function (opt) { - this._applyLayoutStrategy(extend(clone(opt), { - type: 'object_modified' - })); - this._set('dirty', true); + _updateObjectsCoords: function(center) { + var center = center || this.getCenterPoint(); + for (var i = this._objects.length; i--; ){ + this._updateObjectCoords(this._objects[i], center); + } }, /** - * keeps track of the selected objects * @private + * @param {Object} object + * @param {fabric.Point} center, current center of group. */ - __objectSelectionMonitor: function (selected, opt) { - var object = opt.target; - if (selected) { - this._activeObjects.push(object); - this._set('dirty', true); - } - else if (this._activeObjects.length > 0) { - var index = this._activeObjects.indexOf(object); - if (index > -1) { - this._activeObjects.splice(index, 1); - this._set('dirty', true); - } - } + _updateObjectCoords: function(object, center) { + var objectLeft = object.left, + objectTop = object.top, + skipControls = true; + + object.set({ + left: objectLeft - center.x, + top: objectTop - center.y + }); + object.group = this; + object.setCoords(skipControls); }, /** - * @private - * @param {boolean} watch - * @param {fabric.Object} object + * Returns string represenation of a group + * @return {String} */ - _watchObject: function (watch, object) { - var directive = watch ? 'on' : 'off'; - // make sure we listen only once - watch && this._watchObject(false, object); - object[directive]('changed', this.__objectMonitor); - object[directive]('modified', this.__objectMonitor); - object[directive]('selected', this.__objectSelectionTracker); - object[directive]('deselected', this.__objectSelectionDisposer); + toString: function() { + return '#'; }, /** - * Checks if object can enter group and logs relevant warnings - * @private - * @param {fabric.Object} object - * @returns + * Adds an object to a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable */ - canEnter: function (object) { - if (object === this || this.isDescendantOf(object)) { - /* _DEV_MODE_START_ */ - console.warn('fabric.Group: trying to add group to itself, this call has no effect'); - /* _DEV_MODE_END_ */ - return false; + addWithUpdate: function(object) { + var nested = !!this.group; + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + if (object) { + if (nested) { + // if this group is inside another group, we need to pre transform the object + fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); + } + this._objects.push(object); + object.group = this; + object._set('canvas', this.canvas); } - else if (object.group && object.group === this) { - /* _DEV_MODE_START_ */ - console.warn('fabric.Group: duplicate objects are not supported inside group, this call has no effect'); - /* _DEV_MODE_END_ */ - return false; + this._calcBounds(); + this._updateObjectsCoords(); + this.dirty = true; + if (nested) { + this.group.addWithUpdate(); } - else if (object.group) { - /* _DEV_MODE_START_ */ - console.warn('fabric.Group: object is about to enter group and leave another'); - /* _DEV_MODE_END_ */ + else { + this.setCoords(); } - return true; + return this; }, /** - * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane - * @returns {boolean} true if object entered group + * Removes an object from a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable */ - enterGroup: function (object, removeParentTransform) { - if (!this.canEnter(object)) { - return false; - } - if (object.group) { - object.group.remove(object); - } - this._enterGroup(object, removeParentTransform); - return true; + removeWithUpdate: function(object) { + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + + this.remove(object); + this._calcBounds(); + this._updateObjectsCoords(); + this.setCoords(); + this.dirty = true; + return this; }, /** * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane - */ - _enterGroup: function (object, removeParentTransform) { - if (removeParentTransform) { - // can this be converted to utils (sendObjectToPlane)? - applyTransformToObject( - object, - multiplyTransformMatrices( - invertTransform(this.calcTransformMatrix()), - object.calcTransformMatrix() - ) - ); - } - this._shouldSetNestedCoords() && object.setCoords(); - object._set('group', this); + */ + _onObjectAdded: function(object) { + this.dirty = true; + object.group = this; object._set('canvas', this.canvas); - this.interactive && this._watchObject(true, object); - var activeObject = this.canvas && this.canvas.getActiveObject && this.canvas.getActiveObject(); - // if we are adding the activeObject in a group - if (activeObject && (activeObject === object || object.isDescendantOf(activeObject))) { - this._activeObjects.push(object); - } }, /** * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it */ - exitGroup: function (object, removeParentTransform) { - this._exitGroup(object, removeParentTransform); - object._set('canvas', undefined); + _onObjectRemoved: function(object) { + this.dirty = true; + delete object.group; }, /** * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it - */ - _exitGroup: function (object, removeParentTransform) { - object._set('group', undefined); - if (!removeParentTransform) { - applyTransformToObject( - object, - multiplyTransformMatrices( - this.calcTransformMatrix(), - object.calcTransformMatrix() - ) - ); - object.setCoords(); + */ + _set: function(key, value) { + var i = this._objects.length; + if (this.useSetOnGroup) { + while (i--) { + this._objects[i].setOnGroup(key, value); + } } - this._watchObject(false, object); - var index = this._activeObjects.length > 0 ? this._activeObjects.indexOf(object) : -1; - if (index > -1) { - this._activeObjects.splice(index, 1); + if (key === 'canvas') { + while (i--) { + this._objects[i]._set(key, value); + } } + fabric.Object.prototype._set.call(this, key, value); }, /** - * @private - * @param {'added'|'removed'} type - * @param {fabric.Object[]} targets - */ - _onAfterObjectsChange: function (type, targets) { - this._applyLayoutStrategy({ - type: type, - targets: targets - }); - this._set('dirty', true); - }, - - /** - * @private - * @param {fabric.Object} object + * Returns 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 */ - _onObjectAdded: function (object) { - this.enterGroup(object, true); - object.fire('added', { target: this }); + toObject: function(propertiesToInclude) { + var _includeDefaultValues = this.includeDefaultValues; + var objsToObject = this._objects + .filter(function (obj) { + return !obj.excludeFromExport; + }) + .map(function (obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; }, /** - * @private - * @param {fabric.Object} object + * Returns object representation of an instance, in dataless mode. + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance */ - _onRelativeObjectAdded: function (object) { - this.enterGroup(object, false); - object.fire('added', { target: this }); + toDatalessObject: function(propertiesToInclude) { + var objsToObject, sourcePath = this.sourcePath; + if (sourcePath) { + objsToObject = sourcePath; + } + else { + var _includeDefaultValues = this.includeDefaultValues; + objsToObject = this._objects.map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toDatalessObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + } + var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; }, /** - * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on */ - _onObjectRemoved: function (object, removeParentTransform) { - this.exitGroup(object, removeParentTransform); - object.fire('removed', { target: this }); + render: function(ctx) { + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; }, /** @@ -20464,7 +20317,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot shouldCache: function() { var ownCache = fabric.Object.prototype.shouldCache.call(this); if (ownCache) { - for (var i = 0; i < this._objects.length; i++) { + for (var i = 0, len = this._objects.length; i < len; i++) { if (this._objects[i].willDrawShadow()) { this.ownCaching = false; return false; @@ -20482,7 +20335,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (fabric.Object.prototype.willDrawShadow.call(this)) { return true; } - for (var i = 0; i < this._objects.length; i++) { + for (var i = 0, len = this._objects.length; i < len; i++) { if (this._objects[i].willDrawShadow()) { return true; } @@ -20491,11 +20344,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, /** - * Check if instance or its group are caching, recursively up + * Check if this group or its parent group are caching, recursively up * @return {Boolean} */ - isOnACache: function () { - return this.ownCaching || (!!this.group && this.group.isOnACache()); + isOnACache: function() { + return this.ownCaching || (this.group && this.group.isOnACache()); }, /** @@ -20503,8 +20356,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {CanvasRenderingContext2D} ctx Context to render on */ drawObject: function(ctx) { - this._renderBackground(ctx); - for (var i = 0; i < this._objects.length; i++) { + for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i].render(ctx); } this._drawClipPath(ctx, this.clipPath); @@ -20520,7 +20372,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (!this.statefullCache) { return false; } - for (var i = 0; i < this._objects.length; i++) { + for (var i = 0, len = this._objects.length; i < len; i++) { if (this._objects[i].isCacheDirty(true)) { if (this._cacheCanvas) { // if this group has not a cache canvas there is nothing to clean @@ -20530,836 +20382,220 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return true; } } - return false; - }, - - /** - * @override - * @return {Boolean} - */ - setCoords: function () { - this.callSuper('setCoords'); - this._shouldSetNestedCoords() && this.forEachObject(function (object) { - object.setCoords(); - }); - }, - - /** - * Renders instance on a given context - * @param {CanvasRenderingContext2D} ctx context to render instance on - */ - render: function (ctx) { - // used to inform objects not to double opacity - this._transformDone = true; - this.callSuper('render', ctx); - this._transformDone = false; - }, - - /** - * @public - * @param {Partial & { layout?: string }} [context] pass values to use for layout calculations - */ - triggerLayout: function (context) { - if (context && context.layout) { - context.prevLayout = this.layout; - this.layout = context.layout; - } - this._applyLayoutStrategy({ type: 'imperative', context: context }); - }, - - /** - * @private - * @param {fabric.Object} object - * @param {fabric.Point} diff - * @param {boolean} [setCoords] perf enhancement, instead of iterating over objects again - */ - _adjustObjectPosition: function (object, diff, setCoords) { - // layer doesn't need coords so we don't set them - if (object instanceof fabric.Layer) { - object.forEachObject(function (obj) { - this._adjustObjectPosition(obj, diff, setCoords); - }.bind(this)); - } - else { - object.set({ - left: object.left + diff.x, - top: object.top + diff.y, - }); - setCoords && object.setCoords(); - } - }, - - /** - * initial layout logic: - * calculate bbox of objects (if necessary) and translate it according to options received from the constructor (left, top, width, height) - * so it is placed in the center of the bbox received from the constructor - * - * @private - * @param {LayoutContext} context - */ - _applyLayoutStrategy: function (context) { - var isFirstLayout = context.type === 'initialization'; - if (!isFirstLayout && !this._firstLayoutDone) { - // reject layout requests before initialization layout - return; - } - var options = isFirstLayout && context.options; - var initialTransform = options && { - angle: options.angle || 0, - skewX: options.skewX || 0, - skewY: options.skewY || 0, - }; - var center = this.getRelativeCenterPoint(); - var result = this.getLayoutStrategyResult(this.layout, this._objects.concat(), context); - if (result) { - // handle positioning - var newCenter = new fabric.Point(result.centerX, result.centerY); - var vector = center.subtract(newCenter).add(new fabric.Point(result.correctionX || 0, result.correctionY || 0)); - var diff = transformPoint(vector, invertTransform(this.calcOwnMatrix()), true); - var objectsSetCoords = false; - // set dimensions - this.set({ width: result.width, height: result.height }); - if (!newCenter.eq(center) || initialTransform) { - // set position - this.setPositionByOrigin(newCenter, 'center', 'center'); - initialTransform && this.set(initialTransform); - // perf: avoid iterating over objects twice by setting coords only on instance - // and delegating the task to `_adjustObjectPosition` - this.callSuper('setCoords'); - objectsSetCoords = this.subTargetCheck; - } - // adjust objects to account for new center - !context.objectsRelativeToGroup && this.forEachObject(function (object) { - this._adjustObjectPosition(object, diff, objectsSetCoords); - }, this); - // clip path as well - !isFirstLayout && this.layout !== 'clip-path' && this.clipPath && !this.clipPath.absolutePositioned - && this._adjustObjectPosition(this.clipPath, diff, objectsSetCoords); - } - else if (isFirstLayout) { - // fill `result` with initial values for the layout hook - result = { - centerX: center.x, - centerY: center.y, - width: this.width, - height: this.height, - }; - initialTransform && this.set(initialTransform); - } - else { - // no `result` so we return - return; - } - // flag for next layouts - this._firstLayoutDone = true; - // fire layout hook and event (event will fire only for layouts after initialization layout) - this.onLayout(context, result); - this.fire('layout', { - context: context, - result: result, - diff: diff - }); - this._bubbleLayout(context); - }, - - - /** - * bubble layout recursive up - * @private - */ - _bubbleLayout: function (context) { - if (this.group && this.group._applyLayoutStrategy) { - // append the path recursion to context - if (!context.path) { - context.path = []; - } - context.path.push(this); - // all parents should invalidate their layout - this.group._applyLayoutStrategy(context); - } - }, - - /** - * Override this method to customize layout. - * If you need to run logic once layout completes use `onLayout` - * @public - * - * @typedef {'initialization'|'object_modified'|'added'|'removed'|'layout_change'|'imperative'} LayoutContextType - * - * @typedef LayoutContext context object with data regarding what triggered the call - * @property {LayoutContextType} type - * @property {fabric.Object[]} [path] array of objects starting from the object that triggered the call to the current one - * - * @typedef LayoutResult positioning and layout data **relative** to instance's parent - * @property {number} centerX new centerX as measured by the containing plane (same as `left` with `originX` set to `center`) - * @property {number} centerY new centerY as measured by the containing plane (same as `top` with `originY` set to `center`) - * @property {number} [correctionX] correctionX to translate objects by, measured as `centerX` - * @property {number} [correctionY] correctionY to translate objects by, measured as `centerY` - * @property {number} width - * @property {number} height - * - * @param {string} layoutDirective - * @param {fabric.Object[]} objects - * @param {LayoutContext} context - * @returns {LayoutResult | undefined} - */ - getLayoutStrategyResult: function (layoutDirective, objects, context) { // eslint-disable-line no-unused-vars - // `fit-content-lazy` performance enhancement - // skip if instance had no objects before the `added` event because it may have kept layout after removing all previous objects - if (layoutDirective === 'fit-content-lazy' - && context.type === 'added' && objects.length > context.targets.length) { - // calculate added objects' bbox with existing bbox - var addedObjects = context.targets.concat(this); - return this.prepareBoundingBox(layoutDirective, addedObjects, context); - } - else if (layoutDirective === 'fit-content' || layoutDirective === 'fit-content-lazy' - || (layoutDirective === 'fixed' && context.type === 'initialization')) { - return this.prepareBoundingBox(layoutDirective, objects, context); - } - else if (layoutDirective === 'clip-path' && this.clipPath) { - var clipPath = this.clipPath; - var clipPathSizeAfter = clipPath._getTransformedDimensions(); - if (clipPath.absolutePositioned && (context.type === 'initialization' || context.type === 'layout_change')) { - // we want the center point to exist in group's containing plane - var clipPathCenter = clipPath.getCenterPoint(); - if (this.group) { - // send point from canvas plane to group's containing plane - var inv = invertTransform(this.group.calcTransformMatrix()); - clipPathCenter = transformPoint(clipPathCenter, inv); - } - return { - centerX: clipPathCenter.x, - centerY: clipPathCenter.y, - width: clipPathSizeAfter.x, - height: clipPathSizeAfter.y, - }; - } - else if (!clipPath.absolutePositioned) { - var center; - var clipPathRelativeCenter = clipPath.getRelativeCenterPoint(), - // we want the center point to exist in group's containing plane, so we send it upwards - clipPathCenter = transformPoint(clipPathRelativeCenter, this.calcOwnMatrix(), true); - if (context.type === 'initialization' || context.type === 'layout_change') { - var bbox = this.prepareBoundingBox(layoutDirective, objects, context) || {}; - center = new fabric.Point(bbox.centerX || 0, bbox.centerY || 0); - return { - centerX: center.x + clipPathCenter.x, - centerY: center.y + clipPathCenter.y, - correctionX: bbox.correctionX - clipPathCenter.x, - correctionY: bbox.correctionY - clipPathCenter.y, - width: clipPath.width, - height: clipPath.height, - }; - } - else { - center = this.getRelativeCenterPoint(); - return { - centerX: center.x + clipPathCenter.x, - centerY: center.y + clipPathCenter.y, - width: clipPathSizeAfter.x, - height: clipPathSizeAfter.y, - }; - } - } - } - else if (layoutDirective === 'svg' && context.type === 'initialization') { - var bbox = this.getObjectsBoundingBox(objects, true) || {}; - return Object.assign(bbox, { - correctionX: -bbox.offsetX || 0, - correctionY: -bbox.offsetY || 0, - }); - } - }, - - /** - * Override this method to customize layout. - * A wrapper around {@link fabric.Group#getObjectsBoundingBox} - * @public - * @param {string} layoutDirective - * @param {fabric.Object[]} objects - * @param {LayoutContext} context - * @returns {LayoutResult | undefined} - */ - prepareBoundingBox: function (layoutDirective, objects, context) { - if (context.type === 'initialization') { - return this.prepareInitialBoundingBox(layoutDirective, objects, context); - } - else if (context.type === 'imperative' && context.context) { - return Object.assign( - this.getObjectsBoundingBox(objects) || {}, - context.context - ); - } - else { - return this.getObjectsBoundingBox(objects); - } - }, - - /** - * Calculates center taking into account originX, originY while not being sure that width/height are initialized - * @public - * @param {string} layoutDirective - * @param {fabric.Object[]} objects - * @param {LayoutContext} context - * @returns {LayoutResult | undefined} - */ - prepareInitialBoundingBox: function (layoutDirective, objects, context) { - var options = context.options || {}, - hasX = typeof options.left === 'number', - hasY = typeof options.top === 'number', - hasWidth = typeof options.width === 'number', - hasHeight = typeof options.height === 'number'; - - // performance enhancement - // skip layout calculation if bbox is defined - if ((hasX && hasY && hasWidth && hasHeight && context.objectsRelativeToGroup) || objects.length === 0) { - // return nothing to skip layout - return; - } - - var bbox = this.getObjectsBoundingBox(objects) || {}; - var width = hasWidth ? this.width : (bbox.width || 0), - height = hasHeight ? this.height : (bbox.height || 0), - calculatedCenter = new fabric.Point(bbox.centerX || 0, bbox.centerY || 0), - origin = new fabric.Point(this.resolveOriginX(this.originX), this.resolveOriginY(this.originY)), - size = new fabric.Point(width, height), - strokeWidthVector = this._getTransformedDimensions({ width: 0, height: 0 }), - sizeAfter = this._getTransformedDimensions({ - width: width, - height: height, - strokeWidth: 0 - }), - bboxSizeAfter = this._getTransformedDimensions({ - width: bbox.width, - height: bbox.height, - strokeWidth: 0 - }), - rotationCorrection = new fabric.Point(0, 0); - - if (this.angle) { - var rad = degreesToRadians(this.angle), - sin = Math.abs(fabric.util.sin(rad)), - cos = Math.abs(fabric.util.cos(rad)); - sizeAfter.setXY( - sizeAfter.x * cos + sizeAfter.y * sin, - sizeAfter.x * sin + sizeAfter.y * cos - ); - bboxSizeAfter.setXY( - bboxSizeAfter.x * cos + bboxSizeAfter.y * sin, - bboxSizeAfter.x * sin + bboxSizeAfter.y * cos - ); - strokeWidthVector = fabric.util.rotateVector(strokeWidthVector, rad); - // correct center after rotating - var strokeCorrection = strokeWidthVector.multiply(origin.scalarAdd(-0.5).scalarDivide(-2)); - rotationCorrection = sizeAfter.subtract(size).scalarDivide(2).add(strokeCorrection); - calculatedCenter.addEquals(rotationCorrection); - } - // calculate center and correction - var originT = origin.scalarAdd(0.5); - var originCorrection = sizeAfter.multiply(originT); - var centerCorrection = new fabric.Point( - hasWidth ? bboxSizeAfter.x / 2 : originCorrection.x, - hasHeight ? bboxSizeAfter.y / 2 : originCorrection.y - ); - var center = new fabric.Point( - hasX ? this.left - (sizeAfter.x + strokeWidthVector.x) * origin.x : calculatedCenter.x - centerCorrection.x, - hasY ? this.top - (sizeAfter.y + strokeWidthVector.y) * origin.y : calculatedCenter.y - centerCorrection.y - ); - var offsetCorrection = new fabric.Point( - hasX ? - center.x - calculatedCenter.x + bboxSizeAfter.x * (hasWidth ? 0.5 : 0) : - -(hasWidth ? (sizeAfter.x - strokeWidthVector.x) * 0.5 : sizeAfter.x * originT.x), - hasY ? - center.y - calculatedCenter.y + bboxSizeAfter.y * (hasHeight ? 0.5 : 0) : - -(hasHeight ? (sizeAfter.y - strokeWidthVector.y) * 0.5 : sizeAfter.y * originT.y) - ).add(rotationCorrection); - var correction = new fabric.Point( - hasWidth ? -sizeAfter.x / 2 : 0, - hasHeight ? -sizeAfter.y / 2 : 0 - ).add(offsetCorrection); - - return { - centerX: center.x, - centerY: center.y, - correctionX: correction.x, - correctionY: correction.y, - width: size.x, - height: size.y, - }; - }, - - /** - * Calculate the bbox of objects relative to instance's containing plane - * @public - * @param {fabric.Object[]} objects - * @returns {LayoutResult | null} bounding box - */ - getObjectsBoundingBox: function (objects, ignoreOffset) { - if (objects.length === 0) { - return null; - } - var objCenter, sizeVector, min = new fabric.Point(0, 0), max = new fabric.Point(0, 0), a, b; - objects.forEach(function (object, i) { - if (object instanceof fabric.Layer) { - var bbox = object.getObjectsBoundingBox(object._objects.slice(0)); - if (!bbox) { - return; - } - sizeVector = object._getTransformedDimensions({ - width: bbox.width, - height: bbox.height - }).scalarDivideEquals(2); - objCenter = new fabric.Point(bbox.centerX, bbox.centerY); - } - else { - sizeVector = object._getTransformedDimensions().scalarDivideEquals(2); - objCenter = object.getRelativeCenterPoint(); - } - if (object.angle) { - var rad = degreesToRadians(object.angle), - sin = Math.abs(fabric.util.sin(rad)), - cos = Math.abs(fabric.util.cos(rad)), - rx = sizeVector.x * cos + sizeVector.y * sin, - ry = sizeVector.x * sin + sizeVector.y * cos; - sizeVector = new fabric.Point(rx, ry); - } - a = objCenter.subtract(sizeVector); - b = objCenter.add(sizeVector); - if (i === 0) { - min = new fabric.Point(Math.min(a.x, b.x), Math.min(a.y, b.y)); - max = new fabric.Point(Math.max(a.x, b.x), Math.max(a.y, b.y)); - } - else { - min.setXY(Math.min(min.x, a.x, b.x), Math.min(min.y, a.y, b.y)); - max.setXY(Math.max(max.x, a.x, b.x), Math.max(max.y, a.y, b.y)); - } - }); - - var size = max.subtract(min), - relativeCenter = ignoreOffset ? size.scalarDivide(2) : min.midPointFrom(max), - // we send `relativeCenter` up to group's containing plane - offset = transformPoint(min, this.calcOwnMatrix()), - center = transformPoint(relativeCenter, this.calcOwnMatrix()); - - return { - offsetX: offset.x, - offsetY: offset.y, - centerX: center.x, - centerY: center.y, - width: size.x, - height: size.y, - }; - }, - - /** - * Hook that is called once layout has completed. - * Provided for layout customization, override if necessary. - * Complements `getLayoutStrategyResult`, which is called at the beginning of layout. - * @public - * @param {LayoutContext} context layout context - * @param {LayoutResult} result layout result - */ - onLayout: function (/* context, result */) { - // override by subclass - }, - - /** - * - * @private - * @param {'toObject'|'toDatalessObject'} [method] - * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @returns {fabric.Object[]} serialized objects - */ - __serializeObjects: function (method, propertiesToInclude) { - var _includeDefaultValues = this.includeDefaultValues; - return this._objects - .filter(function (obj) { - return !obj.excludeFromExport; - }) - .map(function (obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = _includeDefaultValues; - var data = obj[method || 'toObject'](propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - //delete data.version; - return data; - }); - }, - - /** - * Returns object representation of an instance - * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - var obj = this.callSuper('toObject', ['layout', 'subTargetCheck', 'interactive'].concat(propertiesToInclude)); - obj.objects = this.__serializeObjects('toObject', propertiesToInclude); - return obj; - }, - - toString: function () { - return '#'; - }, - - dispose: function () { - this._activeObjects = []; - this.forEachObject(function (object) { - this._watchObject(false, object); - object.dispose && object.dispose(); - }, this); - }, - - /* _TO_SVG_START_ */ - - /** - * @private - */ - _createSVGBgRect: function (reviver) { - if (!this.backgroundColor) { - return ''; - } - var fillStroke = fabric.Rect.prototype._toSVG.call(this, reviver); - var commons = fillStroke.indexOf('COMMON_PARTS'); - fillStroke[commons] = 'for="group" '; - return fillStroke.join(''); - }, - - /** - * Returns svg representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - _toSVG: function (reviver) { - var svgString = ['\n']; - var bg = this._createSVGBgRect(reviver); - bg && svgString.push('\t\t', bg); - for (var i = 0; i < this._objects.length; i++) { - svgString.push('\t\t', this._objects[i].toSVG(reviver)); - } - svgString.push('\n'); - return svgString; - }, - - /** - * Returns styles-string for svg-export, specific version for group - * @return {String} - */ - getSvgStyles: function() { - var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? - 'opacity: ' + this.opacity + ';' : '', - visibility = this.visible ? '' : ' visibility: hidden;'; - return [ - opacity, - this.getSvgFilter(), - visibility - ].join(''); - }, - - /** - * Returns svg clipPath representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toClipPathSVG: function (reviver) { - var svgString = []; - var bg = this._createSVGBgRect(reviver); - bg && svgString.push('\t', bg); - for (var i = 0; i < this._objects.length; i++) { - svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); - } - return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); - }, - /* _TO_SVG_END_ */ - }); - - /** - * @todo support loading from svg - * @private - * @static - * @memberOf fabric.Group - * @param {Object} object Object to create a group from - * @returns {Promise} - */ - fabric.Group.fromObject = function(object) { - var objects = object.objects || [], - options = clone(object, true); - delete options.objects; - return Promise.all([ - fabric.util.enlivenObjects(objects), - fabric.util.enlivenObjectEnlivables(options) - ]).then(function (enlivened) { - return new fabric.Group(enlivened[0], Object.assign(options, enlivened[1]), true); - }); - }; - -})(typeof exports !== 'undefined' ? exports : this); - - -(function (global) { - - 'use strict'; - - var fabric = global.fabric || (global.fabric = {}); - - if (fabric.Layer) { - fabric.warn('fabric.Layer is already defined'); - return; - } - - /** - * Layer class - * @class fabric.Layer - * @extends fabric.Group - * @see {@link fabric.Layer#initialize} for constructor definition - */ - fabric.Layer = fabric.util.createClass(fabric.Group, /** @lends fabric.Layer.prototype */ { - - /** - * @default - * @type string - */ - type: 'layer', - - /** - * @override - * @default - */ - layout: 'auto', - - /** - * @override - * @default - */ - objectCaching: false, - - /** - * @override - * @default - */ - strokeWidth: 0, - - /** - * @override - * @default - */ - hasControls: false, - - /** - * @override - * @default - */ - hasBorders: false, - - /** - * @override - * @default - */ - lockMovementX: true, - - /** - * @override - * @default - */ - lockMovementY: true, - - /** - * @default - * @override - */ - originX: 'center', - - /** - * @default - * @override - */ - originY: 'center', + return false; + }, /** - * we don't want to int with the layer, only with it's objects - * this makes group selection possible over a layer - * @override - * @default + * Restores original state of each of group objects (original state is that which was before group was created). + * if the nested boolean is true, the original state will be restored just for the + * first group and not for all the group chain + * @private + * @param {Boolean} nested tell the function to restore object state up to the parent group and not more + * @return {fabric.Group} thisArg + * @chainable */ - selectable: false, + _restoreObjectsState: function() { + var groupMatrix = this.calcOwnMatrix(); + this._objects.forEach(function(object) { + // instead of using _this = this; + fabric.util.addTransformToObject(object, groupMatrix); + delete object.group; + object.setCoords(); + }); + return this; + }, /** - * Constructor - * - * @param {fabric.Object[]} [objects] instance objects - * @param {Object} [options] Options object + * Destroys a group (restoring state of its objects) * @return {fabric.Group} thisArg + * @chainable */ - initialize: function (objects, options) { - this.callSuper('initialize', objects, options); - this.__canvasMonitor = this.__canvasMonitor.bind(this); - this.__groupMonitor = this.__groupMonitor.bind(this); - this.__onAdded = this._watchParent.bind(this, true); - this.__onRemoved = this._watchParent.bind(this, false); - this.on('added:initialized', this.__onAdded); - this.on('added', this.__onAdded); - this.on('removed', this.__onRemoved); - // trigger layout in case parent is passed in options - var parent = this.group || this.canvas; - parent && this.__onAdded({ target: parent }); + destroy: function() { + // when group is destroyed objects needs to get a repaint to be eventually + // displayed on canvas. + this._objects.forEach(function(object) { + object.set('dirty', true); + }); + return this._restoreObjectsState(); }, - /** - * @override we want instance to fill parent so we disregard transformations - * @param {CanvasRenderingContext2D} ctx Context - */ - transform: function (ctx) { - var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); - ctx.transform(1, 0, 0, 1, m[4], m[5]); + dispose: function () { + this.callSuper('dispose'); + this.forEachObject(function (object) { + object.dispose && object.dispose(); + }); + this._objects = []; }, /** - * @override apply instance's transformations on objects - * @param {CanvasRenderingContext2D} ctx Context to render on + * make a group an active selection, remove the group from canvas + * the group has to be on canvas for this to work. + * @return {fabric.ActiveSelection} thisArg + * @chainable */ - drawObject: function (ctx) { - this._renderBackground(ctx); - ctx.save(); - var m = this.calcTransformMatrix(!this.needsFullTransform(ctx)); - ctx.transform(m[0], m[1], m[2], m[3], 0, 0); - for (var i = 0, len = this._objects.length; i < len; i++) { - this._objects[i].render(ctx); + toActiveSelection: function() { + if (!this.canvas) { + return; } - ctx.restore(); - this._drawClipPath(ctx, this.clipPath); + var objects = this._objects, canvas = this.canvas; + this._objects = []; + var options = this.toObject(); + delete options.objects; + var activeSelection = new fabric.ActiveSelection([]); + activeSelection.set(options); + activeSelection.type = 'activeSelection'; + canvas.remove(this); + objects.forEach(function(object) { + object.group = activeSelection; + object.dirty = true; + canvas.add(object); + }); + activeSelection.canvas = canvas; + activeSelection._objects = objects; + canvas._activeObject = activeSelection; + activeSelection.setCoords(); + return activeSelection; }, /** - * @private - * @override we want instance to fill parent so we disregard transformations - * @returns {fabric.Point} dimensions + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable */ - _getTransformedDimensions: function () { - return this.callSuper('_getTransformedDimensions', { - scaleX: 1, - scaleY: 1, - skewX: 0, - skewY: 0, - width: this.width, - height: this.height, - strokeWidth: 0 - }); + ungroupOnCanvas: function() { + return this._restoreObjectsState(); }, /** - * we need to invalidate instance's group if objects have changed - * @override - * @private + * Sets coordinates of all objects inside group + * @return {fabric.Group} thisArg + * @chainable */ - __objectMonitor: function (opt) { - this.group && this.group.__objectMonitor(opt); + setObjectsCoords: function() { + var skipControls = true; + this.forEachObject(function(object) { + object.setCoords(skipControls); + }); + return this; }, /** * @private - * @param {boolean} watch - * @param {{target:fabric.Group|fabric.Canvas}} [opt] */ - _watchParent: function (watch, opt) { - var target = opt && opt.target; - // make sure we listen only once - this.canvas && this.canvas.off('resize', this.__canvasMonitor); - this.group && this.group.off('layout', this.__groupMonitor); - if (!watch) { - return; - } - else if (target instanceof fabric.Group) { - this._applyLayoutStrategy({ type: 'group' }); - this.group.on('layout', this.__groupMonitor); - } - else if (target instanceof fabric.StaticCanvas) { - this._applyLayoutStrategy({ type: 'canvas' }); - this.canvas.on('resize', this.__canvasMonitor); + _calcBounds: function(onlyWidthHeight) { + var aX = [], + aY = [], + o, prop, coords, + props = ['tr', 'br', 'bl', 'tl'], + i = 0, iLen = this._objects.length, + j, jLen = props.length; + + for ( ; i < iLen; ++i) { + o = this._objects[i]; + coords = o.calcACoords(); + for (j = 0; j < jLen; j++) { + prop = props[j]; + aX.push(coords[prop].x); + aY.push(coords[prop].y); + } + o.aCoords = coords; } + + this._getBounds(aX, aY, onlyWidthHeight); }, /** * @private */ - __canvasMonitor: function () { - this._applyLayoutStrategy({ type: 'canvas_resize' }); + _getBounds: function(aX, aY, onlyWidthHeight) { + var minXY = new fabric.Point(min(aX), min(aY)), + maxXY = new fabric.Point(max(aX), max(aY)), + top = minXY.y || 0, left = minXY.x || 0, + width = (maxXY.x - minXY.x) || 0, + height = (maxXY.y - minXY.y) || 0; + this.width = width; + this.height = height; + if (!onlyWidthHeight) { + // the bounding box always finds the topleft most corner. + // whatever is the group origin, we set up here the left/top position. + this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); + } }, + /* _TO_SVG_START_ */ /** - * @private + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance */ - __groupMonitor: function (context) { - this._applyLayoutStrategy(Object.assign({}, context, { type: 'group_layout' })); + _toSVG: function(reviver) { + var svgString = ['\n']; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t\t', this._objects[i].toSVG(reviver)); + } + svgString.push('\n'); + return svgString; }, /** - * @private - * @override we do not want to bubble layout + * Returns styles-string for svg-export, specific version for group + * @return {String} */ - _bubbleLayout: function () { - // noop + getSvgStyles: function() { + var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? + 'opacity: ' + this.opacity + ';' : '', + visibility = this.visible ? '' : ' visibility: hidden;'; + return [ + opacity, + this.getSvgFilter(), + visibility + ].join(''); }, /** - * Layer will layout itself once it is added to a canvas/group and by listening to it's parent `resize`/`layout` events respectively - * Override this method to customize layout - * @public - * @param {string} layoutDirective - * @param {fabric.Object[]} objects - * @param {object} context object with data regarding what triggered the call - * @param {'initializion'|'canvas'|'canvas_resize'|'layout_change'} context.type - * @param {fabric.Object[]} context.path array of objects starting from the object that triggered the call to the current one - * @returns {Object} options object + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance */ - getLayoutStrategyResult: function (layoutDirective, objects, context) { // eslint-disable-line no-unused-vars - if ((context.type === 'canvas' || context.type === 'canvas_resize') && this.canvas && !this.group) { - return { - centerX: this.canvas.width / 2, - centerY: this.canvas.height / 2, - width: this.canvas.width, - height: this.canvas.height - }; - } - else if ((context.type === 'group' || context.type === 'group_layout') && this.group) { - var w = this.group.width, h = this.group.height; - return { - centerX: 0, - centerY: 0, - width: w, - height: h - }; + toClipPathSVG: function(reviver) { + var svgString = []; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); } - }, - toString: function () { - return '#'; + return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); }, - - dispose: function () { - this.off('added:initialized', this.__onAdded); - this.off('added', this.__onAdded); - this.off('removed', this.__onRemoved); - this._watchParent(false); - this.callSuper('dispose'); - } - + /* _TO_SVG_END_ */ }); /** - * Returns fabric.Layer instance from an object representation + * Returns {@link fabric.Group} instance from an object representation * @static - * @memberOf fabric.Layer - * @param {Object} object Object to create an instance from - * @returns {Promise} + * @memberOf fabric.Group + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an group instance is created */ - fabric.Layer.fromObject = function (object) { - var objects = object.objects || [], + fabric.Group.fromObject = function(object, callback) { + var objects = object.objects, options = fabric.util.object.clone(object, true); delete options.objects; - return Promise.all([ - fabric.util.enlivenObjects(objects), - fabric.util.enlivenObjectEnlivables(options) - ]).then(function (enlivened) { - return new fabric.Layer(enlivened[0], Object.assign(options, enlivened[1]), true); + if (typeof objects === 'string') { + // it has to be an url or something went wrong. + fabric.loadSVGFromURL(objects, function (elements) { + var group = fabric.util.groupSVGElements(elements, object, objects); + group.set(options); + callback && callback(group); + }); + return; + } + 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.Group(enlivenedObjects, options, true)); + }); }); }; @@ -21392,99 +20628,58 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ type: 'activeSelection', - /** - * @override - */ - layout: 'fit-content', - - /** - * @override - */ - subTargetCheck: false, - - /** - * @override - */ - interactive: false, - /** * Constructor - * - * @param {fabric.Object[]} [objects] instance objects + * @param {Object} objects ActiveSelection objects * @param {Object} [options] Options object - * @param {boolean} [objectsRelativeToGroup] true if objects exist in group coordinate plane - * @return {fabric.ActiveSelection} thisArg - */ - initialize: function (objects, options, objectsRelativeToGroup) { - this.callSuper('initialize', objects, options, objectsRelativeToGroup); - this.setCoords(); - }, - - /** - * @private - */ - _shouldSetNestedCoords: function () { - return true; - }, - - /** - * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object is in canvas coordinate plane - * @returns {boolean} true if object entered group + * @return {Object} thisArg */ - enterGroup: function (object, removeParentTransform) { - if (!this.canEnter(object)) { - return false; - } - if (object.group) { - // save ref to group for later in order to return to it - var parent = object.group; - parent._exitGroup(object); - object.__owningGroup = parent; + initialize: function(objects, options) { + options = options || {}; + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; } - this._enterGroup(object, removeParentTransform); - return true; - }, - /** - * we want objects to retain their canvas ref when exiting instance - * @private - * @param {fabric.Object} object - * @param {boolean} [removeParentTransform] true if object should exit group without applying group's transform to it - */ - exitGroup: function (object, removeParentTransform) { - this._exitGroup(object, removeParentTransform); - var parent = object.__owningGroup; - if (parent) { - // return to owning group - parent.enterGroup(object); - delete object.__owningGroup; + if (options.originX) { + this.originX = options.originX; } + if (options.originY) { + this.originY = options.originY; + } + this._calcBounds(); + this._updateObjectsCoords(); + fabric.Object.prototype.initialize.call(this, options); + this.setCoords(); }, /** - * @private - * @param {'added'|'removed'} type - * @param {fabric.Object[]} targets + * Change te activeSelection to a normal group, + * High level function that automatically adds it to canvas as + * active object. no events fired. + * @since 2.0.0 + * @return {fabric.Group} */ - _onAfterObjectsChange: function (type, targets) { - var groups = []; - targets.forEach(function (object) { - object.group && !groups.includes(object.group) && groups.push(object.group); + toGroup: function() { + var objects = this._objects.concat(); + this._objects = []; + var options = fabric.Object.prototype.toObject.call(this); + var newGroup = new fabric.Group([]); + delete options.type; + newGroup.set(options); + objects.forEach(function(object) { + object.canvas.remove(object); + object.group = newGroup; }); - if (type === 'removed') { - // invalidate groups' layout and mark as dirty - groups.forEach(function (group) { - group._onAfterObjectsChange('added', targets); - }); - } - else { - // mark groups as dirty - groups.forEach(function (group) { - group._set('dirty', true); - }); + newGroup._objects = objects; + if (!this.canvas) { + return newGroup; } + var canvas = this.canvas; + canvas.add(newGroup); + canvas._activeObject = newGroup; + newGroup.setCoords(); + return newGroup; }, /** @@ -21493,7 +20688,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Boolean} [cancel] */ onDeselect: function() { - this.removeAll(); + this.destroy(); return false; }, @@ -21540,7 +20735,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot childrenOverride.hasControls = false; } childrenOverride.forActiveSelection = true; - for (var i = 0; i < this._objects.length; i++) { + for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i]._renderControls(ctx, childrenOverride); } ctx.restore(); @@ -21552,14 +20747,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.ActiveSelection * @param {Object} object Object to create a group from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created */ - fabric.ActiveSelection.fromObject = function(object) { - var objects = object.objects, - options = fabric.util.object.clone(object, true); - delete options.objects; - return fabric.util.enlivenObjects(objects).then(function(enlivenedObjects) { - return new fabric.ActiveSelection(enlivenedObjects, object, true); + fabric.ActiveSelection.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); }); }; @@ -21710,6 +20903,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Please check video element events for seeking. * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} element Image element * @param {Object} [options] Options object + * @param {function} [callback] callback function to call after eventual filters applied. * @return {fabric.Image} thisArg */ initialize: function(element, options) { @@ -21937,18 +21131,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * Sets source of an image * @param {String} src Source string (URL) + * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) * @param {Object} [options] Options object * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @return {Promise} thisArg + * @return {fabric.Image} thisArg + * @chainable */ - setSrc: function(src, options) { - var _this = this; - return fabric.util.loadImage(src, options).then(function(img) { - _this.setElement(img, options); - _this._setWidthHeight(); - return _this; - }); + setSrc: function(src, callback, options) { + fabric.util.loadImage(src, function(img, isError) { + this.setElement(img, options); + this._setWidthHeight(); + callback && callback(this, isError); + }, this, options && options.crossOrigin); + return this; }, /** @@ -21963,8 +21159,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot var filter = this.resizeFilter, minimumScale = this.minimumScaleTrigger, objectScale = this.getTotalObjectScaling(), - scaleX = objectScale.x, - scaleY = objectScale.y, + scaleX = objectScale.scaleX, + scaleY = objectScale.scaleY, elementToFilter = this._filteredEl || this._originalElement; if (this.group) { this.set('dirty', true); @@ -22120,7 +21316,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _needsResize: function() { var scale = this.getTotalObjectScaling(); - return (scale.x !== this._lastScaleX || scale.y !== this._lastScaleY); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); }, /** @@ -22152,6 +21348,22 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot this._setWidthHeight(options); }, + /** + * @private + * @param {Array} filters to be initialized + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(filters, callback) { + if (filters && filters.length) { + fabric.util.enlivenObjects(filters, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, 'fabric.Image.filters'); + } + else { + callback && callback(); + } + }, + /** * @private * Set the width and the height of the image object, using the element or the @@ -22249,39 +21461,39 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Creates an instance of fabric.Image from its object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} - */ - fabric.Image.fromObject = function(_object) { - var object = fabric.util.object.clone(_object), - filters = object.filters, - resizeFilter = object.resizeFilter; - // the generic enliving will fail on filters for now - delete object.resizeFilter; - delete object.filters; - return Promise.all([ - fabric.util.loadImage(object.src, { crossOrigin: _object.crossOrigin }), - filters && fabric.util.enlivenObjects(filters, 'fabric.Image.filters'), - resizeFilter && fabric.util.enlivenObjects([resizeFilter], 'fabric.Image.filters'), - fabric.util.enlivenObjectEnlivables(object), - ]) - .then(function(imgAndFilters) { - object.filters = imgAndFilters[1] || []; - object.resizeFilter = imgAndFilters[2] && imgAndFilters[2][0]; - return new fabric.Image(imgAndFilters[0], Object.assign(object, imgAndFilters[3])); + * @param {Function} callback Callback to invoke when an image instance is created + */ + fabric.Image.fromObject = function(_object, callback) { + var object = fabric.util.object.clone(_object); + fabric.util.loadImage(object.src, function(img, isError) { + if (isError) { + callback && callback(null, true); + return; + } + fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { + object.filters = filters || []; + fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { + object.resizeFilter = resizeFilters[0]; + fabric.util.enlivenObjectEnlivables(object, object, function () { + var image = new fabric.Image(img, object); + callback(image, false); + }); + }); }); + }, null, object.crossOrigin); }; /** * Creates an instance of fabric.Image from an URL string * @static * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. * @param {Object} [imgOptions] Options object - * @returns {Promise} */ - fabric.Image.fromURL = function(url, imgOptions) { - return fabric.util.loadImage(url, imgOptions || {}).then(function(img) { - return new fabric.Image(img, imgOptions); - }); + fabric.Image.fromURL = function(url, callback, imgOptions) { + fabric.util.loadImage(url, function(img, isError) { + callback && callback(new fabric.Image(img, imgOptions), isError); + }, null, imgOptions && imgOptions.crossOrigin); }; /* _FROM_SVG_START_ */ @@ -22305,10 +21517,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ fabric.Image.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); - fabric.Image.fromURL(parsedAttributes['xlink:href'], Object.assign({ }, options || { }, parsedAttributes)) - .then(function(fabricImage) { - callback(fabricImage); - }); + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, + extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); }; /* _FROM_SVG_END_ */ @@ -23218,14 +22428,10 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag } }); -/** - * Create filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @returns {Promise} - */ -fabric.Image.filters.BaseFilter.fromObject = function(object) { - return Promise.resolve(new fabric.Image.filters[object.type](object)); +fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { + var filter = new fabric.Image.filters[object.type](object); + callback && callback(filter); + return filter; }; @@ -23380,10 +22586,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] function to invoke after filter creation + * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix */ fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; })(typeof exports !== 'undefined' ? exports : this); @@ -23493,10 +22700,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness */ fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -23846,10 +23054,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute */ fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24001,10 +23210,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale */ fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24112,10 +23322,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert */ fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24248,10 +23459,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise */ fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24386,10 +23598,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate */ fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24560,10 +23773,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite */ fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -24899,10 +24113,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor */ fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25141,16 +24356,17 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} callback to be invoked after filter creation + * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage */ - fabric.Image.filters.BlendImage.fromObject = function(object) { - return fabric.Image.fromObject(object.image).then(function(image) { + fabric.Image.filters.BlendImage.fromObject = function(object, callback) { + fabric.Image.fromObject(object.image, function(image) { var options = fabric.util.object.clone(object); options.image = image; - return new fabric.Image.filters.BlendImage(options); + callback(new fabric.Image.filters.BlendImage(options)); }); }; @@ -25638,10 +24854,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize */ fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25752,10 +24969,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast */ fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25811,7 +25029,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * Saturation value, from -1 to 1. * Increases/decreases the color saturation. * A value of 0 has no effect. - * + * * @param {Number} saturation * @default */ @@ -25872,10 +25090,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate */ fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -25932,7 +25151,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * Vibrance value, from -1 to 1. * Increases/decreases the saturation of more muted colors with less effect on saturated colors. * A value of 0 has no effect. - * + * * @param {Number} vibrance * @default */ @@ -25995,10 +25214,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Vibrance} Instance of fabric.Image.filters.Vibrance */ fabric.Image.filters.Vibrance.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -26217,10 +25437,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @returns {Promise} + * Deserialize a JSON definition of a BlurFilter into a concrete instance. */ filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -26354,10 +25571,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma */ fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -26426,13 +25644,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { /** * Deserialize a JSON definition of a ComposedFilter into a concrete instance. */ - fabric.Image.filters.Composed.fromObject = function(object) { - var filters = object.subFilters || []; - return Promise.all(filters.map(function(filter) { - return fabric.Image.filters[filter.type].fromObject(filter); - })).then(function(enlivedFilters) { - return new fabric.Image.filters.Composed({ subFilters: enlivedFilters }); - }); + fabric.Image.filters.Composed.fromObject = function(object, callback) { + var filters = object.subFilters || [], + subFilters = filters.map(function(filter) { + return new fabric.Image.filters[filter.type](filter); + }), + instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); + callback && callback(instance); + return instance; }; })(typeof exports !== 'undefined' ? exports : this); @@ -26535,10 +25754,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { }); /** - * Create filter instance from an object representation + * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation */ fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; @@ -27429,20 +26649,11 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { /** * Measure and return the info of a single grapheme. * needs the the info of previous graphemes already filled - * Override to customize measuring - * - * @typedef {object} GraphemeBBox - * @property {number} width - * @property {number} height - * @property {number} kernedWidth - * @property {number} left - * @property {number} deltaY - * + * @private * @param {String} grapheme to be measured * @param {Number} lineIndex index of the line where the char is * @param {Number} charIndex position in the line * @param {String} [prevGrapheme] character preceding the one to be measured - * @returns {GraphemeBBox} grapheme bbox */ _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), @@ -27600,9 +26811,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, - // this was changed in the PR #7674 - // currentDirection = ctx.canvas.getAttribute('dir'); - drawingLeft, currentDirection = ctx.direction; + drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); @@ -27864,15 +27073,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { leftOffset = lineDiff; } if (direction === 'rtl') { - if (textAlign === 'right' || textAlign === 'justify' || textAlign === 'justify-right') { - leftOffset = 0; - } - else if (textAlign === 'left' || textAlign === 'justify-left') { - leftOffset = -lineDiff; - } - else if (textAlign === 'center' || textAlign === 'justify-center') { - leftOffset = -lineDiff / 2; - } + leftOffset -= lineDiff; } return leftOffset; }, @@ -28078,15 +27279,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { this.callSuper('render', ctx); }, - /** - * Override this method to customize grapheme splitting - * @param {string} value - * @returns {string[]} array of graphemes - */ - graphemeSplit: function (value) { - return fabric.util.string.graphemeSplit(value); - }, - /** * Returns the text as an array of lines. * @param {String} text text to split @@ -28098,7 +27290,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { newLine = ['\n'], newText = []; for (var i = 0; i < lines.length; i++) { - newLines[i] = this.graphemeSplit(lines[i]); + newLines[i] = fabric.util.string.graphemeSplit(lines[i]); newText = newText.concat(newLines[i], newLine); } newText.pop(); @@ -28274,10 +27466,22 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * @static * @memberOf fabric.Text * @param {Object} object plain js Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created */ - fabric.Text.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Text, object, 'text'); + fabric.Text.fromObject = function(object, callback) { + var objectCopy = clone(object), path = object.path; + delete objectCopy.path; + return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { + if (path) { + fabric.Object._fromObject('Path', path, function(pathInstance) { + textInstance.set('path', pathInstance); + callback(textInstance); + }, 'path'); + } + else { + callback(textInstance); + } + }, 'text'); }; fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace']; @@ -28614,6 +27818,16 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { (function() { + + function parseDecoration(object) { + if (object.textDecoration) { + object.textDecoration.indexOf('underline') > -1 && (object.underline = true); + object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true); + object.textDecoration.indexOf('overline') > -1 && (object.overline = true); + delete object.textDecoration; + } + } + /** * IText class (introduced in v1.4) Events are also fired with "text:" * prefix when observing canvas. @@ -28801,21 +28015,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { this.initBehavior(); }, - /** - * While editing handle differently - * @private - * @param {string} key - * @param {*} value - */ - _set: function (key, value) { - if (this.isEditing && this._savedProps && key in this._savedProps) { - this._savedProps[key] = value; - } - else { - this.callSuper('_set', key, value); - } - }, - /** * Sets selection start (left boundary of a selection) * @param {Number} index Index to set selection start to @@ -28986,15 +28185,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { - boundaries.left *= -1; - } - else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { - boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); - } - else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { - boundaries.left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0); - } + boundaries.left *= -1; } this.cursorOffsetCache = boundaries; return this.cursorOffsetCache; @@ -29083,15 +28274,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { ctx.fillStyle = this.selectionColor; } if (this.direction === 'rtl') { - if (this.textAlign === 'right' || this.textAlign === 'justify' || this.textAlign === 'justify-right') { - drawStart = this.width - drawStart - drawWidth; - } - else if (this.textAlign === 'left' || this.textAlign === 'justify-left') { - drawStart = boundaries.left + lineOffset - boxEnd; - } - else if (this.textAlign === 'center' || this.textAlign === 'justify-center') { - drawStart = boundaries.left + lineOffset - boxEnd; - } + drawStart = this.width - drawStart - drawWidth; } ctx.fillRect( drawStart, @@ -29143,10 +28326,18 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { * @static * @memberOf fabric.IText * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {function} [callback] invoked with new instance as argument */ - fabric.IText.fromObject = function(object) { - return fabric.Object._fromObject(fabric.IText, object, 'text'); + fabric.IText.fromObject = function(object, callback) { + parseDecoration(object); + if (object.styles) { + for (var i in object.styles) { + for (var j in object.styles[i]) { + parseDecoration(object.styles[i][j]); + } + } + } + fabric.Object._fromObject('IText', object, callback, 'text'); }; })(); @@ -29178,9 +28369,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { */ initAddedHandler: function() { var _this = this; - this.on('added', function (opt) { - // make sure we listen to the canvas added event - var canvas = opt.target; + this.on('added', function() { + var canvas = _this.canvas; if (canvas) { if (!canvas._hasITextHandlers) { canvas._hasITextHandlers = true; @@ -29194,9 +28384,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { initRemovedHandler: function() { var _this = this; - this.on('removed', function (opt) { - // make sure we listen to the canvas removed event - var canvas = opt.target; + this.on('removed', function() { + var canvas = _this.canvas; if (canvas) { canvas._iTextInstances = canvas._iTextInstances || []; fabric.util.removeFromArray(canvas._iTextInstances, _this); @@ -29296,14 +28485,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { this.abortCursorAnimation(); this._currentCursorOpacity = 1; - if (delay) { - this._cursorTimeout2 = setTimeout(function () { - _this._tick(); - }, delay); - } - else { - this._tick(); - } + this._cursorTimeout2 = setTimeout(function() { + _this._tick(); + }, delay); }, /** @@ -29592,12 +28776,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { */ fromStringToGraphemeSelection: function(start, end, text) { var smallerTextStart = text.slice(0, start), - graphemeStart = this.graphemeSplit(smallerTextStart).length; + graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length; if (start === end) { return { selectionStart: graphemeStart, selectionEnd: graphemeStart }; } var smallerTextEnd = text.slice(start, end), - graphemeEnd = this.graphemeSplit(smallerTextEnd).length; + graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length; return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd }; }, @@ -29752,8 +28936,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object) { this.canvas.defaultCursor = this._savedProps.defaultCursor; this.canvas.moveCursor = this._savedProps.moveCursor; } - - delete this._savedProps; }, /** @@ -30253,8 +29435,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ mouseUpHandler: function(options) { this.__isMousedown = false; - if (!this.editable || - (this.group && !this.group.interactive) || + if (!this.editable || this.group || (options.transform && options.transform.actionPerformed) || (options.e.button && options.e.button !== 1)) { return; @@ -30332,7 +29513,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot break; } } - lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); + lineLeftOffset = this._getLineLeftOffset(lineIndex); width = lineLeftOffset * this.scaleX; line = this._textLines[lineIndex]; // handling of RTL: in order to get things work correctly, @@ -30340,7 +29521,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // so in position detection we mirror the X offset, and when is time // of rendering it, we mirror it again. if (this.direction === 'rtl') { - mouseOffset.x = this.width * this.scaleX - mouseOffset.x; + mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; } for (var j = 0, jlen = line.length; j < jlen; j++) { prevWidth = width; @@ -30398,7 +29579,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // https://bugs.chromium.org/p/chromium/issues/detail?id=870966 this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' + - ' padding-top: ' + style.fontSize + ';'; + ' paddingーtop: ' + style.fontSize + ';'; if (this.hiddenTextareaContainer) { this.hiddenTextareaContainer.appendChild(this.hiddenTextarea); @@ -30407,7 +29588,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot fabric.document.body.appendChild(this.hiddenTextarea); } - fabric.util.addListener(this.hiddenTextarea, 'blur', this.blur.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); @@ -30481,13 +29661,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this.hiddenTextarea && this.hiddenTextarea.focus(); }, - /** - * Override this method to customize cursor behavior on textbox blur - */ - blur: function () { - this.abortCursorAnimation(); - }, - /** * Handles keydown event * only used for arrows and combination of modifier keys. @@ -31069,7 +30242,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (end > start) { this.removeStyleFromTo(start, end); } - var graphemes = this.graphemeSplit(text); + var graphemes = fabric.util.string.graphemeSplit(text); this.insertNewStyleBlock(graphemes, start, style); this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end)); this.text = this._text.join(''); @@ -31139,7 +30312,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''), (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''), (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''), - (this.direction === 'rtl' ? 'direction="' + this.direction + '" ' : ''), 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textAndBg.textSpans.join(''), '\n' @@ -31162,9 +30334,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); - if (this.direction === 'rtl') { - lineOffset += this.width; - } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } @@ -31239,12 +30408,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset)); charsToRender = ''; actualStyle = nextStyle; - if (this.direction === 'rtl') { - textLeftOffset -= boxWidth; - } - else { - textLeftOffset += boxWidth; - } + textLeftOffset += boxWidth; boxWidth = 0; } } @@ -31609,7 +30773,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot var wrapped = [], i; this.isWrapping = true; for (i = 0; i < lines.length; i++) { - wrapped.push.apply(wrapped, this._wrapLine(lines[i], i, desiredWidth)); + wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth)); } this.isWrapping = false; return wrapped; @@ -31617,15 +30781,13 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * Helper function to measure a string of text, given its lineIndex and charIndex offset - * It gets called when charBounds are not available yet. - * Override if necessary - * Use with {@link fabric.Textbox#wordSplit} - * + * it gets called when charBounds are not available yet. * @param {CanvasRenderingContext2D} ctx * @param {String} text * @param {number} lineIndex * @param {number} charOffset * @returns {number} + * @private */ _measureWord: function(word, lineIndex, charOffset) { var width = 0, prevGrapheme, skipLeft = true; @@ -31638,16 +30800,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot return width; }, - /** - * Override this method to customize word splitting - * Use with {@link fabric.Textbox#_measureWord} - * @param {string} value - * @returns {string[]} array of words - */ - wordSplit: function (value) { - return value.split(this._wordJoiners); - }, - /** * Wraps a line of text using the width of the Textbox and a context. * @param {Array} line The grapheme array that represent the line @@ -31663,7 +30815,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot graphemeLines = [], line = [], // spaces in different languages? - words = splitByGrapheme ? this.graphemeSplit(_line) : this.wordSplit(_line), + words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners), word = '', offset = 0, infix = splitByGrapheme ? '' : ' ', @@ -31678,25 +30830,14 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot words.push([]); } desiredWidth -= reservedSpace; - // measure words - var data = words.map(function (word) { - // if using splitByGrapheme words are already in graphemes. - word = splitByGrapheme ? word : this.graphemeSplit(word); - var width = this._measureWord(word, lineIndex, offset); - largestWordWidth = Math.max(width, largestWordWidth); - offset += word.length + 1; - return { word: word, width: width }; - }.bind(this)); - var maxWidth = Math.max(desiredWidth, largestWordWidth, this.dynamicMinWidth); - // layout words - offset = 0; for (var i = 0; i < words.length; i++) { - word = data[i].word; - wordWidth = data[i].width; + // if using splitByGrapheme words are already in graphemes. + word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]); + wordWidth = this._measureWord(word, lineIndex, offset); offset += word.length; lineWidth += infixWidth + wordWidth - additionalSpace; - if (lineWidth > maxWidth && !lineJustStarted) { + if (lineWidth > desiredWidth && !lineJustStarted) { graphemeLines.push(line); line = []; lineWidth = wordWidth; @@ -31714,6 +30855,10 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); offset++; lineJustStarted = false; + // keep track of largest word + if (wordWidth > largestWordWidth) { + largestWordWidth = wordWidth; + } } i && graphemeLines.push(line); @@ -31807,10 +30952,10 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @static * @memberOf fabric.Textbox * @param {Object} object Object to create an instance from - * @returns {Promise} + * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created */ - fabric.Textbox.fromObject = function(object) { - return fabric.Object._fromObject(fabric.Textbox, object, 'text'); + fabric.Textbox.fromObject = function(object, callback) { + return fabric.Object._fromObject('Textbox', object, callback, 'text'); }; })(typeof exports !== 'undefined' ? exports : this); From f4c8c5bd557a96545d824502e215f6732ec32586 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 07:52:10 +0300 Subject: [PATCH 70/77] Update group.class.js --- src/shapes/group.class.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 7d27b5ae784..40fa1858b98 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -840,8 +840,8 @@ a = objCenter.subtract(sizeVector); b = objCenter.add(sizeVector); if (i === 0) { - min = new fabric.Point(Math.min(a.x, b.x), Math.min(a.y, b.y)); - max = new fabric.Point(Math.max(a.x, b.x), Math.max(a.y, b.y)); + min.setXY(new fabric.Point(Math.min(a.x, b.x), Math.min(a.y, b.y))); + max.setXY(new fabric.Point(Math.max(a.x, b.x), Math.max(a.y, b.y))); } else { min.setXY(Math.min(min.x, a.x, b.x), Math.min(min.y, a.y, b.y)); From da115b5606bf19fdd3fb153d515f3172809e2225 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 08:40:41 +0300 Subject: [PATCH 71/77] remove dead code Some logic is leaking between PRs. this belongs to #7861 --- src/shapes/group.class.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 40fa1858b98..651ccac6063 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -750,24 +750,6 @@ }), rotationCorrection = new fabric.Point(0, 0); - if (this.angle) { - var rad = degreesToRadians(this.angle), - sin = Math.abs(fabric.util.sin(rad)), - cos = Math.abs(fabric.util.cos(rad)); - sizeAfter.setXY( - sizeAfter.x * cos + sizeAfter.y * sin, - sizeAfter.x * sin + sizeAfter.y * cos - ); - bboxSizeAfter.setXY( - bboxSizeAfter.x * cos + bboxSizeAfter.y * sin, - bboxSizeAfter.x * sin + bboxSizeAfter.y * cos - ); - strokeWidthVector = fabric.util.rotateVector(strokeWidthVector, rad); - // correct center after rotating - var strokeCorrection = strokeWidthVector.multiply(origin.scalarAdd(-0.5).scalarDivide(-2)); - rotationCorrection = sizeAfter.subtract(size).scalarDivide(2).add(strokeCorrection); - calculatedCenter.addEquals(rotationCorrection); - } // calculate center and correction var originT = origin.scalarAdd(0.5); var originCorrection = sizeAfter.multiply(originT); From 5489cdc91d08afd4bcc5c68868b519ba4e196e70 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 08:48:29 +0300 Subject: [PATCH 72/77] revert --- test/unit/layer.js | 907 --------------------------------------------- 1 file changed, 907 deletions(-) delete mode 100644 test/unit/layer.js diff --git a/test/unit/layer.js b/test/unit/layer.js deleted file mode 100644 index 76a4df8c063..00000000000 --- a/test/unit/layer.js +++ /dev/null @@ -1,907 +0,0 @@ -(function() { - var canvas = this.canvas = new fabric.StaticCanvas(null, {enableRetinaScaling: false, width: 600, height: 600}); - - function makeLayerWith2Objects(performLayout, subTargetCheck, interactive) { - var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), - rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0 }); - - var layer = new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); - if (performLayout) { - new fabric.Group([layer], { subTargetCheck, interactive }); - } - return layer; - } - - function makeLayerWith2ObjectsWithOpacity(performLayout, subTargetCheck, interactive) { - var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0, opacity: 0.5 }), - rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0, opacity: 0.8 }); - - var layer = new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); - if (performLayout) { - new fabric.Group([layer], { subTargetCheck, interactive }); - } - return layer; - } - - function makeLayerWith2ObjectsAndNoExport(performLayout, subTargetCheck, interactive) { - var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10, strokeWidth: 0 }), - rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40, strokeWidth: 0, excludeFromExport: true }); - - var layer = new fabric.Layer([rect1, rect2], { strokeWidth: 0 }); - if (performLayout) { - new fabric.Group([layer], { subTargetCheck, interactive }); - } - return layer; - } - - function makeLayerWith4Objects(performLayout, subTargetCheck, interactive) { - var rect1 = new fabric.Rect({ top: 100, left: 100, width: 30, height: 10 }), - rect2 = new fabric.Rect({ top: 120, left: 50, width: 10, height: 40 }), - rect3 = new fabric.Rect({ top: 40, left: 0, width: 20, height: 40 }), - rect4 = new fabric.Rect({ top: 75, left: 75, width: 40, height: 40 }); - - var layer = new fabric.Layer([rect1, rect2, rect3, rect4]); - if (performLayout) { - new fabric.Group([layer], { subTargetCheck, interactive }); - } - return layer; - } - - QUnit.module('fabric.Layer', { - afterEach: function() { - fabric.Object.__uid = 0; - canvas.clear(); - canvas.backgroundColor = fabric.Canvas.prototype.backgroundColor; - canvas.calcOffset(); - } - }); - - QUnit.test('constructor', function(assert) { - var layer = makeLayerWith2Objects(); - - assert.ok(layer); - assert.ok(layer instanceof fabric.Layer, 'should be instance of fabric.Layer'); - }); - - QUnit.test('toString', function(assert) { - var layer = makeLayerWith2Objects(); - assert.equal(layer.toString(), '#', 'should return proper representation'); - }); - - QUnit.test('getObjects', function(assert) { - var rect1 = new fabric.Rect(), - rect2 = new fabric.Rect(); - - var layer = new fabric.Layer([rect1, rect2]); - - assert.ok(typeof layer.getObjects === 'function'); - assert.ok(Array.isArray(layer.getObjects()), 'should be an array'); - assert.equal(layer.getObjects().length, 2, 'should have 2 items'); - assert.deepEqual(layer.getObjects(), [rect1, rect2], 'should return deepEqual objects as those passed to constructor'); - }); - - QUnit.test('getObjects with type', function(assert) { - var rect = new fabric.Rect({ width: 10, height: 20 }), - circle = new fabric.Circle({ radius: 30 }); - - var layer = new fabric.Layer([rect, circle]); - - assert.equal(layer.size(), 2, 'should have length=2 initially'); - - assert.deepEqual(layer.getObjects('rect'), [rect], 'should return rect only'); - assert.deepEqual(layer.getObjects('circle'), [circle], 'should return circle only'); - assert.deepEqual(layer.getObjects('circle', 'rect'), [rect, circle], 'should return circle and rect, in the same order they are'); - }); - - QUnit.test('add', function(assert) { - var layer = makeLayerWith2Objects(); - var rect1 = new fabric.Rect(), - rect2 = new fabric.Rect(), - rect3 = new fabric.Rect(); - - assert.ok(typeof layer.add === 'function'); - layer.add(rect1); - assert.strictEqual(layer.item(layer.size() - 1), rect1, 'last object should be newly added one'); - assert.equal(layer.getObjects().length, 3, 'there should be 3 objects'); - - layer.add(rect2, rect3); - assert.strictEqual(layer.item(layer.size() - 1), rect3, 'last object should be last added one'); - assert.equal(layer.size(), 5, 'there should be 5 objects'); - }); - - QUnit.test('remove', function(assert) { - var rect1 = new fabric.Rect(), - rect2 = new fabric.Rect(), - rect3 = new fabric.Rect(), - layer = new fabric.Layer([rect1, rect2, rect3]); - - assert.ok(typeof layer.remove === 'function'); - layer.remove(rect2); - assert.deepEqual(layer.getObjects(), [rect1, rect3], 'should remove object properly'); - - layer.remove(rect1, rect3); - assert.equal(layer.isEmpty(), true, 'layer should be empty'); - }); - - QUnit.test('size', function(assert) { - var layer = makeLayerWith2Objects(); - - assert.ok(typeof layer.size === 'function'); - assert.equal(layer.size(), 2); - layer.add(new fabric.Rect()); - assert.equal(layer.size(), 3); - layer.remove(layer.getObjects()[0]); - layer.remove(layer.getObjects()[0]); - assert.equal(layer.size(), 1); - }); - - QUnit.test('set', function(assert) { - var layer = makeLayerWith2Objects(), - firstObject = layer.getObjects()[0]; - - assert.ok(typeof layer.set === 'function'); - - layer.set('opacity', 0.12345); - assert.equal(layer.get('opacity'), 0.12345, 'layer\'s "own" property should be set properly'); - assert.equal(firstObject.get('opacity'), 1, 'objects\' value of non delegated property should stay same'); - - layer.set('left', 1234); - assert.equal(layer.get('left'), 1234, 'layer\'s own "left" property should be set properly'); - assert.ok(firstObject.get('left') !== 1234, 'objects\' value should not be affected'); - - layer.set({ left: 888, top: 999 }); - assert.equal(layer.get('left'), 888, 'layer\'s own "left" property should be set properly via object'); - assert.equal(layer.get('top'), 999, 'layer\'s own "top" property should be set properly via object'); - }); - - QUnit.test('contains', function(assert) { - var rect1 = new fabric.Rect(), - rect2 = new fabric.Rect(), - notIncludedRect = new fabric.Rect(), - layer = new fabric.Layer([rect1, rect2]); - - assert.ok(typeof layer.contains === 'function'); - - assert.ok(layer.contains(rect1), 'should contain first object'); - assert.ok(layer.contains(rect2), 'should contain second object'); - - assert.ok(!layer.contains(notIncludedRect), 'should report not-included one properly'); - }); - - QUnit.test('toObject', function(assert) { - var layer = makeLayerWith2Objects(); - - assert.ok(typeof layer.toObject === 'function'); - - var clone = layer.toObject(); - - var expectedObject = { - version: fabric.version, - type: 'layer', - originX: 'center', - originY: 'center', - left: 0, - top: 0, - width: 0, - height: 0, - fill: 'rgb(0,0,0)', - layout: 'auto', - stroke: null, - strokeWidth: 0, - strokeDashArray: null, - strokeLineCap: 'butt', - strokeDashOffset: 0, - strokeLineJoin: 'miter', - strokeMiterLimit: 4, - scaleX: 1, - scaleY: 1, - shadow: null, - visible: true, - backgroundColor: '', - angle: 0, - flipX: false, - flipY: false, - opacity: 1, - fillRule: 'nonzero', - paintFirst: 'fill', - globalCompositeOperation: 'source-over', - skewX: 0, - skewY: 0, - objects: clone.objects, - strokeUniform: false, - subTargetCheck: false, - interactive: false, - }; - - assert.deepEqual(clone, expectedObject); - - assert.ok(layer !== clone, 'should produce different object'); - assert.ok(layer.getObjects() !== clone.objects, 'should produce different object array'); - assert.ok(layer.getObjects()[0] !== clone.objects[0], 'should produce different objects in array'); - - // peform layout - new fabric.Group([layer]); - clone = layer.toObject(); - - Object.assign(expectedObject, { width: 80, height: 60 }); - assert.deepEqual(clone, expectedObject); - - assert.ok(layer !== clone, 'should produce different object'); - assert.ok(layer.getObjects() !== clone.objects, 'should produce different object array'); - assert.ok(layer.getObjects()[0] !== clone.objects[0], 'should produce different objects in array'); - }); - - QUnit.test('toObject without default values', function(assert) { - var layer = makeLayerWith2Objects(); - layer.includeDefaultValues = false; - var clone = layer.toObject(); - var objects = [{ - version: fabric.version, - type: 'rect', - left: 10, - top: -30, - width: 30, - height: 10, - strokeWidth: 0, - }, { - version: fabric.version, - type: 'rect', - left: -40, - top: -10, - width: 10, - height: 40, - strokeWidth: 0, - }]; - var expectedObject = { - version: fabric.version, - type: 'layer', - left: 0, - top: 0, - objects: objects, - }; - assert.deepEqual(clone, expectedObject); - // peform layout - new fabric.Group([layer]); - clone = layer.toObject(); - expectedObject = { - version: fabric.version, - type: 'layer', - left: 0, - top: 0, - width: 80, - height: 60, - objects: objects, - }; - assert.deepEqual(clone, expectedObject); - }); - - - QUnit.test('toObject with excludeFromExport set on an object', function (assert) { - var layer = makeLayerWith2Objects(); - var group2 = makeLayerWith2ObjectsAndNoExport(); - var clone = layer.toObject(); - var clone2 = group2.toObject(); - assert.deepEqual(clone2.objects, group2._objects.filter(obj => !obj.excludeFromExport).map(obj => obj.toObject())); - delete clone.objects; - delete clone2.objects; - assert.deepEqual(clone, clone2); - }); - - QUnit.test('render', function(assert) { - var layer = makeLayerWith2Objects(); - assert.ok(typeof layer.render === 'function'); - }); - - QUnit.test('item', function(assert) { - var layer = makeLayerWith2Objects(); - - assert.ok(typeof layer.item === 'function'); - assert.equal(layer.item(0), layer.getObjects()[0]); - assert.equal(layer.item(1), layer.getObjects()[1]); - assert.equal(layer.item(9999), undefined); - }); - - QUnit.test('moveTo', function(assert) { - var layer = makeLayerWith4Objects(), - groupEl1 = layer.getObjects()[0], - groupEl2 = layer.getObjects()[1], - groupEl3 = layer.getObjects()[2], - groupEl4 = layer.getObjects()[3]; - - assert.ok(typeof layer.item(0).moveTo === 'function'); - - // [ 1, 2, 3, 4 ] - assert.equal(layer.item(0), groupEl1); - assert.equal(layer.item(1), groupEl2); - assert.equal(layer.item(2), groupEl3); - assert.equal(layer.item(3), groupEl4); - assert.equal(layer.item(9999), undefined); - - layer.item(0).moveTo(3); - - // moved 1 to level 3 — [2, 3, 4, 1] - assert.equal(layer.item(3), groupEl1); - assert.equal(layer.item(0), groupEl2); - assert.equal(layer.item(1), groupEl3); - assert.equal(layer.item(2), groupEl4); - assert.equal(layer.item(9999), undefined); - - layer.item(0).moveTo(2); - - // moved 2 to level 2 — [3, 4, 2, 1] - assert.equal(layer.item(3), groupEl1); - assert.equal(layer.item(2), groupEl2); - assert.equal(layer.item(0), groupEl3); - assert.equal(layer.item(1), groupEl4); - assert.equal(layer.item(9999), undefined); - }); - - QUnit.test('complexity', function(assert) { - var layer = makeLayerWith2Objects(); - - assert.ok(typeof layer.complexity === 'function'); - assert.equal(layer.complexity(), 2); - }); - - QUnit.test('removeAll', function(assert) { - var layer = makeLayerWith2Objects(true), - firstObject = layer.item(0), - initialLeftValue = 100, - initialTopValue = 100; - - assert.ok(typeof layer.removeAll === 'function'); - - assert.ok(initialLeftValue !== firstObject.get('left')); - assert.ok(initialTopValue !== firstObject.get('top')); - - layer.removeAll(); - assert.equal(firstObject.get('left'), initialLeftValue, 'should restore initial left value'); - assert.equal(firstObject.get('top'), initialTopValue, 'should restore initial top value'); - }); - - QUnit.test('containsPoint', function(assert) { - - var layer = makeLayerWith2Objects(true); - layer.group.setCoords(); - - // Rect #1 top: 100, left: 100, width: 30, height: 10 - // Rect #2 top: 120, left: 50, width: 10, height: 40 - - assert.ok(typeof layer.containsPoint === 'function'); - - function containsPoint(p) { - return layer.group.containsPoint(p); - } - - assert.ok(!containsPoint({ x: 0, y: 0 })); - - assert.ok(containsPoint({ x: 51, y: 121 })); - assert.ok(containsPoint({ x: 100, y: 160 })); - assert.ok(!containsPoint({ x: 0, y: 0 })); - - layer.group.padding = 30; - layer.group.triggerLayout(); - assert.ok(containsPoint({ x: 51, y: 121 })); - assert.ok(!containsPoint({ x: 100, y: 170 })); - assert.ok(!containsPoint({ x: 0, y: 0 })); - }); - - QUnit.test('fromObject', function(assert) { - var done = assert.async(); - var layer = makeLayerWith2ObjectsWithOpacity(); - - assert.ok(typeof fabric.Layer.fromObject === 'function'); - var groupObject = layer.toObject(); - - fabric.Layer.fromObject(groupObject).then(function(newGroupFromObject) { - - var objectFromOldGroup = layer.toObject(); - var objectFromNewGroup = newGroupFromObject.toObject(); - - assert.ok(newGroupFromObject instanceof fabric.Layer); - - assert.deepEqual(objectFromOldGroup.objects[0], objectFromNewGroup.objects[0]); - assert.deepEqual(objectFromOldGroup.objects[1], objectFromNewGroup.objects[1]); - - // delete `objects` arrays, since `assertHashEqual` fails to compare them for equality - delete objectFromOldGroup.objects; - delete objectFromNewGroup.objects; - - assert.deepEqual(objectFromOldGroup, objectFromNewGroup); - - done(); - }); - }); - - QUnit.test('fromObject after layout', function (assert) { - var done = assert.async(); - var layer = makeLayerWith2ObjectsWithOpacity(true); - - assert.ok(typeof fabric.Layer.fromObject === 'function'); - var groupObject = layer.toObject(); - - fabric.Layer.fromObject(groupObject).then(function (newGroupFromObject) { - - var objectFromOldGroup = layer.toObject(); - var objectFromNewGroup = newGroupFromObject.toObject(); - - assert.ok(newGroupFromObject instanceof fabric.Layer); - - assert.deepEqual(objectFromOldGroup.objects[0], objectFromNewGroup.objects[0]); - assert.deepEqual(objectFromOldGroup.objects[1], objectFromNewGroup.objects[1]); - - // delete `objects` arrays, since `assertHashEqual` fails to compare them for equality - delete objectFromOldGroup.objects; - delete objectFromNewGroup.objects; - - assert.deepEqual(objectFromOldGroup, objectFromNewGroup); - - done(); - }); - }); - - QUnit.test('fromObject with clipPath', function(assert) { - var done = assert.async(); - var clipPath = new fabric.Rect({ - width: 500, - height: 250, - top: 0, - left: 0, - absolutePositioned: true - }); - - var groupObject = new fabric.Layer([ - new fabric.Rect({ width: 100, height: 100, fill: 'red' }), - new fabric.Rect({ width: 100, height: 100, fill: 'yellow', left: 100 }), - new fabric.Rect({ width: 100, height: 100, fill: 'blue', top: 100 }), - new fabric.Rect({ width: 100, height: 100, fill: 'green', left: 100, top: 100 }) - ]); - groupObject.clipPath = clipPath; - - var groupToObject = groupObject.toObject(); - - fabric.Layer.fromObject(groupToObject).then(function(newGroupFromObject) { - - var objectFromNewGroup = newGroupFromObject.toObject(); - - assert.ok(newGroupFromObject instanceof fabric.Layer); - assert.ok(newGroupFromObject.clipPath instanceof fabric.Rect, 'clipPath has been restored'); - assert.deepEqual(objectFromNewGroup, groupToObject, 'double serialization gives same results'); - - done(); - }); - }); - - QUnit.test('fromObject restores oCoords', function(assert) { - var done = assert.async(); - var layer = makeLayerWith2ObjectsWithOpacity(true); - - var groupObject = layer.toObject(); - groupObject.subTargetCheck = true; - - fabric.Layer.fromObject(groupObject).then(function(newGroupFromObject) { - assert.ok(newGroupFromObject._objects[0].lineCoords.tl, 'acoords 0 are restored'); - assert.ok(newGroupFromObject._objects[1].lineCoords.tl, 'acoords 1 are restored'); - done(); - }); - }); - - QUnit.test('toSVG', function(assert) { - var layer = makeLayerWith2Objects(true); - assert.ok(typeof layer.toSVG === 'function'); - var expectedSVG = '\n\n\t\t\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n\n'; - assert.equal(layer.group.toSVG(), expectedSVG); - }); - - QUnit.test('toSVG with a clipPath', function(assert) { - var layer = makeLayerWith2Objects(true); - layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); - var expectedSVG = '\n\n\t\t\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n\n'; - assert.equal(layer.group.toSVG(), expectedSVG); - }); - - QUnit.test('toSVG with a clipPath absolutePositioned', function(assert) { - var layer = makeLayerWith2Objects(true); - layer.clipPath = new fabric.Rect({ width: 100, height: 100 }); - layer.clipPath.absolutePositioned = true; - console.log(JSON.stringify(layer.group.toSVG())) - var expectedSVG = '\n\n\n\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n\n'; - assert.equal(layer.toSVG(), expectedSVG); - }); - - QUnit.test('toSVG with a layer as a clipPath', function(assert) { - var layer = makeLayerWith2Objects(true); - layer.clipPath = makeLayerWith2Objects(); - var expectedSVG = '\n\n\t\t\n\t\t\n\n\n\t\t\n\n\n\t\t\n\n\n\n\n'; - assert.equal(layer.toSVG(), expectedSVG); - }); - - QUnit.test('cloning layer with 2 objects', function(assert) { - var done = assert.async(); - var layer = makeLayerWith2Objects(true); - layer.clone().then(function(clone) { - assert.ok(clone !== layer); - assert.deepEqual(clone.toObject(), layer.toObject()); - done(); - }); - }); - - QUnit.test('get with locked objects', function(assert) { - var layer = makeLayerWith2Objects(); - - assert.equal(layer.get('lockMovementX'), false); - - // TODO acitveGroup - // layer.getObjects()[0].lockMovementX = true; - // assert.equal(layer.get('lockMovementX'), true); - // - // layer.getObjects()[0].lockMovementX = false; - // assert.equal(layer.get('lockMovementX'), false); - - layer.set('lockMovementX', true); - assert.equal(layer.get('lockMovementX'), true); - - // layer.set('lockMovementX', false); - // layer.getObjects()[0].lockMovementY = true; - // layer.getObjects()[1].lockRotation = true; - // - // assert.equal(layer.get('lockMovementY'), true); - // assert.equal(layer.get('lockRotation'), true); - }); - - QUnit.test('z-index methods with layer objects', function(assert) { - - var textBg = new fabric.Rect({ - fill: '#abc', - width: 100, - height: 100 - }); - - var text = new fabric.Text('text'); - var layer = new fabric.Layer([textBg, text]); - - canvas.add(layer); - - assert.ok(layer.getObjects()[0] === textBg); - assert.ok(layer.getObjects()[1] === text); - - textBg.bringToFront(); - - assert.ok(layer.getObjects()[0] === text); - assert.ok(layer.getObjects()[1] === textBg); - - textBg.sendToBack(); - - assert.ok(layer.getObjects()[0] === textBg); - assert.ok(layer.getObjects()[1] === text); - }); - - QUnit.test('layer reference on an object', function(assert) { - var layer = makeLayerWith2Objects(); - var firstObjInGroup = layer.getObjects()[0]; - var secondObjInGroup = layer.getObjects()[1]; - - assert.equal(firstObjInGroup.group, layer); - assert.equal(secondObjInGroup.group, layer); - - layer.remove(firstObjInGroup); - assert.ok(typeof firstObjInGroup.group === 'undefined'); - }); - - QUnit.test('insertAt', function (assert) { - var rect1 = new fabric.Rect({ id: 1 }), - rect2 = new fabric.Rect({ id: 2 }), - rect3 = new fabric.Rect({ id: 3 }), - rect4 = new fabric.Rect({ id: 4 }), - rect5 = new fabric.Rect({ id: 5 }), - rect6 = new fabric.Rect({ id: 6 }), - rect7 = new fabric.Rect({ id: 7 }), - rect8 = new fabric.Rect({ id: 8 }), - layer = new fabric.Layer(), - control = [], - fired = [], - firingControl = []; - - layer.add(rect1, rect2); - control.push(rect1, rect2); - - assert.ok(typeof layer.insertAt === 'function', 'should respond to `insertAt` method'); - - const equalsControl = (description) => { - assert.deepEqual(layer.getObjects().map(o => o.id), control.map(o => o.id), 'should equal control array ' + description); - assert.deepEqual(layer.getObjects(), control, 'should equal control array ' + description); - assert.deepEqual(fired.map(o => o.id), firingControl.map(o => o.id), 'fired events should equal control array ' + description); - assert.deepEqual(fired, firingControl, 'fired events should equal control array ' + description); - } - - assert.ok(typeof layer._onObjectAdded === 'function', 'has a standard _onObjectAdded method'); - [rect1, rect2, rect3, rect4, rect5, rect6, rect7, rect8].forEach(obj => { - obj.on('added', e => { - assert.equal(e.target, layer); - fired.push(obj); - }); - }); - - layer.insertAt(rect3, 1); - control.splice(1, 0, rect3); - firingControl.push(rect3); - equalsControl('rect3'); - layer.insertAt(rect4, 0); - control.splice(0, 0, rect4); - firingControl.push(rect4); - equalsControl('rect4'); - layer.insertAt(rect5, 2); - control.splice(2, 0, rect5); - firingControl.push(rect5); - equalsControl('rect5'); - layer.insertAt([rect6], 2); - control.splice(2, 0, rect6); - firingControl.push(rect6); - equalsControl('rect6'); - layer.insertAt([rect7, rect8], 3); - control.splice(3, 0, rect7, rect8); - firingControl.push(rect7, rect8); - equalsControl('rect7'); - }); - - QUnit.test('dirty flag propagation from children up', function(assert) { - var g1 = makeLayerWith4Objects(); - var obj = g1.item(0); - g1.dirty = false; - obj.dirty = false; - g1.ownCaching = true; - assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); - obj.set('fill', 'red'); - assert.equal(obj.dirty, true, 'Obj has dirty flag set'); - assert.equal(g1.dirty, true, 'Layer has dirty flag set'); - }); - - QUnit.test('dirty flag propagation from children up is stopped if layer is not caching', function(assert) { - var g1 = makeLayerWith4Objects(); - var obj = g1.item(0); - g1.dirty = false; - obj.dirty = false; - g1.ownCaching = false; - assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); - obj.set('fill', 'red'); - assert.equal(obj.dirty, true, 'Obj has dirty flag set'); - assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); - }); - - QUnit.test('dirty flag propagation from children up does not happen if value does not change really', function(assert) { - var g1 = makeLayerWith4Objects(); - var obj = g1.item(0); - obj.fill = 'red'; - g1.dirty = false; - obj.dirty = false; - g1.ownCaching = true; - assert.equal(obj.dirty, false, 'Obj has no dirty flag set'); - assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); - obj.set('fill', 'red'); - assert.equal(obj.dirty, false, 'Obj has no dirty flag set'); - assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); - }); - - QUnit.test('dirty flag propagation from children up with', function (assert) { - var g1 = makeLayerWith4Objects(); - var obj = g1.item(0); - g1.dirty = false; - obj.dirty = false; - // specify that the layer is caching or the test will fail under node since the - // object caching is disabled by default - g1.ownCaching = true; - assert.equal(g1.dirty, false, 'Layer has no dirty flag set'); - obj.set('angle', 5); - assert.equal(obj.dirty, false, 'Obj has dirty flag still false'); - assert.equal(g1.dirty, true, 'Layer has dirty flag set'); - }); - - QUnit.test('_getCacheCanvasDimensions returns dimensions and zoom for cache canvas are influenced by layer', function(assert) { - var g1 = makeLayerWith4Objects(); - var obj = g1.item(0); - var dims = obj._getCacheCanvasDimensions(); - g1.scaleX = 2; - var dims2 = obj._getCacheCanvasDimensions(); - assert.equal((dims2.width - 2), (dims.width - 2) * g1.scaleX, 'width of cache has increased with layer scale'); - }); - - QUnit.test('test layer - pixels.', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - layer = new fabric.Layer([rect1, rect2], {opacity: 1, fill: '', strokeWidth: 0, objectCaching: false}), - isTransparent = fabric.util.isTransparent, - ctx = canvas.contextContainer; - canvas.add(layer); - canvas.renderAll(); - assert.equal(canvas.enableRetinaScaling, false, 'enable retina scaling is off'); - assert.equal(isTransparent(ctx, 0, 0, 0), true, '0,0 is transparent'); - assert.equal(isTransparent(ctx, 1, 1, 0), false, '1,1 is opaque'); - assert.equal(isTransparent(ctx, 2, 2, 0), false, '2,2 is opaque'); - assert.equal(isTransparent(ctx, 3, 3, 0), true, '3,3 is transparent'); - assert.equal(isTransparent(ctx, 4, 4, 0), true, '4,4 is transparent'); - assert.equal(isTransparent(ctx, 5, 5, 0), false, '5,5 is opaque'); - assert.equal(isTransparent(ctx, 6, 6, 0), false, '6,6 is opaque'); - assert.equal(isTransparent(ctx, 7, 7, 0), true, '7,7 is transparent'); - }); - - QUnit.test('layer add', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - layer = new fabric.Layer([rect1]), - group = new fabric.Group([layer], {subTargetCheck:true}); - - var coords = layer.oCoords; - layer.add(rect2); - var newCoords = layer.oCoords; - assert.notEqual(coords, newCoords, 'object coords have been recalculated - add'); - }); - - QUnit.test('layer remove', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - layer = new fabric.Layer([rect1, rect2]); - - var coords = layer.oCoords; - layer.remove(rect2); - var newCoords = layer.oCoords; - assert.notEqual(coords, newCoords, 'object coords have been recalculated - remove'); - }); - - QUnit.test('layer willDrawShadow', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect3 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect4 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - layer = new fabric.Layer([rect1, rect2]), - group2 = new fabric.Layer([rect3, rect4]), - group3 = new fabric.Layer([layer, group2]); - - assert.equal(group3.willDrawShadow(), false, 'layer will not cast shadow because objects do not have it'); - group3.shadow = { offsetX: 1, offsetY: 2, }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow'); - delete group3.shadow; - group2.shadow = { offsetX: 1, offsetY: 2, }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because inner group2 has shadow'); - delete group2.shadow; - rect1.shadow = { offsetX: 1, offsetY: 2, }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because inner rect1 has shadow'); - assert.equal(layer.willDrawShadow(), true, 'layer will cast shadow because inner rect1 has shadow'); - assert.equal(group2.willDrawShadow(), false, 'layer will not cast shadow because no child has shadow'); - }); - - QUnit.test('layer willDrawShadow with no offsets', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect3 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - rect4 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: false}), - layer = new fabric.Layer([rect1, rect2]), - group2 = new fabric.Layer([rect3, rect4]), - group3 = new fabric.Layer([layer, group2]); - - assert.equal(group3.willDrawShadow(), false, 'layer will not cast shadow because objects do not have it'); - group3.shadow = { offsetX: 0, offsetY: 0 }; - assert.equal(group3.willDrawShadow(), false, 'layer will NOT cast shadow because layer itself has shadow but not offsets'); - group3.shadow = { offsetX: 2, offsetY: 0 }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetX different than 0'); - group3.shadow = { offsetX: 0, offsetY: 2 }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetY different than 0'); - group3.shadow = { offsetX: -2, offsetY: 0 }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetX different than 0'); - group3.shadow = { offsetX: 0, offsetY: -2 }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself has shadow and one offsetY different than 0'); - rect1.shadow = { offsetX: 1, offsetY: 2, }; - group3.shadow = { offsetX: 0, offsetY: 0 }; - assert.equal(group3.willDrawShadow(), true, 'layer will cast shadow because layer itself will not, but rect 1 will'); - - }); - - QUnit.test('layer shouldCache', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - rect3 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - rect4 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - layer = new fabric.Layer([rect1, rect2], { objectCaching: true}), - group2 = new fabric.Layer([rect3, rect4], { objectCaching: true}), - group3 = new fabric.Layer([layer, group2], { objectCaching: true}); - - assert.equal(group3.shouldCache(), true, 'group3 will cache because no child has shadow'); - assert.equal(group2.shouldCache(), false, 'group2 will not cache because is drawing on parent group3 cache'); - assert.equal(rect3.shouldCache(), false, 'rect3 will not cache because is drawing on parent2 layer cache'); - - group2.shadow = { offsetX: 2, offsetY: 0 }; - rect1.shadow = { offsetX: 0, offsetY: 2 }; - - assert.equal(group3.shouldCache(), false, 'group3 will cache because children have shadow'); - assert.equal(group2.shouldCache(), true, 'group2 will cache because is not drawing on parent group3 cache and no children have shadow'); - assert.equal(layer.shouldCache(), false, 'layer will not cache because even if is not drawing on parent group3 cache children have shadow'); - - assert.equal(rect1.shouldCache(), true, 'rect1 will cache because none of its parent is caching'); - assert.equal(rect3.shouldCache(), false, 'rect3 will not cache because group2 is caching'); - - }); - - QUnit.test('canvas prop propagation with set', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - layer = new fabric.Layer([rect1, rect2]); - - layer.set('canvas', 'a-canvas'); - assert.equal(layer.canvas, 'a-canvas', 'canvas has been set'); - assert.equal(layer._objects[0].canvas, 'a-canvas', 'canvas has been set on object 0'); - assert.equal(layer._objects[1].canvas, 'a-canvas', 'canvas has been set on object 1'); - }); - - QUnit.test('canvas prop propagation with add', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - layer = new fabric.Layer([rect1, rect2]); - - canvas.add(layer); - assert.equal(layer.canvas, canvas, 'canvas has been set'); - assert.equal(layer._objects[0].canvas, canvas, 'canvas has been set on object 0'); - assert.equal(layer._objects[1].canvas, canvas, 'canvas has been set on object 1'); - }); - - QUnit.test('canvas prop propagation with add to layer', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 2, strokeWidth: 0, fill: 'red', opacity: 1, objectCaching: true}), - layer = new fabric.Layer(); - - canvas.add(layer); - assert.equal(layer.canvas, canvas, 'canvas has been set'); - layer.add(rect1); - assert.equal(layer._objects[0].canvas, canvas, 'canvas has been set on object 0'); - layer.add(rect2); - assert.equal(layer._objects[1].canvas, canvas, 'canvas has been set on object 0'); - }); - - QUnit.test.only('add and coordinates', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), - layer = new fabric.Layer([]), - group = new fabric.Group([layer]); - layer.add(rect1); - layer.add(rect2); - group.left = 5; - group.top = 5; - group.scaleX = 3; - group.scaleY = 2; - group.triggerLayout(); - layer.removeAll(); - assert.equal(rect1.top, 5, 'top has been moved'); - assert.equal(rect1.left, 11, 'left has been moved'); - assert.equal(rect1.scaleX, 3, 'scaleX has been scaled'); - assert.equal(rect1.scaleY, 2, 'scaleY has been scaled'); - assert.equal(rect2.top, 13, 'top has been moved'); - assert.equal(rect2.left, 23, 'left has been moved'); - assert.equal(rect2.scaleX, 2, 'scaleX has been scaled inverted because of angle 90'); - assert.equal(rect2.scaleY, 3, 'scaleY has been scaled inverted because of angle 90'); - }); - - QUnit.skip('addRelativeToGroup and coordinates with nested groups', function(assert) { - var rect1 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), - rect2 = new fabric.Rect({ top: 5, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), - group0 = new fabric.Layer([rect1, rect2]), - rect3 = new fabric.Rect({ top: 2, left: 9, width: 3, height: 2, strokeWidth: 0, fill: 'red' }), - rect4 = new fabric.Rect({ top: 3, left: 5, width: 2, height: 6, angle: 90, strokeWidth: 0, fill: 'red' }), - group1 = new fabric.Layer([rect3, rect4], { scaleX: 3, scaleY: 4 }), - layer = new fabric.Layer([group0, group1], { angle: 90, scaleX: 2, scaleY: 0.5 }), - rect5 = new fabric.Rect({ top: 1, left: 1, width: 3, height: 2, strokeWidth: 0, fill: 'red' }); - - group1.addRelativeToGroup(rect5); - var t = group1.calcTransformMatrix(); - var pos = fabric.util.transformPoint(new fabric.Point(rect5.left, rect5.top), t); - assert.equal(rect5.top, -5.5, 'top has been moved'); - assert.equal(rect5.left, -19.5, 'left has been moved'); - assert.equal(rect5.scaleX, 2, 'scaleX has been scaled'); - assert.equal(rect5.scaleY, 0.5, 'scaleY has been scaled'); - layer.removeAll(); - group1.removeAll(); - assert.equal(rect5.top, 1, 'top is back to original minus rounding errors'); - assert.equal(rect5.left, 1, 'left is back to original'); - assert.equal(rect5.scaleX, 1, 'scaleX is back to original'); - assert.equal(rect5.scaleY, 1, 'scaleY is back to original'); - }); - - -})(); From 5ff32e5e1cbd7f5b4b8f628d79ef5faa99b9cf56 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 09:43:53 +0300 Subject: [PATCH 73/77] Update object_geometry.mixin.js --- src/mixins/object_geometry.mixin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index c1a796cc418..541a2ee0476 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -735,6 +735,8 @@ * @param {Number} [options.scaleY] * @param {Number} [options.skewX] * @param {Number} [options.skewY] + * @param {Number} [options.width] + * @param {Number} [options.height] * @private * @returns {fabric.Point} dimensions */ From 4c0c142335132fa5a91f4bf4c1aadc59e1fde823 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 09:44:10 +0300 Subject: [PATCH 74/77] fix(): pass options to `_getTransformedDimensions` --- src/shapes/layer.class.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index c18673b33b9..ccb2d9f839d 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -136,8 +136,8 @@ * @override we want instance to fill parent so we disregard transformations * @returns {fabric.Point} dimensions */ - _getTransformedDimensions: function () { - return this.callSuper('_getTransformedDimensions', { + _getTransformedDimensions: function (options) { + return this.callSuper('_getTransformedDimensions', Object.assign({ scaleX: 1, scaleY: 1, skewX: 0, @@ -145,7 +145,7 @@ width: this.width, height: this.height, strokeWidth: 0 - }); + }, options)); }, /** From 23416d174e71b49bb7f39524d05f595f8b36495d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 11:03:23 +0300 Subject: [PATCH 75/77] Revert "fix(): pass options to `_getTransformedDimensions`" This reverts commit 4c0c142335132fa5a91f4bf4c1aadc59e1fde823. --- src/shapes/layer.class.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index ccb2d9f839d..c18673b33b9 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -136,8 +136,8 @@ * @override we want instance to fill parent so we disregard transformations * @returns {fabric.Point} dimensions */ - _getTransformedDimensions: function (options) { - return this.callSuper('_getTransformedDimensions', Object.assign({ + _getTransformedDimensions: function () { + return this.callSuper('_getTransformedDimensions', { scaleX: 1, scaleY: 1, skewX: 0, @@ -145,7 +145,7 @@ width: this.width, height: this.height, strokeWidth: 0 - }, options)); + }); }, /** From 2f27480df12f7823d5853dd8f438d349c9c15db7 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 11:42:24 +0300 Subject: [PATCH 76/77] fix(): getDimensions no `strokeWidth` `_getNonTransformedDimensions` `_getTransformedDimensions` --- src/shapes/group.class.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index c0d08b9e8a4..7008b09de7f 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -859,6 +859,29 @@ // override by subclass }, + + /** + * Calculate object dimensions from its properties + * @override disregard `strokeWidth` + * @private + * @returns {fabric.Point} dimensions + */ + _getNonTransformedDimensions: function () { + return new fabric.Point(this.width, this.height); + }, + + /** + * @private + * @override we want instance to fill parent so we disregard transformations + * @param {Object} [options] + * @param {Number} [options.width] + * @param {Number} [options.height] + * @returns {fabric.Point} dimensions + */ + _getTransformedDimensions: function (options) { + return this.callSuper('_getTransformedDimensions', Object.assign(options || {}, { strokeWidth: 0 })); + }, + /** * * @private From 2a2628614b72cf5d23f6aa9a890cd2d6459b7b97 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 1 May 2022 11:42:47 +0300 Subject: [PATCH 77/77] fix(): `_getTransformedDimensions` account for size only --- src/shapes/layer.class.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/shapes/layer.class.js b/src/shapes/layer.class.js index c18673b33b9..6cfcb122e64 100644 --- a/src/shapes/layer.class.js +++ b/src/shapes/layer.class.js @@ -134,18 +134,17 @@ /** * @private * @override we want instance to fill parent so we disregard transformations + * @param {Object} [options] + * @param {Number} [options.width] + * @param {Number} [options.height] * @returns {fabric.Point} dimensions */ - _getTransformedDimensions: function () { - return this.callSuper('_getTransformedDimensions', { - scaleX: 1, - scaleY: 1, - skewX: 0, - skewY: 0, + _getTransformedDimensions: function (options) { + options = Object.assign({ width: this.width, - height: this.height, - strokeWidth: 0 - }); + height: this.height + }, options || {}); + return new fabric.Point(options.width, options.height); }, /**