mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-03-31 04:50:23 +00:00
460 lines
No EOL
12 KiB
JavaScript
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); |