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 })();