      1 //= require "object.class"
      2
      3 (function(){
      4
      5   var fabric = this.fabric || (this.fabric = { }),
      6       extend = fabric.util.object.extend,
      7       min = fabric.util.array.min,
      8       max = fabric.util.array.max,
      9       invoke = fabric.util.array.invoke,
     10       removeFromArray = fabric.util.removeFromArray;
     11
     12   if (fabric.Group) {
     13     return;
     14   }
     15
     16   /**
     17    * @class Group
     18    * @extends fabric.Object
     19    */
     20   fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ {
     21
     22     /**
     23      * @property
     24      * @type String
     25      */
     26     type: 'group',
     27
     28     /**
     29      * Constructor
     30      * @method initialized
     31      * @param {Object} objects Group objects
     32      * @param {Object} [options] Options object
     33      * @return {Object} thisArg
     34      */
     35     initialize: function(objects, options) {
     36       this.objects = objects || [];
     37       this.originalState = { };
     38
     39       this.callSuper('initialize');
     40
     41       this._calcBounds();
     42       this._updateObjectsCoords();
     43
     44       if (options) {
     45         extend(this, options);
     46       }
     47       this._setOpacityIfSame();
     48
     49       // group is active by default
     50       this.setCoords(true);
     51       this.saveCoords();
     52
     53       this.activateAllObjects();
     54     },
     55
     56     /**
     57      * @private
     58      * @method _updateObjectsCoords
     59      */
     60     _updateObjectsCoords: function() {
     61       var groupDeltaX = this.left,
     62           groupDeltaY = this.top;
     63
     64       this.forEachObject(function(object) {
     65
     66         var objectLeft = object.get('left'),
     67             objectTop = object.get('top');
     68
     69         object.set('originalLeft', objectLeft);
     70         object.set('originalTop', objectTop);
     71
     72         object.set('left', objectLeft - groupDeltaX);
     73         object.set('top', objectTop - groupDeltaY);
     74
     75         object.setCoords();
     76
     77         // do not display corners of objects enclosed in a group
     78         object.hideCorners = true;
     79       }, this);
     80     },
     81
     82     /**
     83      * Returns string represenation of a group
     84      * @method toString
     85      * @return {String}
     86      */
     87     toString: function() {
     88       return '#<fabric.Group: (' + this.complexity() + ')>';
     89     },
     90
     91     /**
     92      * Returns an array of all objects in this group
     93      * @method getObjects
     94      * @return {Array} group objects
     95      */
     96     getObjects: function() {
     97       return this.objects;
     98     },
     99
    100     /**
    101      * Adds an object to a group; Then recalculates group's dimension, position.
    102      * @method add
    103      * @param {Object} object
    104      * @return {fabric.Group} thisArg
    105      * @chainable
    106      */
    107     add: function(object) {
    108       this._restoreObjectsState();
    109       this.objects.push(object);
    110       object.setActive(true);
    111       this._calcBounds();
    112       this._updateObjectsCoords();
    113       return this;
    114     },
    115
    116     /**
    117      * Removes an object from a group; Then recalculates group's dimension, position.
    118      * @param {Object} object
    119      * @return {fabric.Group} thisArg
    120      * @chainable
    121      */
    122     remove: function(object) {
    123       this._restoreObjectsState();
    124       removeFromArray(this.objects, object);
    125       object.setActive(false);
    126       this._calcBounds();
    127       this._updateObjectsCoords();
    128       return this;
    129     },
    130
    131     /**
    132      * Returns a size of a group (i.e: length of an array containing its objects)
    133      * @return {Number} Group size
    134      */
    135     size: function() {
    136       return this.getObjects().length;
    137     },
    138
    139     /**
    140      * Sets property to a given value
    141      * @method set
    142      * @param {String} name
    143      * @param {Object|Function} value
    144      * @return {fabric.Group} thisArg
    145      * @chainable
    146      */
    147     set: function(name, value) {
    148       if (typeof value == 'function') {
    149         // recurse
    150         this.set(name, value(this[name]));
    151       }
    152       else {
    153         if (name === 'fill' || name === 'opacity') {
    154           var i = this.objects.length;
    155           this[name] = value;
    156           while (i--) {
    157             this.objects[i].set(name, value);
    158           }
    159         }
    160         else {
    161           this[name] = value;
    162         }
    163       }
    164       return this;
    165     },
    166
    167     /**
    168      * Returns true if a group contains an object
    169      * @method contains
    170      * @param {Object} object Object to check against
    171      * @return {Boolean} `true` if group contains an object
    172      */
    173     contains: function(object) {
    174       return this.objects.indexOf(object) > -1;
    175     },
    176
    177     /**
    178      * Returns object representation of an instance
    179      * @method toObject
    180      * @return {Object} object representation of an instance
    181      */
    182     toObject: function() {
    183       return extend(this.callSuper('toObject'), {
    184         objects: invoke(this.objects, 'clone')
    185       });
    186     },
    187
    188     /**
    189      * Renders instance on a given context
    190      * @method render
    191      * @param {CanvasRenderingContext2D} ctx context to render instance on
    192      */
    193     render: function(ctx) {
    194       ctx.save();
    195       this.transform(ctx);
    196
    197       var groupScaleFactor = Math.max(this.scaleX, this.scaleY);
    198
    199       for (var i = 0, len = this.objects.length, object; object = this.objects[i]; i++) {
    200         var originalScaleFactor = object.borderScaleFactor;
    201         object.borderScaleFactor = groupScaleFactor;
    202         object.render(ctx);
    203         object.borderScaleFactor = originalScaleFactor;
    204       }
    205       this.hideBorders || this.drawBorders(ctx);
    206       this.hideCorners || this.drawCorners(ctx);
    207       ctx.restore();
    208       this.setCoords();
    209     },
    210
    211     /**
    212      * Returns object from the group at the specified index
    213      * @method item
    214      * @param index {Number} index of item to get
    215      * @return {fabric.Object}
    216      */
    217     item: function(index) {
    218       return this.getObjects()[index];
    219     },
    220
    221     /**
    222      * Returns complexity of an instance
    223      * @method complexity
    224      * @return {Number} complexity
    225      */
    226     complexity: function() {
    227       return this.getObjects().reduce(function(total, object) {
    228         total += (typeof object.complexity == 'function') ? object.complexity() : 0;
    229         return total;
    230       }, 0);
    231     },
    232
    233     /**
    234      * Retores original state of each of group objects (original state is that which was before group was created).
    235      * @private
    236      * @method _restoreObjectsState
    237      * @return {fabric.Group} thisArg
    238      * @chainable
    239      */
    240     _restoreObjectsState: function() {
    241       this.objects.forEach(this._restoreObjectState, this);
    242       return this;
    243     },
    244
    245     /**
    246      * Restores original state of a specified object in group
    247      * @private
    248      * @method _restoreObjectState
    249      * @param {fabric.Object} object
    250      * @return {fabric.Group} thisArg
    251      */
    252     _restoreObjectState: function(object) {
    253
    254       var groupLeft = this.get('left'),
    255           groupTop = this.get('top'),
    256           groupAngle = this.getAngle() * (Math.PI / 180),
    257           objectLeft = object.get('originalLeft'),
    258           objectTop = object.get('originalTop'),
    259           rotatedTop = Math.cos(groupAngle) * object.get('top') + Math.sin(groupAngle) * object.get('left'),
    260           rotatedLeft = -Math.sin(groupAngle) * object.get('top') + Math.cos(groupAngle) * object.get('left');
    261
    262       object.setAngle(object.getAngle() + this.getAngle());
    263
    264       object.set('left', groupLeft + rotatedLeft * this.get('scaleX'));
    265       object.set('top', groupTop + rotatedTop * this.get('scaleY'));
    266
    267       object.set('scaleX', object.get('scaleX') * this.get('scaleX'));
    268       object.set('scaleY', object.get('scaleY') * this.get('scaleY'));
    269
    270       object.setCoords();
    271       object.hideCorners = false;
    272       object.setActive(false);
    273       object.setCoords();
    274
    275       return this;
    276     },
    277
    278     /**
    279      * Destroys a group (restoring state of its objects)
    280      * @method destroy
    281      * @return {fabric.Group} thisArg
    282      * @chainable
    283      */
    284     destroy: function() {
    285       return this._restoreObjectsState();
    286     },
    287
    288     /**
    289      * @saveCoords
    290      * @return {fabric.Group} thisArg
    291      * @chainable
    292      */
    293     saveCoords: function() {
    294       this._originalLeft = this.get('left');
    295       this._originalTop = this.get('top');
    296       return this;
    297     },
    298
    299     /**
    300      * @method hasMoved
    301      * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called)
    302      */
    303     hasMoved: function() {
    304       return this._originalLeft !== this.get('left') ||
    305              this._originalTop !== this.get('top');
    306     },
    307
    308     /**
    309      * Sets coordinates of all group objects
    310      * @method setObjectsCoords
    311      * @return {fabric.Group} thisArg
    312      * @chainable
    313      */
    314     setObjectsCoords: function() {
    315       this.forEachObject(function(object) {
    316         object.setCoords();
    317       });
    318       return this;
    319     },
    320
    321     /**
    322      * Activates (makes active) all group objects
    323      * @method activateAllObjects
    324      * @return {fabric.Group} thisArg
    325      * @chainable
    326      */
    327     activateAllObjects: function() {
    328       return this.setActive(true);
    329     },
    330
    331     /**
    332      * Activates (makes active) all group objects
    333      * @method setActive
    334      * @param {Boolean} value `true` to activate object, `false` otherwise
    335      * @return {fabric.Group} thisArg
    336      * @chainable
    337      */
    338     setActive: function(value) {
    339       this.forEachObject(function(object) {
    340         object.setActive(value);
    341       });
    342       return this;
    343     },
    344
    345     /**
    346      * @method forEachObject
    347      * @param {Function} callback
    348      *                   Callback invoked with current object as first argument,
    349      *                   index - as second and an array of all objects - as third.
    350      *                   Iteration happens in reverse order (for performance reasons).
    351      *                   Callback is invoked in a context of Global Object (e.g. `window`)
    352      *                   when no `context` argument is given
    353      *
    354      * @param {Object} context Context (aka thisObject)
    355      *
    356      * @return {fabric.Group} thisArg
    357      * @chainable
    358      */
    359     forEachObject: function(callback, context) {
    360       var objects = this.getObjects(),
    361           i = objects.length;
    362       while (i--) {
    363         callback.call(context, objects[i], i, objects);
    364       }
    365       return this;
    366     },
    367
    368     /**
    369      * @private
    370      * @method _setOpacityIfSame
    371      */
    372     _setOpacityIfSame: function() {
    373       var objects = this.getObjects(),
    374           firstValue = objects[0] ? objects[0].get('opacity') : 1;
    375
    376       var isSameOpacity = objects.every(function(o) {
    377         return o.get('opacity') === firstValue;
    378       });
    379
    380       if (isSameOpacity) {
    381         this.opacity = firstValue;
    382       }
    383     },
    384
    385     /**
    386      * @private
    387      * @method _calcBounds
    388      */
    389     _calcBounds: function() {
    390       var aX = [],
    391           aY = [],
    392           minX, minY, maxX, maxY, o, width, height,
    393           i = 0,
    394           len = this.objects.length;
    395
    396       for (; i < len; ++i) {
    397         o = this.objects[i];
    398         o.setCoords();
    399         for (var prop in o.oCoords) {
    400           aX.push(o.oCoords[prop].x);
    401           aY.push(o.oCoords[prop].y);
    402         }
    403       };
    404
    405       minX = min(aX);
    406       maxX = max(aX);
    407       minY = min(aY);
    408       maxY = max(aY);
    409
    410       width = maxX - minX;
    411       height = maxY - minY;
    412
    413       this.width = width;
    414       this.height = height;
    415
    416       this.left = minX + width / 2;
    417       this.top = minY + height / 2;
    418     },
    419
    420     /**
    421      * @method containsPoint
    422      * @param {Object} point point with `x` and `y` properties
    423      * @return {Boolean} true if point is contained within group
    424      */
    425     containsPoint: function(point) {
    426
    427       var halfWidth = this.get('width') / 2,
    428           halfHeight = this.get('height') / 2,
    429           centerX = this.get('left'),
    430           centerY = this.get('top');
    431
    432       return  centerX - halfWidth < point.x &&
    433               centerX + halfWidth > point.x &&
    434               centerY - halfHeight < point.y &&
    435               centerY + halfHeight > point.y;
    436     },
    437
    438     toGrayscale: function() {
    439       var i = this.objects.length;
    440       while (i--) {
    441         this.objects[i].toGrayscale();
    442       }
    443     }
    444   });
    445
    446   /**
    447    * @static
    448    * @method fabric.Group.fromObject
    449    * @param object {Object} object to create a group from
    450    * @param options {Object} options object
    451    * @return {fabric.Group} an instance of fabric.Group
    452    */
    453   fabric.Group.fromObject = function(object) {
    454     return new fabric.Group(object.objects, object);
    455   }
    456 })();