mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-11 01:40:59 +00:00
1602 lines
47 KiB
JavaScript
1602 lines
47 KiB
JavaScript
(function() {
|
|
|
|
var getPointer = fabric.util.getPointer,
|
|
degreesToRadians = fabric.util.degreesToRadians,
|
|
radiansToDegrees = fabric.util.radiansToDegrees,
|
|
atan2 = Math.atan2,
|
|
abs = Math.abs,
|
|
supportLineDash = fabric.StaticCanvas.supports('setLineDash'),
|
|
|
|
STROKE_OFFSET = 0.5;
|
|
|
|
/**
|
|
* Canvas class
|
|
* @class fabric.Canvas
|
|
* @extends fabric.StaticCanvas
|
|
* @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas}
|
|
* @see {@link fabric.Canvas#initialize} for constructor definition
|
|
*
|
|
* @fires object:added
|
|
* @fires object:modified
|
|
* @fires object:rotating
|
|
* @fires object:scaling
|
|
* @fires object:moving
|
|
* @fires object:selected
|
|
*
|
|
* @fires before:selection:cleared
|
|
* @fires selection:cleared
|
|
* @fires selection:created
|
|
*
|
|
* @fires path:created
|
|
* @fires mouse:down
|
|
* @fires mouse:move
|
|
* @fires mouse:up
|
|
* @fires mouse:over
|
|
* @fires mouse:out
|
|
*
|
|
*/
|
|
fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ {
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {HTMLElement | String} el <canvas> element to initialize instance on
|
|
* @param {Object} [options] Options object
|
|
* @return {Object} thisArg
|
|
*/
|
|
initialize: function(el, options) {
|
|
options || (options = { });
|
|
|
|
this._initStatic(el, options);
|
|
this._initInteractive();
|
|
this._createCacheCanvas();
|
|
},
|
|
|
|
/**
|
|
* When true, objects can be transformed by one side (unproportionally)
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
uniScaleTransform: false,
|
|
|
|
/**
|
|
* Indicates which key enable unproportional scaling
|
|
* values: altKey, shiftKey, ctrlKey
|
|
* @since 1.6.2
|
|
* @type String
|
|
* @default
|
|
*/
|
|
uniScaleKey: 'shiftKey',
|
|
|
|
/**
|
|
* When true, objects use center point as the origin of scale transformation.
|
|
* <b>Backwards incompatibility note:</b> This property replaces "centerTransform" (Boolean).
|
|
* @since 1.3.4
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
centeredScaling: false,
|
|
|
|
/**
|
|
* When true, objects use center point as the origin of rotate transformation.
|
|
* <b>Backwards incompatibility note:</b> This property replaces "centerTransform" (Boolean).
|
|
* @since 1.3.4
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
centeredRotation: false,
|
|
|
|
/**
|
|
* Indicates which key enable centered Transfrom
|
|
* values: altKey, shiftKey, ctrlKey
|
|
* @since 1.6.2
|
|
* @type String
|
|
* @default
|
|
*/
|
|
centeredKey: 'altKey',
|
|
|
|
/**
|
|
* Indicates which key enable alternate action on corner
|
|
* values: altKey, shiftKey, ctrlKey
|
|
* @since 1.6.2
|
|
* @type String
|
|
* @default
|
|
*/
|
|
altActionKey: 'shiftKey',
|
|
|
|
/**
|
|
* Indicates that canvas is interactive. This property should not be changed.
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
interactive: true,
|
|
|
|
/**
|
|
* Indicates whether group selection should be enabled
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
selection: true,
|
|
|
|
/**
|
|
* Indicates which key enable multiple click selection
|
|
* values: altKey, shiftKey, ctrlKey, cmdKey
|
|
* @since 1.6.2
|
|
* @type String
|
|
* @default
|
|
*/
|
|
selectionKey: 'shiftKey',
|
|
|
|
/**
|
|
* Indicates which key enable alternative selection
|
|
* in case of target overlapping with active object
|
|
* values: altKey, shiftKey, ctrlKey, cmdKey
|
|
* @since 1.6.5
|
|
* @type null|String
|
|
* @default
|
|
*/
|
|
altSelectionKey: null,
|
|
|
|
/**
|
|
* Color of selection
|
|
* @type String
|
|
* @default
|
|
*/
|
|
selectionColor: 'rgba(100, 100, 255, 0.3)', // blue
|
|
|
|
/**
|
|
* Default dash array pattern
|
|
* If not empty the selection border is dashed
|
|
* @type Array
|
|
*/
|
|
selectionDashArray: [],
|
|
|
|
/**
|
|
* Color of the border of selection (usually slightly darker than color of selection itself)
|
|
* @type String
|
|
* @default
|
|
*/
|
|
selectionBorderColor: 'rgba(255, 255, 255, 0.3)',
|
|
|
|
/**
|
|
* Width of a line used in object/group selection
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
selectionLineWidth: 1,
|
|
|
|
/**
|
|
* Default cursor value used when hovering over an object on canvas
|
|
* @type String
|
|
* @default
|
|
*/
|
|
hoverCursor: 'move',
|
|
|
|
/**
|
|
* Default cursor value used when moving an object on canvas
|
|
* @type String
|
|
* @default
|
|
*/
|
|
moveCursor: 'move',
|
|
|
|
/**
|
|
* Default cursor value used for the entire canvas
|
|
* @type String
|
|
* @default
|
|
*/
|
|
defaultCursor: 'default',
|
|
|
|
/**
|
|
* Cursor value used during free drawing
|
|
* @type String
|
|
* @default
|
|
*/
|
|
freeDrawingCursor: 'crosshair',
|
|
|
|
/**
|
|
* Cursor value used for rotation point
|
|
* @type String
|
|
* @default
|
|
*/
|
|
rotationCursor: 'crosshair',
|
|
|
|
/**
|
|
* Default element class that's given to wrapper (div) element of canvas
|
|
* @type String
|
|
* @default
|
|
*/
|
|
containerClass: 'canvas-container',
|
|
|
|
/**
|
|
* When true, object detection happens on per-pixel basis rather than on per-bounding-box
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
perPixelTargetFind: false,
|
|
|
|
/**
|
|
* Number of pixels around target pixel to tolerate (consider active) during object detection
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
targetFindTolerance: 0,
|
|
|
|
/**
|
|
* When true, target detection is skipped when hovering over canvas. This can be used to improve performance.
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
skipTargetFind: false,
|
|
|
|
/**
|
|
* When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing.
|
|
* After mousedown, mousemove creates a shape,
|
|
* and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas.
|
|
* @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing}
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
isDrawingMode: false,
|
|
|
|
/**
|
|
* Indicates whether objects should remain in current stack position when selected.
|
|
* When false objects are brought to top and rendered as part of the selection group
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
preserveObjectStacking: false,
|
|
|
|
/**
|
|
* Indicates if the right click on canvas can output the context menu or not
|
|
* @type Boolean
|
|
* @since 1.6.5
|
|
* @default
|
|
*/
|
|
stopContextMenu: false,
|
|
|
|
/**
|
|
* Indicates if the canvas can fire right click events
|
|
* @type Boolean
|
|
* @since 1.6.5
|
|
* @default
|
|
*/
|
|
fireRightClick: false,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_initInteractive: function() {
|
|
this._currentTransform = null;
|
|
this._groupSelector = null;
|
|
this._initWrapperElement();
|
|
this._createUpperCanvas();
|
|
this._initEventListeners();
|
|
|
|
this._initRetinaScaling();
|
|
|
|
this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this);
|
|
|
|
this.calcOffset();
|
|
},
|
|
|
|
/**
|
|
* Divides objects in two groups, one to render immediately
|
|
* and one to render as activeGroup.
|
|
* @return {Array} objects to render immediately and pushes the other in the activeGroup.
|
|
*/
|
|
_chooseObjectsToRender: function() {
|
|
var activeGroup = this.getActiveGroup(),
|
|
activeObject = this.getActiveObject(),
|
|
object, objsToRender = [], activeGroupObjects = [];
|
|
|
|
if ((activeGroup || activeObject) && !this.preserveObjectStacking) {
|
|
for (var i = 0, length = this._objects.length; i < length; i++) {
|
|
object = this._objects[i];
|
|
if ((!activeGroup || !activeGroup.contains(object)) && object !== activeObject) {
|
|
objsToRender.push(object);
|
|
}
|
|
else {
|
|
activeGroupObjects.push(object);
|
|
}
|
|
}
|
|
if (activeGroup) {
|
|
activeGroup._set('_objects', activeGroupObjects);
|
|
objsToRender.push(activeGroup);
|
|
}
|
|
activeObject && objsToRender.push(activeObject);
|
|
}
|
|
else {
|
|
objsToRender = this._objects;
|
|
}
|
|
return objsToRender;
|
|
},
|
|
|
|
/**
|
|
* Renders both the top canvas and the secondary container canvas.
|
|
* @return {fabric.Canvas} instance
|
|
* @chainable
|
|
*/
|
|
renderAll: function () {
|
|
if (this.selection && !this._groupSelector && !this.isDrawingMode) {
|
|
this.clearContext(this.contextTop);
|
|
}
|
|
var canvasToDrawOn = this.contextContainer;
|
|
this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender());
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Method to render only the top canvas.
|
|
* Also used to render the group selection box.
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
renderTop: function () {
|
|
var ctx = this.contextTop;
|
|
this.clearContext(ctx);
|
|
|
|
// we render the top context - last object
|
|
if (this.selection && this._groupSelector) {
|
|
this._drawSelection(ctx);
|
|
}
|
|
|
|
this.fire('after:render');
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Resets the current transform to its original values and chooses the type of resizing based on the event
|
|
* @private
|
|
*/
|
|
_resetCurrentTransform: function() {
|
|
var t = this._currentTransform;
|
|
|
|
t.target.set({
|
|
scaleX: t.original.scaleX,
|
|
scaleY: t.original.scaleY,
|
|
skewX: t.original.skewX,
|
|
skewY: t.original.skewY,
|
|
left: t.original.left,
|
|
top: t.original.top
|
|
});
|
|
|
|
if (this._shouldCenterTransform(t.target)) {
|
|
if (t.action === 'rotate') {
|
|
this._setOriginToCenter(t.target);
|
|
}
|
|
else {
|
|
if (t.originX !== 'center') {
|
|
if (t.originX === 'right') {
|
|
t.mouseXSign = -1;
|
|
}
|
|
else {
|
|
t.mouseXSign = 1;
|
|
}
|
|
}
|
|
if (t.originY !== 'center') {
|
|
if (t.originY === 'bottom') {
|
|
t.mouseYSign = -1;
|
|
}
|
|
else {
|
|
t.mouseYSign = 1;
|
|
}
|
|
}
|
|
|
|
t.originX = 'center';
|
|
t.originY = 'center';
|
|
}
|
|
}
|
|
else {
|
|
t.originX = t.original.originX;
|
|
t.originY = t.original.originY;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Checks if point is contained within an area of given object
|
|
* @param {Event} e Event object
|
|
* @param {fabric.Object} target Object to test against
|
|
* @param {Object} [point] x,y object of point coordinates we want to check.
|
|
* @return {Boolean} true if point is contained within an area of given object
|
|
*/
|
|
containsPoint: function (e, target, point) {
|
|
var ignoreZoom = true,
|
|
pointer = point || this.getPointer(e, ignoreZoom),
|
|
xy;
|
|
|
|
if (target.group && target.group === this.getActiveGroup()) {
|
|
xy = this._normalizePointer(target.group, pointer);
|
|
}
|
|
else {
|
|
xy = { x: pointer.x, y: pointer.y };
|
|
}
|
|
// http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html
|
|
// http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
|
|
return (target.containsPoint(xy) || target._findTargetCorner(pointer));
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_normalizePointer: function (object, pointer) {
|
|
var m = object.calcTransformMatrix(),
|
|
invertedM = fabric.util.invertTransform(m),
|
|
vpt = this.viewportTransform,
|
|
vptPointer = this.restorePointerVpt(pointer),
|
|
p = fabric.util.transformPoint(vptPointer, invertedM);
|
|
return fabric.util.transformPoint(p, vpt);
|
|
},
|
|
|
|
/**
|
|
* Returns true if object is transparent at a certain location
|
|
* @param {fabric.Object} target Object to check
|
|
* @param {Number} x Left coordinate
|
|
* @param {Number} y Top coordinate
|
|
* @return {Boolean}
|
|
*/
|
|
isTargetTransparent: function (target, x, y) {
|
|
var hasBorders = target.hasBorders,
|
|
transparentCorners = target.transparentCorners,
|
|
ctx = this.contextCache,
|
|
originalColor = target.selectionBackgroundColor;
|
|
|
|
target.hasBorders = target.transparentCorners = false;
|
|
target.selectionBackgroundColor = '';
|
|
|
|
ctx.save();
|
|
ctx.transform.apply(ctx, this.viewportTransform);
|
|
target.render(ctx);
|
|
ctx.restore();
|
|
|
|
target.active && target._renderControls(ctx);
|
|
|
|
target.hasBorders = hasBorders;
|
|
target.transparentCorners = transparentCorners;
|
|
target.selectionBackgroundColor = originalColor;
|
|
|
|
var isTransparent = fabric.util.isTransparent(
|
|
ctx, x, y, this.targetFindTolerance);
|
|
|
|
this.clearContext(ctx);
|
|
|
|
return isTransparent;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Event} e Event object
|
|
* @param {fabric.Object} target
|
|
*/
|
|
_shouldClearSelection: function (e, target) {
|
|
var activeGroup = this.getActiveGroup(),
|
|
activeObject = this.getActiveObject();
|
|
|
|
return (
|
|
!target
|
|
||
|
|
(target &&
|
|
activeGroup &&
|
|
!activeGroup.contains(target) &&
|
|
activeGroup !== target &&
|
|
!e[this.selectionKey])
|
|
||
|
|
(target && !target.evented)
|
|
||
|
|
(target &&
|
|
!target.selectable &&
|
|
activeObject &&
|
|
activeObject !== target)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {fabric.Object} target
|
|
*/
|
|
_shouldCenterTransform: function (target) {
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
var t = this._currentTransform,
|
|
centerTransform;
|
|
|
|
if (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY') {
|
|
centerTransform = this.centeredScaling || target.centeredScaling;
|
|
}
|
|
else if (t.action === 'rotate') {
|
|
centerTransform = this.centeredRotation || target.centeredRotation;
|
|
}
|
|
|
|
return centerTransform ? !t.altKey : t.altKey;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getOriginFromCorner: function(target, corner) {
|
|
var origin = {
|
|
x: target.originX,
|
|
y: target.originY
|
|
};
|
|
|
|
if (corner === 'ml' || corner === 'tl' || corner === 'bl') {
|
|
origin.x = 'right';
|
|
}
|
|
else if (corner === 'mr' || corner === 'tr' || corner === 'br') {
|
|
origin.x = 'left';
|
|
}
|
|
|
|
if (corner === 'tl' || corner === 'mt' || corner === 'tr') {
|
|
origin.y = 'bottom';
|
|
}
|
|
else if (corner === 'bl' || corner === 'mb' || corner === 'br') {
|
|
origin.y = 'top';
|
|
}
|
|
|
|
return origin;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getActionFromCorner: function(target, corner, e) {
|
|
if (!corner) {
|
|
return 'drag';
|
|
}
|
|
|
|
switch (corner) {
|
|
case 'mtr':
|
|
return 'rotate';
|
|
case 'ml':
|
|
case 'mr':
|
|
return e[this.altActionKey] ? 'skewY' : 'scaleX';
|
|
case 'mt':
|
|
case 'mb':
|
|
return e[this.altActionKey] ? 'skewX' : 'scaleY';
|
|
default:
|
|
return 'scale';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Event} e Event object
|
|
* @param {fabric.Object} target
|
|
*/
|
|
_setupCurrentTransform: function (e, target) {
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
var pointer = this.getPointer(e),
|
|
corner = target._findTargetCorner(this.getPointer(e, true)),
|
|
action = this._getActionFromCorner(target, corner, e),
|
|
origin = this._getOriginFromCorner(target, corner);
|
|
|
|
this._currentTransform = {
|
|
target: target,
|
|
action: action,
|
|
corner: corner,
|
|
scaleX: target.scaleX,
|
|
scaleY: target.scaleY,
|
|
skewX: target.skewX,
|
|
skewY: target.skewY,
|
|
offsetX: pointer.x - target.left,
|
|
offsetY: pointer.y - target.top,
|
|
originX: origin.x,
|
|
originY: origin.y,
|
|
ex: pointer.x,
|
|
ey: pointer.y,
|
|
lastX: pointer.x,
|
|
lastY: pointer.y,
|
|
left: target.left,
|
|
top: target.top,
|
|
theta: degreesToRadians(target.angle),
|
|
width: target.width * target.scaleX,
|
|
mouseXSign: 1,
|
|
mouseYSign: 1,
|
|
shiftKey: e.shiftKey,
|
|
altKey: e[this.centeredKey]
|
|
};
|
|
|
|
this._currentTransform.original = {
|
|
left: target.left,
|
|
top: target.top,
|
|
scaleX: target.scaleX,
|
|
scaleY: target.scaleY,
|
|
skewX: target.skewX,
|
|
skewY: target.skewY,
|
|
originX: origin.x,
|
|
originY: origin.y
|
|
};
|
|
|
|
this._resetCurrentTransform();
|
|
},
|
|
|
|
/**
|
|
* Translates object by "setting" its left/top
|
|
* @private
|
|
* @param {Number} x pointer's x coordinate
|
|
* @param {Number} y pointer's y coordinate
|
|
* @return {Boolean} true if the translation occurred
|
|
*/
|
|
_translateObject: function (x, y) {
|
|
var transform = this._currentTransform,
|
|
target = transform.target,
|
|
newLeft = x - transform.offsetX,
|
|
newTop = y - transform.offsetY,
|
|
moveX = !target.get('lockMovementX') && target.left !== newLeft,
|
|
moveY = !target.get('lockMovementY') && target.top !== newTop;
|
|
|
|
moveX && target.set('left', newLeft);
|
|
moveY && target.set('top', newTop);
|
|
return moveX || moveY;
|
|
},
|
|
|
|
/**
|
|
* Check if we are increasing a positive skew or lower it,
|
|
* checking mouse direction and pressed corner.
|
|
* @private
|
|
*/
|
|
_changeSkewTransformOrigin: function(mouseMove, t, by) {
|
|
var property = 'originX', origins = { 0: 'center' },
|
|
skew = t.target.skewX, originA = 'left', originB = 'right',
|
|
corner = t.corner === 'mt' || t.corner === 'ml' ? 1 : -1,
|
|
flipSign = 1;
|
|
|
|
mouseMove = mouseMove > 0 ? 1 : -1;
|
|
if (by === 'y') {
|
|
skew = t.target.skewY;
|
|
originA = 'top';
|
|
originB = 'bottom';
|
|
property = 'originY';
|
|
}
|
|
origins[-1] = originA;
|
|
origins[1] = originB;
|
|
|
|
t.target.flipX && (flipSign *= -1);
|
|
t.target.flipY && (flipSign *= -1);
|
|
|
|
if (skew === 0) {
|
|
t.skewSign = -corner * mouseMove * flipSign;
|
|
t[property] = origins[-mouseMove];
|
|
}
|
|
else {
|
|
skew = skew > 0 ? 1 : -1;
|
|
t.skewSign = skew;
|
|
t[property] = origins[skew * corner * flipSign];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Skew object by mouse events
|
|
* @private
|
|
* @param {Number} x pointer's x coordinate
|
|
* @param {Number} y pointer's y coordinate
|
|
* @param {String} by Either 'x' or 'y'
|
|
* @return {Boolean} true if the skewing occurred
|
|
*/
|
|
_skewObject: function (x, y, by) {
|
|
var t = this._currentTransform,
|
|
target = t.target, skewed = false,
|
|
lockSkewingX = target.get('lockSkewingX'),
|
|
lockSkewingY = target.get('lockSkewingY');
|
|
|
|
if ((lockSkewingX && by === 'x') || (lockSkewingY && by === 'y')) {
|
|
return false;
|
|
}
|
|
|
|
// Get the constraint point
|
|
var center = target.getCenterPoint(),
|
|
actualMouseByCenter = target.toLocalPoint(new fabric.Point(x, y), 'center', 'center')[by],
|
|
lastMouseByCenter = target.toLocalPoint(new fabric.Point(t.lastX, t.lastY), 'center', 'center')[by],
|
|
actualMouseByOrigin, constraintPosition, dim = target._getTransformedDimensions();
|
|
|
|
this._changeSkewTransformOrigin(actualMouseByCenter - lastMouseByCenter, t, by);
|
|
actualMouseByOrigin = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY)[by];
|
|
constraintPosition = target.translateToOriginPoint(center, t.originX, t.originY);
|
|
// Actually skew the object
|
|
skewed = this._setObjectSkew(actualMouseByOrigin, t, by, dim);
|
|
t.lastX = x;
|
|
t.lastY = y;
|
|
// Make sure the constraints apply
|
|
target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
|
|
return skewed;
|
|
},
|
|
|
|
/**
|
|
* Set object skew
|
|
* @private
|
|
* @return {Boolean} true if the skewing occurred
|
|
*/
|
|
_setObjectSkew: function(localMouse, transform, by, _dim) {
|
|
var target = transform.target, newValue, skewed = false,
|
|
skewSign = transform.skewSign, newDim, dimNoSkew,
|
|
otherBy, _otherBy, _by, newDimMouse, skewX, skewY;
|
|
|
|
if (by === 'x') {
|
|
otherBy = 'y';
|
|
_otherBy = 'Y';
|
|
_by = 'X';
|
|
skewX = 0;
|
|
skewY = target.skewY;
|
|
}
|
|
else {
|
|
otherBy = 'x';
|
|
_otherBy = 'X';
|
|
_by = 'Y';
|
|
skewX = target.skewX;
|
|
skewY = 0;
|
|
}
|
|
|
|
dimNoSkew = target._getTransformedDimensions(skewX, skewY);
|
|
newDimMouse = 2 * Math.abs(localMouse) - dimNoSkew[by];
|
|
if (newDimMouse <= 2) {
|
|
newValue = 0;
|
|
}
|
|
else {
|
|
newValue = skewSign * Math.atan((newDimMouse / target['scale' + _by]) /
|
|
(dimNoSkew[otherBy] / target['scale' + _otherBy]));
|
|
newValue = fabric.util.radiansToDegrees(newValue);
|
|
}
|
|
skewed = target['skew' + _by] !== newValue;
|
|
target.set('skew' + _by, newValue);
|
|
if (target['skew' + _otherBy] !== 0) {
|
|
newDim = target._getTransformedDimensions();
|
|
newValue = (_dim[otherBy] / newDim[otherBy]) * target['scale' + _otherBy];
|
|
target.set('scale' + _otherBy, newValue);
|
|
}
|
|
return skewed;
|
|
},
|
|
|
|
/**
|
|
* Scales object by invoking its scaleX/scaleY methods
|
|
* @private
|
|
* @param {Number} x pointer's x coordinate
|
|
* @param {Number} y pointer's y coordinate
|
|
* @param {String} by Either 'x' or 'y' - specifies dimension constraint by which to scale an object.
|
|
* When not provided, an object is scaled by both dimensions equally
|
|
* @return {Boolean} true if the scaling occurred
|
|
*/
|
|
_scaleObject: function (x, y, by) {
|
|
var t = this._currentTransform,
|
|
target = t.target,
|
|
lockScalingX = target.get('lockScalingX'),
|
|
lockScalingY = target.get('lockScalingY'),
|
|
lockScalingFlip = target.get('lockScalingFlip');
|
|
|
|
if (lockScalingX && lockScalingY) {
|
|
return false;
|
|
}
|
|
|
|
// Get the constraint point
|
|
var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY),
|
|
localMouse = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY),
|
|
dim = target._getTransformedDimensions(), scaled = false;
|
|
|
|
this._setLocalMouse(localMouse, t);
|
|
|
|
// Actually scale the object
|
|
scaled = this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip, dim);
|
|
|
|
// Make sure the constraints apply
|
|
target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
|
|
return scaled;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @return {Boolean} true if the scaling occurred
|
|
*/
|
|
_setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim) {
|
|
var target = transform.target, forbidScalingX = false, forbidScalingY = false, scaled = false,
|
|
changeX, changeY, scaleX, scaleY;
|
|
|
|
scaleX = localMouse.x * target.scaleX / _dim.x;
|
|
scaleY = localMouse.y * target.scaleY / _dim.y;
|
|
changeX = target.scaleX !== scaleX;
|
|
changeY = target.scaleY !== scaleY;
|
|
|
|
if (lockScalingFlip && scaleX <= 0 && scaleX < target.scaleX) {
|
|
forbidScalingX = true;
|
|
}
|
|
|
|
if (lockScalingFlip && scaleY <= 0 && scaleY < target.scaleY) {
|
|
forbidScalingY = true;
|
|
}
|
|
|
|
if (by === 'equally' && !lockScalingX && !lockScalingY) {
|
|
forbidScalingX || forbidScalingY || (scaled = this._scaleObjectEqually(localMouse, target, transform, _dim));
|
|
}
|
|
else if (!by) {
|
|
forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = scaled || changeX));
|
|
forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY));
|
|
}
|
|
else if (by === 'x' && !target.get('lockUniScaling')) {
|
|
forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = scaled || changeX));
|
|
}
|
|
else if (by === 'y' && !target.get('lockUniScaling')) {
|
|
forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY));
|
|
}
|
|
transform.newScaleX = scaleX;
|
|
transform.newScaleY = scaleY;
|
|
forbidScalingX || forbidScalingY || this._flipObject(transform, by);
|
|
return scaled;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @return {Boolean} true if the scaling occurred
|
|
*/
|
|
_scaleObjectEqually: function(localMouse, target, transform, _dim) {
|
|
|
|
var dist = localMouse.y + localMouse.x,
|
|
lastDist = _dim.y * transform.original.scaleY / target.scaleY +
|
|
_dim.x * transform.original.scaleX / target.scaleX,
|
|
scaled;
|
|
|
|
// We use transform.scaleX/Y instead of target.scaleX/Y
|
|
// because the object may have a min scale and we'll loose the proportions
|
|
transform.newScaleX = transform.original.scaleX * dist / lastDist;
|
|
transform.newScaleY = transform.original.scaleY * dist / lastDist;
|
|
scaled = transform.newScaleX !== target.scaleX || transform.newScaleY !== target.scaleY;
|
|
target.set('scaleX', transform.newScaleX);
|
|
target.set('scaleY', transform.newScaleY);
|
|
return scaled;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_flipObject: function(transform, by) {
|
|
if (transform.newScaleX < 0 && by !== 'y') {
|
|
if (transform.originX === 'left') {
|
|
transform.originX = 'right';
|
|
}
|
|
else if (transform.originX === 'right') {
|
|
transform.originX = 'left';
|
|
}
|
|
}
|
|
|
|
if (transform.newScaleY < 0 && by !== 'x') {
|
|
if (transform.originY === 'top') {
|
|
transform.originY = 'bottom';
|
|
}
|
|
else if (transform.originY === 'bottom') {
|
|
transform.originY = 'top';
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setLocalMouse: function(localMouse, t) {
|
|
var target = t.target;
|
|
|
|
if (t.originX === 'right') {
|
|
localMouse.x *= -1;
|
|
}
|
|
else if (t.originX === 'center') {
|
|
localMouse.x *= t.mouseXSign * 2;
|
|
if (localMouse.x < 0) {
|
|
t.mouseXSign = -t.mouseXSign;
|
|
}
|
|
}
|
|
|
|
if (t.originY === 'bottom') {
|
|
localMouse.y *= -1;
|
|
}
|
|
else if (t.originY === 'center') {
|
|
localMouse.y *= t.mouseYSign * 2;
|
|
if (localMouse.y < 0) {
|
|
t.mouseYSign = -t.mouseYSign;
|
|
}
|
|
}
|
|
|
|
// adjust the mouse coordinates when dealing with padding
|
|
if (abs(localMouse.x) > target.padding) {
|
|
if (localMouse.x < 0) {
|
|
localMouse.x += target.padding;
|
|
}
|
|
else {
|
|
localMouse.x -= target.padding;
|
|
}
|
|
}
|
|
else { // mouse is within the padding, set to 0
|
|
localMouse.x = 0;
|
|
}
|
|
|
|
if (abs(localMouse.y) > target.padding) {
|
|
if (localMouse.y < 0) {
|
|
localMouse.y += target.padding;
|
|
}
|
|
else {
|
|
localMouse.y -= target.padding;
|
|
}
|
|
}
|
|
else {
|
|
localMouse.y = 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Rotates object by invoking its rotate method
|
|
* @private
|
|
* @param {Number} x pointer's x coordinate
|
|
* @param {Number} y pointer's y coordinate
|
|
* @return {Boolean} true if the rotation occurred
|
|
*/
|
|
_rotateObject: function (x, y) {
|
|
|
|
var t = this._currentTransform;
|
|
|
|
if (t.target.get('lockRotation')) {
|
|
return false;
|
|
}
|
|
|
|
var lastAngle = atan2(t.ey - t.top, t.ex - t.left),
|
|
curAngle = atan2(y - t.top, x - t.left),
|
|
angle = radiansToDegrees(curAngle - lastAngle + t.theta);
|
|
|
|
// normalize angle to positive value
|
|
if (angle < 0) {
|
|
angle = 360 + angle;
|
|
}
|
|
|
|
t.target.angle = angle % 360;
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Set the cursor type of the canvas element
|
|
* @param {String} value Cursor type of the canvas element.
|
|
* @see http://www.w3.org/TR/css3-ui/#cursor
|
|
*/
|
|
setCursor: function (value) {
|
|
this.upperCanvasEl.style.cursor = value;
|
|
},
|
|
|
|
/**
|
|
* @param {fabric.Object} target to reset transform
|
|
* @private
|
|
*/
|
|
_resetObjectTransform: function (target) {
|
|
target.scaleX = 1;
|
|
target.scaleY = 1;
|
|
target.skewX = 0;
|
|
target.skewY = 0;
|
|
target.setAngle(0);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx to draw the selection on
|
|
*/
|
|
_drawSelection: function (ctx) {
|
|
var groupSelector = this._groupSelector,
|
|
left = groupSelector.left,
|
|
top = groupSelector.top,
|
|
aleft = abs(left),
|
|
atop = abs(top);
|
|
|
|
if (this.selectionColor) {
|
|
ctx.fillStyle = this.selectionColor;
|
|
|
|
ctx.fillRect(
|
|
groupSelector.ex - ((left > 0) ? 0 : -left),
|
|
groupSelector.ey - ((top > 0) ? 0 : -top),
|
|
aleft,
|
|
atop
|
|
);
|
|
}
|
|
|
|
if (!this.selectionLineWidth || !this.selectionBorderColor) {
|
|
return;
|
|
}
|
|
ctx.lineWidth = this.selectionLineWidth;
|
|
ctx.strokeStyle = this.selectionBorderColor;
|
|
|
|
// selection border
|
|
if (this.selectionDashArray.length > 1 && !supportLineDash) {
|
|
|
|
var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft),
|
|
py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop);
|
|
|
|
ctx.beginPath();
|
|
|
|
fabric.util.drawDashedLine(ctx, px, py, px + aleft, py, this.selectionDashArray);
|
|
fabric.util.drawDashedLine(ctx, px, py + atop - 1, px + aleft, py + atop - 1, this.selectionDashArray);
|
|
fabric.util.drawDashedLine(ctx, px, py, px, py + atop, this.selectionDashArray);
|
|
fabric.util.drawDashedLine(ctx, px + aleft - 1, py, px + aleft - 1, py + atop, this.selectionDashArray);
|
|
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
}
|
|
else {
|
|
fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray);
|
|
ctx.strokeRect(
|
|
groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft),
|
|
groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop),
|
|
aleft,
|
|
atop
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method that determines what object we are clicking on
|
|
* @param {Event} e mouse event
|
|
* @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through
|
|
*/
|
|
findTarget: function (e, skipGroup) {
|
|
if (this.skipTargetFind) {
|
|
return;
|
|
}
|
|
|
|
var ignoreZoom = true,
|
|
pointer = this.getPointer(e, ignoreZoom),
|
|
activeGroup = this.getActiveGroup(),
|
|
activeObject = this.getActiveObject(),
|
|
activeTarget;
|
|
|
|
// first check current group (if one exists)
|
|
// active group does not check sub targets like normal groups.
|
|
// if active group just exits.
|
|
if (activeGroup && !skipGroup && this._checkTarget(pointer, activeGroup)) {
|
|
this._fireOverOutEvents(activeGroup, e);
|
|
return activeGroup;
|
|
}
|
|
// if we hit the corner of an activeObject, let's return that.
|
|
if (activeObject && activeObject._findTargetCorner(pointer)) {
|
|
this._fireOverOutEvents(activeObject, e);
|
|
return activeObject;
|
|
}
|
|
if (activeObject && this._checkTarget(pointer, activeObject)) {
|
|
if (!this.preserveObjectStacking) {
|
|
this._fireOverOutEvents(activeObject, e);
|
|
return activeObject;
|
|
}
|
|
else {
|
|
activeTarget = activeObject;
|
|
}
|
|
}
|
|
|
|
this.targets = [];
|
|
|
|
var target = this._searchPossibleTargets(this._objects, pointer);
|
|
if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) {
|
|
target = activeTarget;
|
|
}
|
|
this._fireOverOutEvents(target, e);
|
|
return target;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_fireOverOutEvents: function(target, e) {
|
|
if (target) {
|
|
if (this._hoveredTarget !== target) {
|
|
if (this._hoveredTarget) {
|
|
this.fire('mouse:out', { target: this._hoveredTarget, e: e });
|
|
this._hoveredTarget.fire('mouseout');
|
|
}
|
|
this.fire('mouse:over', { target: target, e: e });
|
|
target.fire('mouseover');
|
|
this._hoveredTarget = target;
|
|
}
|
|
}
|
|
else if (this._hoveredTarget) {
|
|
this.fire('mouse:out', { target: this._hoveredTarget, e: e });
|
|
this._hoveredTarget.fire('mouseout');
|
|
this._hoveredTarget = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_checkTarget: function(pointer, obj) {
|
|
if (obj &&
|
|
obj.visible &&
|
|
obj.evented &&
|
|
this.containsPoint(null, obj, pointer)){
|
|
if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) {
|
|
var isTransparent = this.isTargetTransparent(obj, pointer.x, pointer.y);
|
|
if (!isTransparent) {
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_searchPossibleTargets: function(objects, pointer) {
|
|
|
|
// Cache all targets where their bounding box contains point.
|
|
var target, i = objects.length, normalizedPointer, subTarget;
|
|
// Do not check for currently grouped objects, since we check the parent group itself.
|
|
// untill we call this function specifically to search inside the activeGroup
|
|
while (i--) {
|
|
if (this._checkTarget(pointer, objects[i])) {
|
|
target = objects[i];
|
|
if (target.type === 'group' && target.subTargetCheck) {
|
|
normalizedPointer = this._normalizePointer(target, pointer);
|
|
subTarget = this._searchPossibleTargets(target._objects, normalizedPointer);
|
|
subTarget && this.targets.push(subTarget);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return target;
|
|
},
|
|
|
|
/**
|
|
* Returns pointer coordinates without the effect of the viewport
|
|
* @param {Object} pointer with "x" and "y" number values
|
|
* @return {Object} object with "x" and "y" number values
|
|
*/
|
|
restorePointerVpt: function(pointer) {
|
|
return fabric.util.transformPoint(
|
|
pointer,
|
|
fabric.util.invertTransform(this.viewportTransform)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Returns pointer coordinates relative to canvas.
|
|
* @param {Event} e
|
|
* @param {Boolean} ignoreZoom
|
|
* @return {Object} object with "x" and "y" number values
|
|
*/
|
|
getPointer: function (e, ignoreZoom, upperCanvasEl) {
|
|
if (!upperCanvasEl) {
|
|
upperCanvasEl = this.upperCanvasEl;
|
|
}
|
|
var pointer = getPointer(e),
|
|
bounds = upperCanvasEl.getBoundingClientRect(),
|
|
boundsWidth = bounds.width || 0,
|
|
boundsHeight = bounds.height || 0,
|
|
cssScale;
|
|
|
|
if (!boundsWidth || !boundsHeight ) {
|
|
if ('top' in bounds && 'bottom' in bounds) {
|
|
boundsHeight = Math.abs( bounds.top - bounds.bottom );
|
|
}
|
|
if ('right' in bounds && 'left' in bounds) {
|
|
boundsWidth = Math.abs( bounds.right - bounds.left );
|
|
}
|
|
}
|
|
|
|
this.calcOffset();
|
|
|
|
pointer.x = pointer.x - this._offset.left;
|
|
pointer.y = pointer.y - this._offset.top;
|
|
if (!ignoreZoom) {
|
|
pointer = this.restorePointerVpt(pointer);
|
|
}
|
|
|
|
if (boundsWidth === 0 || boundsHeight === 0) {
|
|
// If bounds are not available (i.e. not visible), do not apply scale.
|
|
cssScale = { width: 1, height: 1 };
|
|
}
|
|
else {
|
|
cssScale = {
|
|
width: upperCanvasEl.width / boundsWidth,
|
|
height: upperCanvasEl.height / boundsHeight
|
|
};
|
|
}
|
|
|
|
return {
|
|
x: pointer.x * cssScale.width,
|
|
y: pointer.y * cssScale.height
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @throws {CANVAS_INIT_ERROR} If canvas can not be initialized
|
|
*/
|
|
_createUpperCanvas: function () {
|
|
var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, '');
|
|
|
|
this.upperCanvasEl = this._createCanvasElement();
|
|
fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass);
|
|
|
|
this.wrapperEl.appendChild(this.upperCanvasEl);
|
|
|
|
this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl);
|
|
this._applyCanvasStyle(this.upperCanvasEl);
|
|
this.contextTop = this.upperCanvasEl.getContext('2d');
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_createCacheCanvas: function () {
|
|
this.cacheCanvasEl = this._createCanvasElement();
|
|
this.cacheCanvasEl.setAttribute('width', this.width);
|
|
this.cacheCanvasEl.setAttribute('height', this.height);
|
|
this.contextCache = this.cacheCanvasEl.getContext('2d');
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_initWrapperElement: function () {
|
|
this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', {
|
|
'class': this.containerClass
|
|
});
|
|
fabric.util.setStyle(this.wrapperEl, {
|
|
width: this.getWidth() + 'px',
|
|
height: this.getHeight() + 'px',
|
|
position: 'relative'
|
|
});
|
|
fabric.util.makeElementUnselectable(this.wrapperEl);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {HTMLElement} element canvas element to apply styles on
|
|
*/
|
|
_applyCanvasStyle: function (element) {
|
|
var width = this.getWidth() || element.width,
|
|
height = this.getHeight() || element.height;
|
|
|
|
fabric.util.setStyle(element, {
|
|
position: 'absolute',
|
|
width: width + 'px',
|
|
height: height + 'px',
|
|
left: 0,
|
|
top: 0
|
|
});
|
|
element.width = width;
|
|
element.height = height;
|
|
fabric.util.makeElementUnselectable(element);
|
|
},
|
|
|
|
/**
|
|
* Copys the the entire inline style from one element (fromEl) to another (toEl)
|
|
* @private
|
|
* @param {Element} fromEl Element style is copied from
|
|
* @param {Element} toEl Element copied style is applied to
|
|
*/
|
|
_copyCanvasStyle: function (fromEl, toEl) {
|
|
toEl.style.cssText = fromEl.style.cssText;
|
|
},
|
|
|
|
/**
|
|
* Returns context of canvas where object selection is drawn
|
|
* @return {CanvasRenderingContext2D}
|
|
*/
|
|
getSelectionContext: function() {
|
|
return this.contextTop;
|
|
},
|
|
|
|
/**
|
|
* Returns <canvas> element on which object selection is drawn
|
|
* @return {HTMLCanvasElement}
|
|
*/
|
|
getSelectionElement: function () {
|
|
return this.upperCanvasEl;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Object} object
|
|
*/
|
|
_setActiveObject: function(object) {
|
|
if (this._activeObject) {
|
|
this._activeObject.set('active', false);
|
|
}
|
|
this._activeObject = object;
|
|
object.set('active', true);
|
|
},
|
|
|
|
/**
|
|
* Sets given object as the only active object on canvas
|
|
* @param {fabric.Object} object Object to set as an active one
|
|
* @param {Event} [e] Event (passed along when firing "object:selected")
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
setActiveObject: function (object, e) {
|
|
this._setActiveObject(object);
|
|
this.renderAll();
|
|
this.fire('object:selected', { target: object, e: e });
|
|
object.fire('selected', { e: e });
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns currently active object
|
|
* @return {fabric.Object} active object
|
|
*/
|
|
getActiveObject: function () {
|
|
return this._activeObject;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {fabric.Object} obj Object that was removed
|
|
*/
|
|
_onObjectRemoved: function(obj) {
|
|
// removing active object should fire "selection:cleared" events
|
|
if (this.getActiveObject() === obj) {
|
|
this.fire('before:selection:cleared', { target: obj });
|
|
this._discardActiveObject();
|
|
this.fire('selection:cleared', { target: obj });
|
|
obj.fire('deselected');
|
|
}
|
|
this.callSuper('_onObjectRemoved', obj);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_discardActiveObject: function() {
|
|
if (this._activeObject) {
|
|
this._activeObject.set('active', false);
|
|
}
|
|
this._activeObject = null;
|
|
},
|
|
|
|
/**
|
|
* Discards currently active object and fire events
|
|
* @param {event} e
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
discardActiveObject: function (e) {
|
|
var activeObject = this._activeObject;
|
|
this.fire('before:selection:cleared', { target: activeObject, e: e });
|
|
this._discardActiveObject();
|
|
this.fire('selection:cleared', { e: e });
|
|
activeObject && activeObject.fire('deselected', { e: e });
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {fabric.Group} group
|
|
*/
|
|
_setActiveGroup: function(group) {
|
|
this._activeGroup = group;
|
|
if (group) {
|
|
group.set('active', true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets active group to a specified one
|
|
* @param {fabric.Group} group Group to set as a current one
|
|
* @param {Event} e Event object
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
setActiveGroup: function (group, e) {
|
|
this._setActiveGroup(group);
|
|
if (group) {
|
|
this.fire('object:selected', { target: group, e: e });
|
|
group.fire('selected', { e: e });
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns currently active group
|
|
* @return {fabric.Group} Current group
|
|
*/
|
|
getActiveGroup: function () {
|
|
return this._activeGroup;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_discardActiveGroup: function() {
|
|
var g = this.getActiveGroup();
|
|
if (g) {
|
|
g.destroy();
|
|
}
|
|
this.setActiveGroup(null);
|
|
},
|
|
|
|
/**
|
|
* Discards currently active group and fire events
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
discardActiveGroup: function (e) {
|
|
var g = this.getActiveGroup();
|
|
this.fire('before:selection:cleared', { e: e, target: g });
|
|
this._discardActiveGroup();
|
|
this.fire('selection:cleared', { e: e });
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Deactivates all objects on canvas, removing any active group or object
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
deactivateAll: function () {
|
|
var allObjects = this.getObjects(),
|
|
i = 0,
|
|
len = allObjects.length;
|
|
for ( ; i < len; i++) {
|
|
allObjects[i].set('active', false);
|
|
}
|
|
this._discardActiveGroup();
|
|
this._discardActiveObject();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Deactivates all objects and dispatches appropriate events
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
deactivateAllWithDispatch: function (e) {
|
|
var activeGroup = this.getActiveGroup(),
|
|
activeObject = this.getActiveObject();
|
|
if (activeObject || activeGroup) {
|
|
this.fire('before:selection:cleared', { target: activeObject || activeGroup, e: e });
|
|
}
|
|
this.deactivateAll();
|
|
if (activeObject || activeGroup) {
|
|
this.fire('selection:cleared', { e: e, target: activeObject });
|
|
activeObject && activeObject.fire('deselected');
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Clears a canvas element and removes all event listeners
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
dispose: function () {
|
|
this.callSuper('dispose');
|
|
var wrapper = this.wrapperEl;
|
|
this.removeListeners();
|
|
wrapper.removeChild(this.upperCanvasEl);
|
|
wrapper.removeChild(this.lowerCanvasEl);
|
|
delete this.upperCanvasEl;
|
|
if (wrapper.parentNode) {
|
|
wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl);
|
|
}
|
|
delete this.wrapperEl;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Clears all contexts (background, main, top) of an instance
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
clear: function () {
|
|
this.discardActiveGroup();
|
|
this.discardActiveObject();
|
|
this.clearContext(this.contextTop);
|
|
return this.callSuper('clear');
|
|
},
|
|
|
|
/**
|
|
* Draws objects' controls (borders/controls)
|
|
* @param {CanvasRenderingContext2D} ctx Context to render controls on
|
|
*/
|
|
drawControls: function(ctx) {
|
|
var activeGroup = this.getActiveGroup();
|
|
|
|
if (activeGroup) {
|
|
activeGroup._renderControls(ctx);
|
|
}
|
|
else {
|
|
this._drawObjectsControls(ctx);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_drawObjectsControls: function(ctx) {
|
|
for (var i = 0, len = this._objects.length; i < len; ++i) {
|
|
if (!this._objects[i] || !this._objects[i].active) {
|
|
continue;
|
|
}
|
|
this._objects[i]._renderControls(ctx);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_toObject: function(instance, methodName, propertiesToInclude) {
|
|
//If the object is part of the current selection group, it should
|
|
//be transformed appropriately
|
|
//i.e. it should be serialised as it would appear if the selection group
|
|
//were to be destroyed.
|
|
var originalProperties = this._realizeGroupTransformOnObject(instance),
|
|
object = this.callSuper('_toObject', instance, methodName, propertiesToInclude);
|
|
//Undo the damage we did by changing all of its properties
|
|
this._unwindGroupTransformOnObject(instance, originalProperties);
|
|
return object;
|
|
},
|
|
|
|
/**
|
|
* Realises an object's group transformation on it
|
|
* @private
|
|
* @param {fabric.Object} [instance] the object to transform (gets mutated)
|
|
* @returns the original values of instance which were changed
|
|
*/
|
|
_realizeGroupTransformOnObject: function(instance) {
|
|
var layoutProps = ['angle', 'flipX', 'flipY', 'height', 'left', 'scaleX', 'scaleY', 'top', 'width'];
|
|
if (instance.group && instance.group === this.getActiveGroup()) {
|
|
//Copy all the positionally relevant properties across now
|
|
var originalValues = {};
|
|
layoutProps.forEach(function(prop) {
|
|
originalValues[prop] = instance[prop];
|
|
});
|
|
this.getActiveGroup().realizeTransform(instance);
|
|
return originalValues;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Restores the changed properties of instance
|
|
* @private
|
|
* @param {fabric.Object} [instance] the object to un-transform (gets mutated)
|
|
* @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject
|
|
*/
|
|
_unwindGroupTransformOnObject: function(instance, originalValues) {
|
|
if (originalValues) {
|
|
instance.set(originalValues);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setSVGObject: function(markup, instance, reviver) {
|
|
var originalProperties;
|
|
//If the object is in a selection group, simulate what would happen to that
|
|
//object when the group is deselected
|
|
originalProperties = this._realizeGroupTransformOnObject(instance);
|
|
this.callSuper('_setSVGObject', markup, instance, reviver);
|
|
this._unwindGroupTransformOnObject(instance, originalProperties);
|
|
},
|
|
});
|
|
|
|
// copying static properties manually to work around Opera's bug,
|
|
// where "prototype" property is enumerable and overrides existing prototype
|
|
for (var prop in fabric.StaticCanvas) {
|
|
if (prop !== 'prototype') {
|
|
fabric.Canvas[prop] = fabric.StaticCanvas[prop];
|
|
}
|
|
}
|
|
|
|
if (fabric.isTouchSupported) {
|
|
/** @ignore */
|
|
fabric.Canvas.prototype._setCursorFromEvent = function() { };
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
* @class fabric.Element
|
|
* @alias fabric.Canvas
|
|
* @deprecated Use {@link fabric.Canvas} instead.
|
|
* @constructor
|
|
*/
|
|
fabric.Element = fabric.Canvas;
|
|
})();
|