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