fabric.js/src/group.class.js

460 lines
No EOL
12 KiB
JavaScript

//= require "object.class"
(function(global){
"use strict";
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
min = fabric.util.array.min,
max = fabric.util.array.max,
invoke = fabric.util.array.invoke,
removeFromArray = fabric.util.removeFromArray;
if (fabric.Group) {
return;
}
/**
* @class Group
* @extends fabric.Object
*/
fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ {
/**
* @property
* @type String
*/
type: 'group',
/**
* Constructor
* @method initialized
* @param {Object} objects Group objects
* @param {Object} [options] Options object
* @return {Object} thisArg
*/
initialize: function(objects, options) {
this.objects = objects || [];
this.originalState = { };
this.callSuper('initialize');
this._calcBounds();
this._updateObjectsCoords();
if (options) {
extend(this, options);
}
this._setOpacityIfSame();
// group is active by default
this.setCoords(true);
this.saveCoords();
this.activateAllObjects();
},
/**
* @private
* @method _updateObjectsCoords
*/
_updateObjectsCoords: function() {
var groupDeltaX = this.left,
groupDeltaY = this.top;
this.forEachObject(function(object) {
var objectLeft = object.get('left'),
objectTop = object.get('top');
object.set('originalLeft', objectLeft);
object.set('originalTop', objectTop);
object.set('left', objectLeft - groupDeltaX);
object.set('top', objectTop - groupDeltaY);
object.setCoords();
// do not display corners of objects enclosed in a group
object.hideCorners = true;
}, this);
},
/**
* Returns string represenation of a group
* @method toString
* @return {String}
*/
toString: function() {
return '#<fabric.Group: (' + this.complexity() + ')>';
},
/**
* Returns an array of all objects in this group
* @method getObjects
* @return {Array} group objects
*/
getObjects: function() {
return this.objects;
},
/**
* Adds an object to a group; Then recalculates group's dimension, position.
* @method add
* @param {Object} object
* @return {fabric.Group} thisArg
* @chainable
*/
add: function(object) {
this._restoreObjectsState();
this.objects.push(object);
object.setActive(true);
this._calcBounds();
this._updateObjectsCoords();
return this;
},
/**
* Removes an object from a group; Then recalculates group's dimension, position.
* @param {Object} object
* @return {fabric.Group} thisArg
* @chainable
*/
remove: function(object) {
this._restoreObjectsState();
removeFromArray(this.objects, object);
object.setActive(false);
this._calcBounds();
this._updateObjectsCoords();
return this;
},
/**
* Returns a size of a group (i.e: length of an array containing its objects)
* @return {Number} Group size
*/
size: function() {
return this.getObjects().length;
},
/**
* Sets property to a given value
* @method set
* @param {String} name
* @param {Object|Function} value
* @return {fabric.Group} thisArg
* @chainable
*/
set: function(name, value) {
if (typeof value == 'function') {
// recurse
this.set(name, value(this[name]));
}
else {
if (name === 'fill' || name === 'opacity') {
var i = this.objects.length;
this[name] = value;
while (i--) {
this.objects[i].set(name, value);
}
}
else {
this[name] = value;
}
}
return this;
},
/**
* Returns true if a group contains an object
* @method contains
* @param {Object} object Object to check against
* @return {Boolean} `true` if group contains an object
*/
contains: function(object) {
return this.objects.indexOf(object) > -1;
},
/**
* Returns object representation of an instance
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
objects: invoke(this.objects, 'clone')
});
},
/**
* Renders instance on a given context
* @method render
* @param {CanvasRenderingContext2D} ctx context to render instance on
*/
render: function(ctx) {
ctx.save();
this.transform(ctx);
var groupScaleFactor = Math.max(this.scaleX, this.scaleY);
for (var i = 0, len = this.objects.length, object; object = this.objects[i]; i++) {
var originalScaleFactor = object.borderScaleFactor;
object.borderScaleFactor = groupScaleFactor;
object.render(ctx);
object.borderScaleFactor = originalScaleFactor;
}
this.hideBorders || this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
ctx.restore();
this.setCoords();
},
/**
* Returns object from the group at the specified index
* @method item
* @param index {Number} index of item to get
* @return {fabric.Object}
*/
item: function(index) {
return this.getObjects()[index];
},
/**
* Returns complexity of an instance
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return this.getObjects().reduce(function(total, object) {
total += (typeof object.complexity == 'function') ? object.complexity() : 0;
return total;
}, 0);
},
/**
* Retores original state of each of group objects (original state is that which was before group was created).
* @private
* @method _restoreObjectsState
* @return {fabric.Group} thisArg
* @chainable
*/
_restoreObjectsState: function() {
this.objects.forEach(this._restoreObjectState, this);
return this;
},
/**
* Restores original state of a specified object in group
* @private
* @method _restoreObjectState
* @param {fabric.Object} object
* @return {fabric.Group} thisArg
*/
_restoreObjectState: function(object) {
var groupLeft = this.get('left'),
groupTop = this.get('top'),
groupAngle = this.getAngle() * (Math.PI / 180),
objectLeft = object.get('originalLeft'),
objectTop = object.get('originalTop'),
rotatedTop = Math.cos(groupAngle) * object.get('top') + Math.sin(groupAngle) * object.get('left'),
rotatedLeft = -Math.sin(groupAngle) * object.get('top') + Math.cos(groupAngle) * object.get('left');
object.setAngle(object.getAngle() + this.getAngle());
object.set('left', groupLeft + rotatedLeft * this.get('scaleX'));
object.set('top', groupTop + rotatedTop * this.get('scaleY'));
object.set('scaleX', object.get('scaleX') * this.get('scaleX'));
object.set('scaleY', object.get('scaleY') * this.get('scaleY'));
object.setCoords();
object.hideCorners = false;
object.setActive(false);
object.setCoords();
return this;
},
/**
* Destroys a group (restoring state of its objects)
* @method destroy
* @return {fabric.Group} thisArg
* @chainable
*/
destroy: function() {
return this._restoreObjectsState();
},
/**
* Saves coordinates of this instance (to be used together with `hasMoved`)
* @saveCoords
* @return {fabric.Group} thisArg
* @chainable
*/
saveCoords: function() {
this._originalLeft = this.get('left');
this._originalTop = this.get('top');
return this;
},
/**
* Checks whether this group was moved (since `saveCoords` was called last)
* @method hasMoved
* @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called)
*/
hasMoved: function() {
return this._originalLeft !== this.get('left') ||
this._originalTop !== this.get('top');
},
/**
* Sets coordinates of all group objects
* @method setObjectsCoords
* @return {fabric.Group} thisArg
* @chainable
*/
setObjectsCoords: function() {
this.forEachObject(function(object) {
object.setCoords();
});
return this;
},
/**
* Activates (makes active) all group objects
* @method activateAllObjects
* @return {fabric.Group} thisArg
* @chainable
*/
activateAllObjects: function() {
return this.setActive(true);
},
/**
* Activates (makes active) all group objects
* @method setActive
* @param {Boolean} value `true` to activate object, `false` otherwise
* @return {fabric.Group} thisArg
* @chainable
*/
setActive: function(value) {
this.forEachObject(function(object) {
object.setActive(value);
});
return this;
},
/**
* Executes given function for each object in this group
* @method forEachObject
* @param {Function} callback
* Callback invoked with current object as first argument,
* index - as second and an array of all objects - as third.
* Iteration happens in reverse order (for performance reasons).
* Callback is invoked in a context of Global Object (e.g. `window`)
* when no `context` argument is given
*
* @param {Object} context Context (aka thisObject)
*
* @return {fabric.Group} thisArg
* @chainable
*/
forEachObject: fabric.Canvas.prototype.forEachObject,
/**
* @private
* @method _setOpacityIfSame
*/
_setOpacityIfSame: function() {
var objects = this.getObjects(),
firstValue = objects[0] ? objects[0].get('opacity') : 1;
var isSameOpacity = objects.every(function(o) {
return o.get('opacity') === firstValue;
});
if (isSameOpacity) {
this.opacity = firstValue;
}
},
/**
* @private
* @method _calcBounds
*/
_calcBounds: function() {
var aX = [],
aY = [],
minX, minY, maxX, maxY, o, width, height,
i = 0,
len = this.objects.length;
for (; i < len; ++i) {
o = this.objects[i];
o.setCoords();
for (var prop in o.oCoords) {
aX.push(o.oCoords[prop].x);
aY.push(o.oCoords[prop].y);
}
};
minX = min(aX);
maxX = max(aX);
minY = min(aY);
maxY = max(aY);
width = maxX - minX;
height = maxY - minY;
this.width = width;
this.height = height;
this.left = minX + width / 2;
this.top = minY + height / 2;
},
/**
* Checks if point is contained within the group
* @method containsPoint
* @param {fabric.Point} point point with `x` and `y` properties
* @return {Boolean} true if point is contained within group
*/
containsPoint: function(point) {
var halfWidth = this.get('width') / 2,
halfHeight = this.get('height') / 2,
centerX = this.get('left'),
centerY = this.get('top');
return centerX - halfWidth < point.x &&
centerX + halfWidth > point.x &&
centerY - halfHeight < point.y &&
centerY + halfHeight > point.y;
},
/**
* Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them)
* @method toGrayscale
*/
toGrayscale: function() {
var i = this.objects.length;
while (i--) {
this.objects[i].toGrayscale();
}
}
});
/**
* Returns fabric.Group instance from an object representation
* @static
* @method fabric.Group.fromObject
* @param object {Object} object to create a group from
* @param options {Object} options object
* @return {fabric.Group} an instance of fabric.Group
*/
fabric.Group.fromObject = function(object) {
return new fabric.Group(object.objects, object);
}
})(this);