mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-03-24 01:30:26 +00:00
560 lines
15 KiB
JavaScript
560 lines
15 KiB
JavaScript
(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;
|
|
|
|
if (fabric.Group) {
|
|
return;
|
|
}
|
|
|
|
// 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
|
|
};
|
|
|
|
/**
|
|
* Group class
|
|
* @class fabric.Group
|
|
* @extends fabric.Object
|
|
* @mixes fabric.Collection
|
|
* @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups}
|
|
* @see {@link fabric.Group#initialize} for constructor definition
|
|
*/
|
|
fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ {
|
|
|
|
/**
|
|
* Type of an object
|
|
* @type String
|
|
* @default
|
|
*/
|
|
type: 'group',
|
|
|
|
/**
|
|
* Width of stroke
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
strokeWidth: 0,
|
|
|
|
/**
|
|
* Indicates if click events should also check for subtargets
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
subTargetCheck: false,
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {Object} objects Group objects
|
|
* @param {Object} [options] Options object
|
|
* @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already.
|
|
* @return {Object} thisArg
|
|
*/
|
|
initialize: function(objects, options, isAlreadyGrouped) {
|
|
options = options || { };
|
|
|
|
this._objects = [];
|
|
// if objects enclosed in a group have been grouped already,
|
|
// we cannot change properties of objects.
|
|
// Thus we need to set options to group without objects,
|
|
// because delegatedProperties propagate to objects.
|
|
isAlreadyGrouped && this.callSuper('initialize', options);
|
|
|
|
this._objects = objects || [];
|
|
for (var i = this._objects.length; i--; ) {
|
|
this._objects[i].group = this;
|
|
}
|
|
|
|
this.originalState = { };
|
|
|
|
if (options.originX) {
|
|
this.originX = options.originX;
|
|
}
|
|
if (options.originY) {
|
|
this.originY = options.originY;
|
|
}
|
|
|
|
if (isAlreadyGrouped) {
|
|
// do not change coordinate of objects enclosed in a group,
|
|
// because objects coordinate system have been group coodinate system already.
|
|
this._updateObjectsCoords(true);
|
|
}
|
|
else {
|
|
this._calcBounds();
|
|
this._updateObjectsCoords();
|
|
this.callSuper('initialize', options);
|
|
}
|
|
|
|
this.setCoords();
|
|
this.saveCoords();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change
|
|
*/
|
|
_updateObjectsCoords: function(skipCoordsChange) {
|
|
for (var i = this._objects.length; i--; ){
|
|
this._updateObjectCoords(this._objects[i], skipCoordsChange);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Object} object
|
|
* @param {Boolean} [skipCoordsChange] if true, coordinates of object dose not change
|
|
*/
|
|
_updateObjectCoords: function(object, skipCoordsChange) {
|
|
// do not display corners of objects enclosed in a group
|
|
object.__origHasControls = object.hasControls;
|
|
object.hasControls = false;
|
|
|
|
if (skipCoordsChange) {
|
|
return;
|
|
}
|
|
|
|
var objectLeft = object.getLeft(),
|
|
objectTop = object.getTop(),
|
|
center = this.getCenterPoint();
|
|
|
|
object.set({
|
|
originalLeft: objectLeft,
|
|
originalTop: objectTop,
|
|
left: objectLeft - center.x,
|
|
top: objectTop - center.y
|
|
});
|
|
object.setCoords();
|
|
},
|
|
|
|
/**
|
|
* Returns string represenation of a group
|
|
* @return {String}
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.Group: (' + this.complexity() + ')>';
|
|
},
|
|
|
|
/**
|
|
* Adds an object to a group; Then recalculates group's dimension, position.
|
|
* @param {Object} object
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
addWithUpdate: function(object) {
|
|
this._restoreObjectsState();
|
|
fabric.util.resetObjectTransform(this);
|
|
if (object) {
|
|
this._objects.push(object);
|
|
object.group = this;
|
|
object._set('canvas', this.canvas);
|
|
}
|
|
// since _restoreObjectsState set objects inactive
|
|
this.forEachObject(this._setObjectActive, this);
|
|
this._calcBounds();
|
|
this._updateObjectsCoords();
|
|
this.dirty = true;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setObjectActive: function(object) {
|
|
object.set('active', true);
|
|
object.group = this;
|
|
},
|
|
|
|
/**
|
|
* Removes an object from a group; Then recalculates group's dimension, position.
|
|
* @param {Object} object
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
removeWithUpdate: function(object) {
|
|
this._restoreObjectsState();
|
|
fabric.util.resetObjectTransform(this);
|
|
// since _restoreObjectsState set objects inactive
|
|
this.forEachObject(this._setObjectActive, this);
|
|
|
|
this.remove(object);
|
|
this._calcBounds();
|
|
this._updateObjectsCoords();
|
|
this.dirty = true;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_onObjectAdded: function(object) {
|
|
this.dirty = true;
|
|
object.group = this;
|
|
object._set('canvas', this.canvas);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_onObjectRemoved: function(object) {
|
|
this.dirty = true;
|
|
delete object.group;
|
|
object.set('active', false);
|
|
},
|
|
|
|
/**
|
|
* Properties that are delegated to group objects when reading/writing
|
|
* @param {Object} delegatedProperties
|
|
*/
|
|
delegatedProperties: {
|
|
fill: true,
|
|
stroke: true,
|
|
strokeWidth: true,
|
|
fontFamily: true,
|
|
fontWeight: true,
|
|
fontSize: true,
|
|
fontStyle: true,
|
|
lineHeight: true,
|
|
textDecoration: true,
|
|
textAlign: true,
|
|
backgroundColor: true
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_set: function(key, value) {
|
|
var i = this._objects.length;
|
|
|
|
if (this.delegatedProperties[key] || key === 'canvas') {
|
|
while (i--) {
|
|
this._objects[i].set(key, value);
|
|
}
|
|
}
|
|
else {
|
|
while (i--) {
|
|
this._objects[i].setOnGroup(key, value);
|
|
}
|
|
}
|
|
|
|
this.callSuper('_set', key, value);
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of an instance
|
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
* @return {Object} object representation of an instance
|
|
*/
|
|
toObject: function(propertiesToInclude) {
|
|
return extend(this.callSuper('toObject', propertiesToInclude), {
|
|
objects: invoke(this._objects, 'toObject', propertiesToInclude)
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Renders instance on a given context
|
|
* @param {CanvasRenderingContext2D} ctx context to render instance on
|
|
*/
|
|
render: function(ctx) {
|
|
this._transformDone = true;
|
|
this.callSuper('render', ctx);
|
|
this._transformDone = false;
|
|
},
|
|
|
|
/**
|
|
* Execute the drawing operation for an object on a specified context
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {Boolean} [noTransform] When true, context is not transformed
|
|
*/
|
|
drawObject: function(ctx) {
|
|
for (var i = 0, len = this._objects.length; i < len; i++) {
|
|
this._renderObject(this._objects[i], ctx);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if cache is dirty
|
|
*/
|
|
isCacheDirty: function() {
|
|
if (this.callSuper('isCacheDirty')) {
|
|
return true
|
|
}
|
|
if (!this.statefullCache) {
|
|
return false;
|
|
}
|
|
for (var i = 0, len = this._objects.length; i < len; i++) {
|
|
if (this._objects[i].isCacheDirty(true)) {
|
|
var dim = this._getNonTransformedDimensions();
|
|
this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y);
|
|
return true
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_renderObject: function(object, ctx) {
|
|
// do not render if object is not visible
|
|
if (!object.visible) {
|
|
return;
|
|
}
|
|
|
|
var originalHasRotatingPoint = object.hasRotatingPoint;
|
|
object.hasRotatingPoint = false;
|
|
object.render(ctx);
|
|
object.hasRotatingPoint = originalHasRotatingPoint;
|
|
},
|
|
|
|
/**
|
|
* Retores original state of each of group objects (original state is that which was before group was created).
|
|
* @private
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
_restoreObjectsState: function() {
|
|
this._objects.forEach(this._restoreObjectState, this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Realises the transform from this group onto the supplied object
|
|
* i.e. it tells you what would happen if the supplied object was in
|
|
* the group, and then the group was destroyed. It mutates the supplied
|
|
* object.
|
|
* @param {fabric.Object} object
|
|
* @return {fabric.Object} transformedObject
|
|
*/
|
|
realizeTransform: function(object) {
|
|
var matrix = object.calcTransformMatrix(),
|
|
options = fabric.util.qrDecompose(matrix),
|
|
center = new fabric.Point(options.translateX, options.translateY);
|
|
object.scaleX = options.scaleX;
|
|
object.scaleY = options.scaleY;
|
|
object.skewX = options.skewX;
|
|
object.skewY = options.skewY;
|
|
object.angle = options.angle;
|
|
object.flipX = false;
|
|
object.flipY = false;
|
|
object.setPositionByOrigin(center, 'center', 'center');
|
|
return object;
|
|
},
|
|
|
|
/**
|
|
* Restores original state of a specified object in group
|
|
* @private
|
|
* @param {fabric.Object} object
|
|
* @return {fabric.Group} thisArg
|
|
*/
|
|
_restoreObjectState: function(object) {
|
|
this.realizeTransform(object);
|
|
object.setCoords();
|
|
object.hasControls = object.__origHasControls;
|
|
delete object.__origHasControls;
|
|
object.set('active', false);
|
|
delete object.group;
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Destroys a group (restoring state of its objects)
|
|
* @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)
|
|
* @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
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
setObjectsCoords: function() {
|
|
this.forEachObject(function(object) {
|
|
object.setCoords();
|
|
});
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_calcBounds: function(onlyWidthHeight) {
|
|
var aX = [],
|
|
aY = [],
|
|
o, prop,
|
|
props = ['tr', 'br', 'bl', 'tl'],
|
|
i = 0, iLen = this._objects.length,
|
|
j, jLen = props.length;
|
|
|
|
for ( ; i < iLen; ++i) {
|
|
o = this._objects[i];
|
|
o.setCoords();
|
|
for (j = 0; j < jLen; j++) {
|
|
prop = props[j];
|
|
aX.push(o.oCoords[prop].x);
|
|
aY.push(o.oCoords[prop].y);
|
|
}
|
|
}
|
|
|
|
this.set(this._getBounds(aX, aY, onlyWidthHeight));
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_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
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
/* _TO_SVG_START_ */
|
|
/**
|
|
* Returns svg representation of an instance
|
|
* @param {Function} [reviver] Method for further parsing of svg representation.
|
|
* @return {String} svg representation of an instance
|
|
*/
|
|
toSVG: function(reviver) {
|
|
var markup = this._createBaseSVGMarkup();
|
|
markup.push(
|
|
'<g ', this.getSvgId(), 'transform="',
|
|
/* avoiding styles intentionally */
|
|
this.getSvgTransform(),
|
|
this.getSvgTransformMatrix(),
|
|
'" style="',
|
|
this.getSvgFilter(),
|
|
'">\n'
|
|
);
|
|
|
|
for (var i = 0, len = this._objects.length; i < len; i++) {
|
|
markup.push('\t', this._objects[i].toSVG(reviver));
|
|
}
|
|
|
|
markup.push('</g>\n');
|
|
|
|
return reviver ? reviver(markup.join('')) : markup.join('');
|
|
},
|
|
/* _TO_SVG_END_ */
|
|
|
|
/**
|
|
* Returns requested property
|
|
* @param {String} prop Property to get
|
|
* @return {*}
|
|
*/
|
|
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];
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Returns {@link fabric.Group} instance from an object representation
|
|
* @static
|
|
* @memberOf fabric.Group
|
|
* @param {Object} object Object to create a group from
|
|
* @param {Function} [callback] Callback to invoke when an group instance is created
|
|
*/
|
|
fabric.Group.fromObject = function(object, callback) {
|
|
fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) {
|
|
delete object.objects;
|
|
callback && callback(new fabric.Group(enlivenedObjects, object, true));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Indicates that instances of this type are async
|
|
* @static
|
|
* @memberOf fabric.Group
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
fabric.Group.async = true;
|
|
|
|
})(typeof exports !== 'undefined' ? exports : this);
|