(function(global){ "use strict"; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, min = fabric.util.array.min, max = fabric.util.array.max, invoke = fabric.util.array.invoke, removeFromArray = fabric.util.removeFromArray; if (fabric.Group) { return; } // lock-related properties, for use in fabric.Group#get // to enable locking behavior on group // when one of its objects has lock-related properties set var _lockProperties = { lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, lockUniScaling: true }; /** * Group class * @class Group * @extends fabric.Object */ fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ { /** * Type of an object * @property * @type String */ type: 'group', /** * Constructor * @method initialized * @param {Object} objects Group objects * @param {Object} [options] Options object * @return {Object} thisArg */ initialize: function(objects, options) { options = options || { }; this._objects = objects || []; for (var i = this._objects.length; i--; ) { this._objects[i].group = this; } this.originalState = { }; this.callSuper('initialize'); this._calcBounds(); this._updateObjectsCoords(); if (options) { extend(this, options); } this._setOpacityIfSame(); // group is active by default this.setCoords(true); this.saveCoords(); //this.activateAllObjects(); }, /** * @private * @method _updateObjectsCoords */ _updateObjectsCoords: function() { var groupDeltaX = this.left, groupDeltaY = this.top; this.forEachObject(function(object) { var objectLeft = object.get('left'), objectTop = object.get('top'); object.set('originalLeft', objectLeft); object.set('originalTop', objectTop); object.set('left', objectLeft - groupDeltaX); object.set('top', objectTop - groupDeltaY); object.setCoords(); // do not display corners of objects enclosed in a group object.__origHasControls = object.hasControls; object.hasControls = false; }, this); }, /** * Returns string represenation of a group * @method toString * @return {String} */ toString: function() { return '#'; }, /** * Returns an array of all objects in this group * @method getObjects * @return {Array} group objects */ getObjects: function() { return this._objects; }, /** * Adds an object to a group; Then recalculates group's dimension, position. * @method addWithUpdate * @param {Object} object * @return {fabric.Group} thisArg * @chainable */ addWithUpdate: function(object) { this._restoreObjectsState(); this._objects.push(object); object.group = this; this._calcBounds(); this._updateObjectsCoords(); return this; }, /** * Removes an object from a group; Then recalculates group's dimension, position. * @method removeWithUpdate * @param {Object} object * @return {fabric.Group} thisArg * @chainable */ removeWithUpdate: function(object) { this._restoreObjectsState(); removeFromArray(this._objects, object); delete object.group; object.setActive(false); this._calcBounds(); this._updateObjectsCoords(); return this; }, /** * Adds an object to a group * @method add * @param {Object} object * @return {fabric.Group} thisArg * @chainable */ add: function(object) { this._objects.push(object); object.group = this; return this; }, /** * Removes an object from a group * @method remove * @param {Object} object * @return {fabric.Group} thisArg * @chainable */ remove: function(object) { removeFromArray(this._objects, object); delete object.group; return this; }, /** * Returns a size of a group (i.e: length of an array containing its objects) * @return {Number} Group size */ size: function() { return this.getObjects().length; }, /** * @param delegatedProperties * @type Object * Properties that are delegated to group objects when reading/writing */ delegatedProperties: { fill: true, opacity: true, fontFamily: true, fontWeight: true, fontSize: true, fontStyle: true, lineHeight: true, textDecoration: true, textShadow: true, textAlign: true, backgroundColor: true }, /** * @private */ _set: function(key, value) { if (key in this.delegatedProperties) { var i = this._objects.length; this[key] = value; while (i--) { this._objects[i].set(key, value); } } else { this[key] = value; } }, /** * Returns true if a group contains an object * @method contains * @param {Object} object Object to check against * @return {Boolean} `true` if group contains an object */ contains: function(object) { return this._objects.indexOf(object) > -1; }, /** * Returns object representation of an instance * @method toObject * @param {Array} propertiesToInclude * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return extend(this.callSuper('toObject', propertiesToInclude), { objects: invoke(this._objects, 'toObject', propertiesToInclude) }); }, /** * Renders instance on a given context * @method render * @param {CanvasRenderingContext2D} ctx context to render instance on */ render: function(ctx, noTransform) { // do not render if object is not visible if (!this.visible) return; ctx.save(); this.transform(ctx); var groupScaleFactor = Math.max(this.scaleX, this.scaleY); this.clipTo && fabric.util.clipContext(this, ctx); //The array is now sorted in order of highest first, so start from end. for (var i = this._objects.length; i > 0; i--) { var object = this._objects[i-1], originalScaleFactor = object.borderScaleFactor, originalHasRotatingPoint = object.hasRotatingPoint; // do not render if object is not visible if (!object.visible) continue; object.borderScaleFactor = groupScaleFactor; object.hasRotatingPoint = false; object.render(ctx); object.borderScaleFactor = originalScaleFactor; object.hasRotatingPoint = originalHasRotatingPoint; } this.clipTo && ctx.restore(); if (!noTransform && this.active) { this.drawBorders(ctx); this.drawControls(ctx); } ctx.restore(); this.setCoords(); }, /** * Returns object from the group at the specified index * @method item * @param index {Number} index of item to get * @return {fabric.Object} */ item: function(index) { return this.getObjects()[index]; }, /** * Returns complexity of an instance * @method complexity * @return {Number} complexity */ complexity: function() { return this.getObjects().reduce(function(total, object) { total += (typeof object.complexity === 'function') ? object.complexity() : 0; return total; }, 0); }, /** * Retores original state of each of group objects (original state is that which was before group was created). * @private * @method _restoreObjectsState * @return {fabric.Group} thisArg * @chainable */ _restoreObjectsState: function() { this._objects.forEach(this._restoreObjectState, this); return this; }, /** * Restores original state of a specified object in group * @private * @method _restoreObjectState * @param {fabric.Object} object * @return {fabric.Group} thisArg */ _restoreObjectState: function(object) { var groupLeft = this.get('left'), groupTop = this.get('top'), groupAngle = this.getAngle() * (Math.PI / 180), rotatedTop = Math.cos(groupAngle) * object.get('top') + Math.sin(groupAngle) * object.get('left'), rotatedLeft = -Math.sin(groupAngle) * object.get('top') + Math.cos(groupAngle) * object.get('left'); object.setAngle(object.getAngle() + this.getAngle()); object.set('left', groupLeft + rotatedLeft * this.get('scaleX')); object.set('top', groupTop + rotatedTop * this.get('scaleY')); object.set('scaleX', object.get('scaleX') * this.get('scaleX')); object.set('scaleY', object.get('scaleY') * this.get('scaleY')); object.setCoords(); object.hasControls = object.__origHasControls; delete object.__origHasControls; object.setActive(false); object.setCoords(); delete object.group; return this; }, /** * Destroys a group (restoring state of its objects) * @method destroy * @return {fabric.Group} thisArg * @chainable */ destroy: function() { return this._restoreObjectsState(); }, /** * Saves coordinates of this instance (to be used together with `hasMoved`) * @saveCoords * @return {fabric.Group} thisArg * @chainable */ saveCoords: function() { this._originalLeft = this.get('left'); this._originalTop = this.get('top'); return this; }, /** * Checks whether this group was moved (since `saveCoords` was called last) * @method hasMoved * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) */ hasMoved: function() { return this._originalLeft !== this.get('left') || this._originalTop !== this.get('top'); }, /** * Sets coordinates of all group objects * @method setObjectsCoords * @return {fabric.Group} thisArg * @chainable */ setObjectsCoords: function() { this.forEachObject(function(object) { object.setCoords(); }); return this; }, /** * Activates (makes active) all group objects * @method activateAllObjects * @return {fabric.Group} thisArg * @chainable */ activateAllObjects: function() { this.forEachObject(function(object) { object.setActive(); }); return this; }, /** * Executes given function for each object in this group * @method forEachObject * @param {Function} callback * Callback invoked with current object as first argument, * index - as second and an array of all objects - as third. * Iteration happens in reverse order (for performance reasons). * Callback is invoked in a context of Global Object (e.g. `window`) * when no `context` argument is given * * @param {Object} context Context (aka thisObject) * * @return {fabric.Group} thisArg * @chainable */ forEachObject: fabric.StaticCanvas.prototype.forEachObject, /** * @private * @method _setOpacityIfSame */ _setOpacityIfSame: function() { var objects = this.getObjects(), firstValue = objects[0] ? objects[0].get('opacity') : 1; var isSameOpacity = objects.every(function(o) { return o.get('opacity') === firstValue; }); if (isSameOpacity) { this.opacity = firstValue; } }, /** * @private * @method _calcBounds */ _calcBounds: function() { var aX = [], aY = [], minX, minY, maxX, maxY, o, width, height, i = 0, len = this._objects.length; for (; i < len; ++i) { o = this._objects[i]; o.setCoords(); for (var prop in o.oCoords) { aX.push(o.oCoords[prop].x); aY.push(o.oCoords[prop].y); } } minX = min(aX); maxX = max(aX); minY = min(aY); maxY = max(aY); width = (maxX - minX) || 0; height = (maxY - minY) || 0; this.width = width; this.height = height; this.left = (minX + width / 2) || 0; this.top = (minY + height / 2) || 0; }, /** * Checks if point is contained within the group * @method containsPoint * @param {fabric.Point} point point with `x` and `y` properties * @return {Boolean} true if point is contained within group */ containsPoint: function(point) { var halfWidth = this.get('width') / 2, halfHeight = this.get('height') / 2, centerX = this.get('left'), centerY = this.get('top'); return centerX - halfWidth < point.x && centerX + halfWidth > point.x && centerY - halfHeight < point.y && centerY + halfHeight > point.y; }, /** * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) * @method toGrayscale * @return {fabric.Group} thisArg * @chainable */ toGrayscale: function() { var i = this._objects.length; while (i--) { this._objects[i].toGrayscale(); } return this; }, /** * Returns svg representation of an instance * @method toSVG * @return {String} svg representation of an instance */ toSVG: function() { var objectsMarkup = [ ]; for (var i = this._objects.length; i--; ) { objectsMarkup.push(this._objects[i].toSVG()); } return ( '' + objectsMarkup.join('') + ''); }, /** * Returns requested property * @method get * @param {String} prop Property to get * @return {Any} */ get: function(prop) { if (prop in _lockProperties) { if (this[prop]) { return this[prop]; } else { for (var i = 0, len = this._objects.length; i < len; i++) { if (this._objects[i][prop]) { return true; } } return false; } } else { if (prop in this.delegatedProperties) { return this._objects[0] && this._objects[0].get(prop); } return this[prop]; } } }); /** * Returns {@link fabric.Group} instance from an object representation * @static * @method fabric.Group.fromObject * @param {Object} object Object to create a group from * @param {Object} [options] Options object * @return {fabric.Group} An instance of fabric.Group */ fabric.Group.fromObject = function(object, callback) { fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { delete object.objects; callback && callback(new fabric.Group(enlivenedObjects, object)); }); }; /** * Indicates that instances of this type are async * @static * @type Boolean */ fabric.Group.async = true; })(typeof exports !== 'undefined' ? exports : this);