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: function(callback, context) { 365 var objects = this.getObjects(), 366 i = objects.length; 367 while (i--) { 368 callback.call(context, objects[i], i, objects); 369 } 370 return this; 371 }, 372 373 /** 374 * @private 375 * @method _setOpacityIfSame 376 */ 377 _setOpacityIfSame: function() { 378 var objects = this.getObjects(), 379 firstValue = objects[0] ? objects[0].get('opacity') : 1; 380 381 var isSameOpacity = objects.every(function(o) { 382 return o.get('opacity') === firstValue; 383 }); 384 385 if (isSameOpacity) { 386 this.opacity = firstValue; 387 } 388 }, 389 390 /** 391 * @private 392 * @method _calcBounds 393 */ 394 _calcBounds: function() { 395 var aX = [], 396 aY = [], 397 minX, minY, maxX, maxY, o, width, height, 398 i = 0, 399 len = this.objects.length; 400 401 for (; i < len; ++i) { 402 o = this.objects[i]; 403 o.setCoords(); 404 for (var prop in o.oCoords) { 405 aX.push(o.oCoords[prop].x); 406 aY.push(o.oCoords[prop].y); 407 } 408 }; 409 410 minX = min(aX); 411 maxX = max(aX); 412 minY = min(aY); 413 maxY = max(aY); 414 415 width = maxX - minX; 416 height = maxY - minY; 417 418 this.width = width; 419 this.height = height; 420 421 this.left = minX + width / 2; 422 this.top = minY + height / 2; 423 }, 424 425 /** 426 * Checks if point is contained within the group 427 * @method containsPoint 428 * @param {fabric.Point} point point with `x` and `y` properties 429 * @return {Boolean} true if point is contained within group 430 */ 431 containsPoint: function(point) { 432 433 var halfWidth = this.get('width') / 2, 434 halfHeight = this.get('height') / 2, 435 centerX = this.get('left'), 436 centerY = this.get('top'); 437 438 return centerX - halfWidth < point.x && 439 centerX + halfWidth > point.x && 440 centerY - halfHeight < point.y && 441 centerY + halfHeight > point.y; 442 }, 443 444 /** 445 * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) 446 * @method toGrayscale 447 */ 448 toGrayscale: function() { 449 var i = this.objects.length; 450 while (i--) { 451 this.objects[i].toGrayscale(); 452 } 453 } 454 }); 455 456 /** 457 * Returns fabric.Group instance from an object representation 458 * @static 459 * @method fabric.Group.fromObject 460 * @param object {Object} object to create a group from 461 * @param options {Object} options object 462 * @return {fabric.Group} an instance of fabric.Group 463 */ 464 fabric.Group.fromObject = function(object) { 465 return new fabric.Group(object.objects, object); 466 } 467 })(this);