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 * Saves coordinates of this instance (to be used together with `hasMoved`) 290 * @saveCoords 291 * @return {fabric.Group} thisArg 292 * @chainable 293 */ 294 saveCoords: function() { 295 this._originalLeft = this.get('left'); 296 this._originalTop = this.get('top'); 297 return this; 298 }, 299 300 /** 301 * Checks whether this group was moved (since `saveCoords` was called last) 302 * @method hasMoved 303 * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) 304 */ 305 hasMoved: function() { 306 return this._originalLeft !== this.get('left') || 307 this._originalTop !== this.get('top'); 308 }, 309 310 /** 311 * Sets coordinates of all group objects 312 * @method setObjectsCoords 313 * @return {fabric.Group} thisArg 314 * @chainable 315 */ 316 setObjectsCoords: function() { 317 this.forEachObject(function(object) { 318 object.setCoords(); 319 }); 320 return this; 321 }, 322 323 /** 324 * Activates (makes active) all group objects 325 * @method activateAllObjects 326 * @return {fabric.Group} thisArg 327 * @chainable 328 */ 329 activateAllObjects: function() { 330 return this.setActive(true); 331 }, 332 333 /** 334 * Activates (makes active) all group objects 335 * @method setActive 336 * @param {Boolean} value `true` to activate object, `false` otherwise 337 * @return {fabric.Group} thisArg 338 * @chainable 339 */ 340 setActive: function(value) { 341 this.forEachObject(function(object) { 342 object.setActive(value); 343 }); 344 return this; 345 }, 346 347 /** 348 * Executes given function for each object in this group 349 * @method forEachObject 350 * @param {Function} callback 351 * Callback invoked with current object as first argument, 352 * index - as second and an array of all objects - as third. 353 * Iteration happens in reverse order (for performance reasons). 354 * Callback is invoked in a context of Global Object (e.g. `window`) 355 * when no `context` argument is given 356 * 357 * @param {Object} context Context (aka thisObject) 358 * 359 * @return {fabric.Group} thisArg 360 * @chainable 361 */ 362 forEachObject: function(callback, context) { 363 var objects = this.getObjects(), 364 i = objects.length; 365 while (i--) { 366 callback.call(context, objects[i], i, objects); 367 } 368 return this; 369 }, 370 371 /** 372 * @private 373 * @method _setOpacityIfSame 374 */ 375 _setOpacityIfSame: function() { 376 var objects = this.getObjects(), 377 firstValue = objects[0] ? objects[0].get('opacity') : 1; 378 379 var isSameOpacity = objects.every(function(o) { 380 return o.get('opacity') === firstValue; 381 }); 382 383 if (isSameOpacity) { 384 this.opacity = firstValue; 385 } 386 }, 387 388 /** 389 * @private 390 * @method _calcBounds 391 */ 392 _calcBounds: function() { 393 var aX = [], 394 aY = [], 395 minX, minY, maxX, maxY, o, width, height, 396 i = 0, 397 len = this.objects.length; 398 399 for (; i < len; ++i) { 400 o = this.objects[i]; 401 o.setCoords(); 402 for (var prop in o.oCoords) { 403 aX.push(o.oCoords[prop].x); 404 aY.push(o.oCoords[prop].y); 405 } 406 }; 407 408 minX = min(aX); 409 maxX = max(aX); 410 minY = min(aY); 411 maxY = max(aY); 412 413 width = maxX - minX; 414 height = maxY - minY; 415 416 this.width = width; 417 this.height = height; 418 419 this.left = minX + width / 2; 420 this.top = minY + height / 2; 421 }, 422 423 /** 424 * Checks if point is contained within the group 425 * @method containsPoint 426 * @param {fabric.Point} point point with `x` and `y` properties 427 * @return {Boolean} true if point is contained within group 428 */ 429 containsPoint: function(point) { 430 431 var halfWidth = this.get('width') / 2, 432 halfHeight = this.get('height') / 2, 433 centerX = this.get('left'), 434 centerY = this.get('top'); 435 436 return centerX - halfWidth < point.x && 437 centerX + halfWidth > point.x && 438 centerY - halfHeight < point.y && 439 centerY + halfHeight > point.y; 440 }, 441 442 /** 443 * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) 444 * @method toGrayscale 445 */ 446 toGrayscale: function() { 447 var i = this.objects.length; 448 while (i--) { 449 this.objects[i].toGrayscale(); 450 } 451 } 452 }); 453 454 /** 455 * Returns fabric.Group instance from an object representation 456 * @static 457 * @method fabric.Group.fromObject 458 * @param object {Object} object to create a group from 459 * @param options {Object} options object 460 * @return {fabric.Group} an instance of fabric.Group 461 */ 462 fabric.Group.fromObject = function(object) { 463 return new fabric.Group(object.objects, object); 464 } 465 })();