//= require "object.class" (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; } /** * @class Group * @extends fabric.Object */ fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ { /** * @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) { this.objects = objects || []; 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.hideCorners = true; }, 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); 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); 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); 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); 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; }, /** * Sets property to a given value * @method set * @param {String} name * @param {Object|Function} value * @return {fabric.Group} thisArg * @chainable */ set: function(name, value) { if (typeof value == 'function') { // recurse this.set(name, value(this[name])); } else { if (name === 'fill' || name === 'opacity') { var i = this.objects.length; this[name] = value; while (i--) { this.objects[i].set(name, value); } } else { this[name] = value; } } return this; }, /** * 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 * @return {Object} object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { objects: invoke(this.objects, 'clone') }); }, /** * Renders instance on a given context * @method render * @param {CanvasRenderingContext2D} ctx context to render instance on */ render: function(ctx, noTransform) { ctx.save(); this.transform(ctx); var groupScaleFactor = Math.max(this.scaleX, this.scaleY); for (var i = 0, len = this.objects.length, object; object = this.objects[i]; i++) { var originalScaleFactor = object.borderScaleFactor; object.borderScaleFactor = groupScaleFactor; object.render(ctx); object.borderScaleFactor = originalScaleFactor; } if (!noTransform && this.active) { this.drawBorders(ctx); this.hideCorners || this.drawCorners(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), objectLeft = object.get('originalLeft'), objectTop = object.get('originalTop'), 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.hideCorners = false; object.setActive(false); object.setCoords(); 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 */ toGrayscale: function() { var i = this.objects.length; while (i--) { this.objects[i].toGrayscale(); } } }); /** * Returns fabric.Group instance from an object representation * @static * @method fabric.Group.fromObject * @param object {Object} object to create a group from * @param options {Object} 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)); }); }; fabric.Group.async = true; })(typeof exports != 'undefined' ? exports : this);