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