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