fabric.js/src/canvas.class.js
2015-11-17 21:29:39 +01:00

1295 lines
36 KiB
JavaScript

(function() {
var getPointer = fabric.util.getPointer,
degreesToRadians = fabric.util.degreesToRadians,
radiansToDegrees = fabric.util.radiansToDegrees,
atan2 = Math.atan2,
abs = Math.abs,
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: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();
fabric.Canvas.activeInstance = this;
},
/**
* When true, objects can be transformed by one side (unproportionally)
* @type Boolean
* @default
*/
uniScaleTransform: false,
/**
* 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 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,
/**
* 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,
/**
* @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();
},
/**
* Resets the current transform to its original values and chooses the type of resizing based on the event
* @private
* @param {Event} e Event object fired on mousemove
*/
_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
* @return {Boolean} true if point is contained within an area of given object
*/
containsPoint: function (e, target) {
var pointer = this.getPointer(e, true),
xy = this._normalizePointer(target, pointer);
// 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 activeGroup = this.getActiveGroup(),
x = pointer.x,
y = pointer.y,
isObjectInGroup = (
activeGroup &&
object.type !== 'group' &&
activeGroup.contains(object)),
lt;
if (isObjectInGroup) {
lt = fabric.util.transformPoint(activeGroup.getCenterPoint(), this.viewportTransform, true);
x -= lt.x;
y -= lt.y;
x /= activeGroup.scaleX;
y /= activeGroup.scaleY;
}
return { x: x, y: y };
},
/**
* 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;
target.hasBorders = target.transparentCorners = false;
target.render(this.contextCache);
target._renderControls(this.contextCache);
target.hasBorders = hasBorders;
target.transparentCorners = transparentCorners;
var isTransparent = fabric.util.isTransparent(
this.contextCache, x, y, this.targetFindTolerance);
this.clearContext(this.contextCache);
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.shiftKey)
||
(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.shiftKey ? 'skewY' : 'scaleX';
case 'mt':
case 'mb':
return e.shiftKey ? '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.altKey
};
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
*/
_translateObject: function (x, y) {
var target = this._currentTransform.target;
if (!target.get('lockMovementX')) {
target.set('left', x - this._currentTransform.offsetX);
}
if (!target.get('lockMovementY')) {
target.set('top', y - this._currentTransform.offsetY);
}
},
/**
* 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'
*/
_skewObject: function (x, y, by) {
var t = this._currentTransform,
target = t.target,
lockSkewingX = target.get('lockSkewingX'),
lockSkewingY = target.get('lockSkewingY');
if ((lockSkewingX && by === 'x') || (lockSkewingY && by === 'y')) {
return;
}
// 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
this._setObjectSkew(actualMouseByOrigin, t, by, dim);
t.lastX = x;
t.lastY = y;
// Make sure the constraints apply
target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
},
_setObjectSkew: function(localMouse, transform, by, _dim) {
var target = transform.target, newValue,
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);
}
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);
}
},
/**
* 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
*/
_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;
}
// 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();
this._setLocalMouse(localMouse, t);
// Actually scale the object
this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip, dim);
// Make sure the constraints apply
target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
},
/**
* @private
*/
_setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim) {
var target = transform.target, forbidScalingX = false, forbidScalingY = false;
transform.newScaleX = localMouse.x * target.scaleX / _dim.x;
transform.newScaleY = localMouse.y * target.scaleY / _dim.y;
if (lockScalingFlip && transform.newScaleX <= 0 && transform.newScaleX < target.scaleX) {
forbidScalingX = true;
}
if (lockScalingFlip && transform.newScaleY <= 0 && transform.newScaleY < target.scaleY) {
forbidScalingY = true;
}
if (by === 'equally' && !lockScalingX && !lockScalingY) {
forbidScalingX || forbidScalingY || this._scaleObjectEqually(localMouse, target, transform, _dim);
}
else if (!by) {
forbidScalingX || lockScalingX || target.set('scaleX', transform.newScaleX);
forbidScalingY || lockScalingY || target.set('scaleY', transform.newScaleY);
}
else if (by === 'x' && !target.get('lockUniScaling')) {
forbidScalingX || lockScalingX || target.set('scaleX', transform.newScaleX);
}
else if (by === 'y' && !target.get('lockUniScaling')) {
forbidScalingY || lockScalingY || target.set('scaleY', transform.newScaleY);
}
forbidScalingX || forbidScalingY || this._flipObject(transform, by);
},
/**
* @private
*/
_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;
// 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;
target.set('scaleX', transform.newScaleX);
target.set('scaleY', transform.newScaleY);
},
/**
* @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
*/
_rotateObject: function (x, y) {
var t = this._currentTransform;
if (t.target.get('lockRotation')) {
return;
}
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;
},
/**
* 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;
},
/**
* @private
*/
_resetObjectTransform: function (target) {
target.scaleX = 1;
target.scaleY = 1;
target.skewX = 0;
target.skewY = 0;
target.setAngle(0);
},
/**
* @private
*/
_drawSelection: function () {
var ctx = this.contextTop,
groupSelector = this._groupSelector,
left = groupSelector.left,
top = groupSelector.top,
aleft = abs(left),
atop = abs(top);
ctx.fillStyle = this.selectionColor;
ctx.fillRect(
groupSelector.ex - ((left > 0) ? 0 : -left),
groupSelector.ey - ((top > 0) ? 0 : -top),
aleft,
atop
);
ctx.lineWidth = this.selectionLineWidth;
ctx.strokeStyle = this.selectionBorderColor;
// selection border
if (this.selectionDashArray.length > 1) {
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 {
ctx.strokeRect(
groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft),
groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop),
aleft,
atop
);
}
},
/**
* @private
*/
_isLastRenderedObject: function(e) {
return (
this.controlsAboveOverlay &&
this.lastRenderedObjectWithControlsAboveOverlay &&
this.lastRenderedObjectWithControlsAboveOverlay.visible &&
this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) &&
this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(this.getPointer(e, true)));
},
/**
* Method that determines what object we are clicking on
* @param {Event} e mouse event
* @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through
*/
findTarget: function (e, skipGroup) {
if (this.skipTargetFind) {
return;
}
if (this._isLastRenderedObject(e)) {
return this.lastRenderedObjectWithControlsAboveOverlay;
}
// first check current group (if one exists)
var activeGroup = this.getActiveGroup();
if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) {
return activeGroup;
}
var target = this._searchPossibleTargets(e, skipGroup);
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(e, obj, pointer) {
if (obj &&
obj.visible &&
obj.evented &&
this.containsPoint(e, obj)){
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(e, skipGroup) {
// Cache all targets where their bounding box contains point.
var target,
pointer = this.getPointer(e, true),
i = this._objects.length;
// 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._objects[i].group || skipGroup) && this._checkTarget(e, this._objects[i], pointer)){
this.relatedTarget = this._objects[i];
target = this._objects[i];
break;
}
}
return target;
},
/**
* Returns pointer coordinates relative to canvas.
* @param {Event} e
* @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 = fabric.util.transformPoint(
pointer,
fabric.util.invertTransform(this.viewportTransform)
);
}
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 &lt;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
*/
_discardActiveObject: function() {
if (this._activeObject) {
this._activeObject.set('active', false);
}
this._activeObject = null;
},
/**
* Discards currently active object
* @return {fabric.Canvas} thisArg
* @chainable
*/
discardActiveObject: function (e) {
this._discardActiveObject();
this.renderAll();
this.fire('selection:cleared', { 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 speicified one
* @param {fabric.Group} group Group to set as a current one
* @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
* @return {fabric.Canvas} thisArg
*/
discardActiveGroup: function (e) {
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
*/
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
*/
deactivateAllWithDispatch: function (e) {
var activeObject = this.getActiveGroup() || this.getActiveObject();
if (activeObject) {
this.fire('before:selection:cleared', { target: activeObject, e: e });
}
this.deactivateAll();
if (activeObject) {
this.fire('selection:cleared', { e: e });
}
return this;
},
/**
* 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);
this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i];
}
}
});
// 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() { };
}
/**
* @class fabric.Element
* @alias fabric.Canvas
* @deprecated Use {@link fabric.Canvas} instead.
* @constructor
*/
fabric.Element = fabric.Canvas;
})();