fabric.js/src/shapes/group.class.js

551 lines
14 KiB
JavaScript
Raw Normal View History

(function(global) {
'use strict';
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
min = fabric.util.array.min,
2010-07-26 23:24:16 +00:00
max = fabric.util.array.max,
2014-07-05 17:06:21 +00:00
invoke = fabric.util.array.invoke;
if (fabric.Group) {
return;
}
2012-12-15 16:05:23 +00:00
// lock-related properties, for use in fabric.Group#get
// to enable locking behavior on group
// when one of its objects has lock-related properties set
var _lockProperties = {
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
lockUniScaling: true
};
/**
2012-12-13 14:36:43 +00:00
* Group class
2013-04-25 18:21:32 +00:00
* @class fabric.Group
* @extends fabric.Object
2013-09-09 00:42:16 +00:00
* @mixes fabric.Collection
* @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#groups}
2013-10-05 18:21:28 +00:00
* @see {@link fabric.Group#initialize} for constructor definition
*/
fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ {
/**
2012-12-13 14:36:43 +00:00
* Type of an object
* @type String
* @default
*/
2010-06-09 22:34:55 +00:00
type: 'group',
2010-06-09 22:34:55 +00:00
/**
* Constructor
2010-06-09 22:34:55 +00:00
* @param {Object} objects Group objects
* @param {Object} [options] Options object
2010-06-09 22:34:55 +00:00
* @return {Object} thisArg
*/
initialize: function(objects, options) {
options = options || { };
this._objects = objects || [];
for (var i = this._objects.length; i--; ) {
this._objects[i].group = this;
}
this.originalState = { };
2010-06-09 22:34:55 +00:00
this.callSuper('initialize');
2014-10-20 14:52:07 +00:00
if (options.originX) {
this.originX = options.originX;
}
if (options.originY) {
this.originY = options.originY;
2014-07-04 09:52:12 +00:00
}
2014-07-04 19:07:04 +00:00
this._calcBounds();
this._updateObjectsCoords();
2014-11-30 17:38:43 +00:00
this.callSuper('initialize', options);
2014-10-20 14:52:07 +00:00
2014-07-04 19:07:04 +00:00
this.setCoords();
this.saveCoords();
2010-06-09 22:34:55 +00:00
},
2010-06-09 22:34:55 +00:00
/**
* @private
*/
_updateObjectsCoords: function() {
2013-11-10 16:24:32 +00:00
this.forEachObject(this._updateObjectCoords, this);
},
2013-11-10 16:24:32 +00:00
/**
* @private
*/
_updateObjectCoords: function(object) {
var objectLeft = object.getLeft(),
objectTop = object.getTop(),
center = this.getCenterPoint();
2013-11-10 16:24:32 +00:00
object.set({
originalLeft: objectLeft,
originalTop: objectTop,
left: objectLeft - center.x,
top: objectTop - center.y
2013-11-10 16:24:32 +00:00
});
2013-11-10 16:24:32 +00:00
object.setCoords();
2013-11-10 16:24:32 +00:00
// do not display corners of objects enclosed in a group
object.__origHasControls = object.hasControls;
object.hasControls = false;
2010-06-09 22:34:55 +00:00
},
2010-06-09 22:34:55 +00:00
/**
* Returns string represenation of a group
2010-06-09 22:34:55 +00:00
* @return {String}
*/
toString: function() {
return '#<fabric.Group: (' + this.complexity() + ')>';
2010-06-09 22:34:55 +00:00
},
2010-06-09 22:34:55 +00:00
/**
* Adds an object to a group; Then recalculates group's dimension, position.
2010-06-09 22:34:55 +00:00
* @param {Object} object
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
* @chainable
*/
addWithUpdate: function(object) {
2010-06-09 22:34:55 +00:00
this._restoreObjectsState();
2014-07-04 19:07:04 +00:00
if (object) {
this._objects.push(object);
object.group = this;
2014-07-04 19:07:04 +00:00
}
2013-03-30 00:36:15 +00:00
// since _restoreObjectsState set objects inactive
2013-11-10 16:24:32 +00:00
this.forEachObject(this._setObjectActive, this);
2010-06-09 22:34:55 +00:00
this._calcBounds();
this._updateObjectsCoords();
return this;
},
2013-11-10 16:24:32 +00:00
/**
* @private
*/
_setObjectActive: function(object) {
object.set('active', true);
object.group = this;
},
2010-06-09 22:34:55 +00:00
/**
* Removes an object from a group; Then recalculates group's dimension, position.
2010-06-09 22:34:55 +00:00
* @param {Object} object
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
* @chainable
*/
removeWithUpdate: function(object) {
this._moveFlippedObject(object);
2010-06-09 22:34:55 +00:00
this._restoreObjectsState();
2013-11-10 16:24:32 +00:00
2013-03-30 00:36:15 +00:00
// since _restoreObjectsState set objects inactive
2013-11-10 16:24:32 +00:00
this.forEachObject(this._setObjectActive, this);
2013-03-30 00:36:15 +00:00
this.remove(object);
2010-06-09 22:34:55 +00:00
this._calcBounds();
this._updateObjectsCoords();
2013-11-10 16:24:32 +00:00
2010-06-09 22:34:55 +00:00
return this;
},
/**
* @private
*/
_onObjectAdded: function(object) {
object.group = this;
},
/**
* @private
*/
_onObjectRemoved: function(object) {
delete object.group;
2013-03-30 00:36:15 +00:00
object.set('active', false);
2010-06-09 22:34:55 +00:00
},
2012-12-15 16:05:23 +00:00
/**
* Properties that are delegated to group objects when reading/writing
2013-08-12 09:52:13 +00:00
* @param {Object} delegatedProperties
2012-12-15 16:05:23 +00:00
*/
delegatedProperties: {
fill: true,
opacity: true,
fontFamily: true,
fontWeight: true,
fontSize: true,
2013-03-05 18:30:40 +00:00
fontStyle: true,
2012-12-15 16:05:23 +00:00
lineHeight: true,
textDecoration: true,
textAlign: true,
2012-12-15 16:05:23 +00:00
backgroundColor: true
},
2010-06-09 22:34:55 +00:00
/**
* @private
2010-06-09 22:34:55 +00:00
*/
_set: function(key, value) {
2012-12-15 16:05:23 +00:00
if (key in this.delegatedProperties) {
var i = this._objects.length;
while (i--) {
this._objects[i].set(key, value);
}
2010-06-09 22:34:55 +00:00
}
2014-11-30 17:38:43 +00:00
this.callSuper('_set', key, value);
2010-06-09 22:34:55 +00:00
},
2010-06-09 22:34:55 +00:00
/**
* Returns object representation of an instance
2013-09-26 12:12:02 +00:00
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
2010-06-09 22:34:55 +00:00
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
objects: invoke(this._objects, 'toObject', propertiesToInclude)
2010-06-09 22:34:55 +00:00
});
},
2010-06-09 22:34:55 +00:00
/**
* Renders instance on a given context
* @param {CanvasRenderingContext2D} ctx context to render instance on
2010-06-09 22:34:55 +00:00
*/
2014-07-05 17:06:21 +00:00
render: function(ctx) {
// do not render if object is not visible
if (!this.visible) {
return;
}
2013-11-19 16:21:29 +00:00
ctx.save();
this.clipTo && fabric.util.clipContext(this, ctx);
2013-11-10 16:24:32 +00:00
// the array is now sorted in order of highest first, so start from end
2013-03-30 00:36:15 +00:00
for (var i = 0, len = this._objects.length; i < len; i++) {
2013-11-10 16:24:32 +00:00
this._renderObject(this._objects[i], ctx);
2010-06-09 22:34:55 +00:00
}
2013-11-10 16:24:32 +00:00
this.clipTo && ctx.restore();
2010-06-09 22:34:55 +00:00
ctx.restore();
},
2014-06-06 16:36:17 +00:00
/**
* Renders controls and borders for the object
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Boolean} [noTransform] When true, context is not transformed
*/
_renderControls: function(ctx, noTransform) {
this.callSuper('_renderControls', ctx, noTransform);
for (var i = 0, len = this._objects.length; i < len; i++) {
this._objects[i]._renderControls(ctx);
}
},
2013-11-10 16:24:32 +00:00
/**
* @private
*/
_renderObject: function(object, ctx) {
2014-06-06 16:36:17 +00:00
var originalHasRotatingPoint = object.hasRotatingPoint;
2013-11-10 16:24:32 +00:00
// do not render if object is not visible
if (!object.visible) {
return;
}
2013-11-10 16:24:32 +00:00
object.hasRotatingPoint = false;
object.render(ctx);
object.hasRotatingPoint = originalHasRotatingPoint;
},
2010-06-09 22:34:55 +00:00
/**
* Retores original state of each of group objects (original state is that which was before group was created).
2010-06-09 22:34:55 +00:00
* @private
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
* @chainable
*/
_restoreObjectsState: function() {
this._objects.forEach(this._restoreObjectState, this);
2010-06-09 22:34:55 +00:00
return this;
},
2010-06-09 22:34:55 +00:00
/**
* Moves a flipped object to the position where it's displayed
2010-06-09 22:34:55 +00:00
* @private
* @param {fabric.Object} object
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
*/
_moveFlippedObject: function(object) {
var oldOriginX = object.get('originX'),
oldOriginY = object.get('originY'),
center = object.getCenterPoint();
2013-11-10 16:24:32 +00:00
2013-09-25 19:50:09 +00:00
object.set({
originX: 'center',
originY: 'center',
left: center.x,
top: center.y
});
2013-11-10 16:24:32 +00:00
this._toggleFlipping(object);
var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY);
object.set({
originX: oldOriginX,
originY: oldOriginY,
left: newOrigin.x,
top: newOrigin.y
});
return this;
},
/**
* @private
*/
_toggleFlipping: function(object) {
if (this.flipX) {
object.toggle('flipX');
object.set('left', -object.get('left'));
2013-09-25 19:50:09 +00:00
object.setAngle(-object.getAngle());
}
if (this.flipY) {
object.toggle('flipY');
object.set('top', -object.get('top'));
object.setAngle(-object.getAngle());
}
},
2010-06-09 22:34:55 +00:00
/**
* Restores original state of a specified object in group
2010-06-09 22:34:55 +00:00
* @private
* @param {fabric.Object} object
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
*/
_restoreObjectState: function(object) {
2013-10-30 14:09:02 +00:00
this._setObjectPosition(object);
2010-06-09 22:34:55 +00:00
object.setCoords();
object.hasControls = object.__origHasControls;
delete object.__origHasControls;
object.set('active', false);
2010-06-09 22:34:55 +00:00
object.setCoords();
delete object.group;
2010-06-09 22:34:55 +00:00
return this;
},
2013-10-30 14:09:02 +00:00
/**
* @private
*/
_setObjectPosition: function(object) {
2014-10-20 15:55:09 +00:00
var center = this.getCenterPoint(),
2013-10-30 14:09:02 +00:00
rotated = this._getRotatedLeftTop(object);
2013-11-10 16:31:59 +00:00
object.set({
angle: object.getAngle() + this.getAngle(),
2014-10-20 15:55:09 +00:00
left: center.x + rotated.left,
top: center.y + rotated.top,
2013-11-10 16:31:59 +00:00
scaleX: object.get('scaleX') * this.get('scaleX'),
scaleY: object.get('scaleY') * this.get('scaleY')
});
2013-10-30 14:09:02 +00:00
},
/**
* @private
*/
_getRotatedLeftTop: function(object) {
var groupAngle = this.getAngle() * (Math.PI / 180);
return {
left: (-Math.sin(groupAngle) * object.getTop() * this.get('scaleY') +
Math.cos(groupAngle) * object.getLeft() * this.get('scaleX')),
top: (Math.cos(groupAngle) * object.getTop() * this.get('scaleY') +
Math.sin(groupAngle) * object.getLeft() * this.get('scaleX'))
};
},
2010-06-09 22:34:55 +00:00
/**
* Destroys a group (restoring state of its objects)
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
* @chainable
*/
destroy: function() {
this._objects.forEach(this._moveFlippedObject, this);
2010-06-09 22:34:55 +00:00
return this._restoreObjectsState();
},
2010-06-09 22:34:55 +00:00
/**
2010-10-19 20:27:24 +00:00
* Saves coordinates of this instance (to be used together with `hasMoved`)
2010-06-09 22:34:55 +00:00
* @saveCoords
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
* @chainable
*/
saveCoords: function() {
this._originalLeft = this.get('left');
this._originalTop = this.get('top');
return this;
},
/**
2010-10-19 20:27:24 +00:00
* Checks whether this group was moved (since `saveCoords` was called last)
* @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called)
*/
2010-06-09 22:34:55 +00:00
hasMoved: function() {
return this._originalLeft !== this.get('left') ||
this._originalTop !== this.get('top');
},
2010-06-09 22:34:55 +00:00
/**
* Sets coordinates of all group objects
* @return {fabric.Group} thisArg
2010-06-09 22:34:55 +00:00
* @chainable
*/
setObjectsCoords: function() {
this.forEachObject(function(object) {
object.setCoords();
});
return this;
},
2010-06-09 22:34:55 +00:00
/**
* @private
*/
2014-02-11 04:24:29 +00:00
_calcBounds: function(onlyWidthHeight) {
var aX = [],
aY = [],
2013-11-10 16:43:23 +00:00
o;
2010-06-09 22:34:55 +00:00
2013-11-10 16:43:23 +00:00
for (var i = 0, len = this._objects.length; i < len; ++i) {
o = this._objects[i];
2010-06-09 22:34:55 +00:00
o.setCoords();
for (var prop in o.oCoords) {
aX.push(o.oCoords[prop].x);
aY.push(o.oCoords[prop].y);
}
}
2014-02-11 04:24:29 +00:00
this.set(this._getBounds(aX, aY, onlyWidthHeight));
2013-11-10 16:43:23 +00:00
},
2013-11-10 16:43:23 +00:00
/**
* @private
*/
2014-02-11 04:24:29 +00:00
_getBounds: function(aX, aY, onlyWidthHeight) {
var ivt = fabric.util.invertTransform(this.getViewportTransform()),
minXY = fabric.util.transformPoint(new fabric.Point(min(aX), min(aY)), ivt),
maxXY = fabric.util.transformPoint(new fabric.Point(max(aX), max(aY)), ivt),
obj = {
width: (maxXY.x - minXY.x) || 0,
height: (maxXY.y - minXY.y) || 0
};
2014-02-11 04:24:29 +00:00
if (!onlyWidthHeight) {
obj.left = minXY.x || 0;
obj.top = minXY.y || 0;
if (this.originX === 'center') {
obj.left += obj.width / 2;
}
if (this.originX === 'right') {
obj.left += obj.width;
}
if (this.originY === 'center') {
obj.top += obj.height / 2;
}
if (this.originY === 'bottom') {
obj.top += obj.height;
}
2014-02-11 04:24:29 +00:00
}
return obj;
2010-06-09 22:34:55 +00:00
},
/* _TO_SVG_START_ */
2012-08-09 10:59:20 +00:00
/**
* Returns svg representation of an instance
* @param {Function} [reviver] Method for further parsing of svg representation.
* @return {String} svg representation of an instance
2012-08-09 10:59:20 +00:00
*/
toSVG: function(reviver) {
var markup = [
//jscs:disable validateIndentation
'<g ',
'transform="', this.getSvgTransform(),
2014-08-05 10:45:07 +00:00
'">\n'
//jscs:enable validateIndentation
];
for (var i = 0, len = this._objects.length; i < len; i++) {
markup.push(this._objects[i].toSVG(reviver));
2012-08-09 10:59:20 +00:00
}
2014-08-05 10:45:07 +00:00
markup.push('</g>\n');
return reviver ? reviver(markup.join('')) : markup.join('');
},
/* _TO_SVG_END_ */
/**
2012-12-15 16:05:23 +00:00
* Returns requested property
* @param {String} prop Property to get
* @return {Any}
*/
get: function(prop) {
if (prop in _lockProperties) {
if (this[prop]) {
return this[prop];
}
else {
for (var i = 0, len = this._objects.length; i < len; i++) {
if (this._objects[i][prop]) {
return true;
}
}
return false;
}
}
else {
if (prop in this.delegatedProperties) {
return this._objects[0] && this._objects[0].get(prop);
}
return this[prop];
}
2010-06-09 22:34:55 +00:00
}
});
2010-06-09 22:34:55 +00:00
/**
2012-12-13 14:36:43 +00:00
* Returns {@link fabric.Group} instance from an object representation
2010-06-09 22:34:55 +00:00
* @static
* @memberOf fabric.Group
2012-12-13 14:36:43 +00:00
* @param {Object} object Object to create a group from
2014-07-17 14:18:57 +00:00
* @param {Function} [callback] Callback to invoke when an group instance is created
2012-12-13 14:36:43 +00:00
* @return {fabric.Group} An instance of fabric.Group
2010-06-09 22:34:55 +00:00
*/
fabric.Group.fromObject = function(object, callback) {
fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) {
delete object.objects;
callback && callback(new fabric.Group(enlivenedObjects, object));
});
};
2012-12-15 16:05:23 +00:00
/**
* Indicates that instances of this type are async
* @static
* @memberOf fabric.Group
2012-12-15 16:05:23 +00:00
* @type Boolean
* @default
2012-12-15 16:05:23 +00:00
*/
fabric.Group.async = true;
})(typeof exports !== 'undefined' ? exports : this);