mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-10 17:30:59 +00:00
1116 lines
30 KiB
JavaScript
1116 lines
30 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,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_initInteractive: function() {
|
|
this._currentTransform = null;
|
|
this._groupSelector = null;
|
|
this._initWrapperElement();
|
|
this._createUpperCanvas();
|
|
this._initEventListeners();
|
|
|
|
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(e) {
|
|
var t = this._currentTransform;
|
|
|
|
t.target.set({
|
|
'scaleX': t.original.scaleX,
|
|
'scaleY': t.original.scaleY,
|
|
'left': t.original.left,
|
|
'top': t.original.top
|
|
});
|
|
|
|
if (this._shouldCenterTransform(e, 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),
|
|
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(e, this._offset));
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_normalizePointer: function (object, pointer) {
|
|
var activeGroup = this.getActiveGroup(),
|
|
x = pointer.x,
|
|
y = pointer.y;
|
|
|
|
var isObjectInGroup = (
|
|
activeGroup &&
|
|
object.type !== 'group' &&
|
|
activeGroup.contains(object)
|
|
);
|
|
|
|
if (isObjectInGroup) {
|
|
x -= activeGroup.left;
|
|
y -= activeGroup.top;
|
|
}
|
|
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;
|
|
|
|
this._draw(this.contextCache, target);
|
|
|
|
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 {Event} e Event object
|
|
* @param {fabric.Object} target
|
|
*/
|
|
_shouldCenterTransform: function (e, 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 ? !e.altKey : e.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) {
|
|
var action = 'drag';
|
|
if (corner) {
|
|
action = (corner === 'ml' || corner === 'mr')
|
|
? 'scaleX'
|
|
: (corner === 'mt' || corner === 'mb')
|
|
? 'scaleY'
|
|
: corner === 'mtr'
|
|
? 'rotate'
|
|
: 'scale';
|
|
}
|
|
return action;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Event} e Event object
|
|
* @param {fabric.Object} target
|
|
*/
|
|
_setupCurrentTransform: function (e, target) {
|
|
if (!target) return;
|
|
|
|
var corner = target._findTargetCorner(e, this._offset),
|
|
pointer = getPointer(e, target.canvas.upperCanvasEl),
|
|
action = this._getActionFromCorner(target, corner),
|
|
origin = this._getOriginFromCorner(target, corner);
|
|
|
|
this._currentTransform = {
|
|
target: target,
|
|
action: action,
|
|
scaleX: target.scaleX,
|
|
scaleY: target.scaleY,
|
|
offsetX: pointer.x - target.left,
|
|
offsetY: pointer.y - target.top,
|
|
originX: origin.x,
|
|
originY: origin.y,
|
|
ex: pointer.x,
|
|
ey: pointer.y,
|
|
left: target.left,
|
|
top: target.top,
|
|
theta: degreesToRadians(target.angle),
|
|
width: target.width * target.scaleX,
|
|
mouseXSign: 1,
|
|
mouseYSign: 1
|
|
};
|
|
|
|
this._currentTransform.original = {
|
|
left: target.left,
|
|
top: target.top,
|
|
scaleX: target.scaleX,
|
|
scaleY: target.scaleY,
|
|
originX: origin.x,
|
|
originY: origin.y
|
|
};
|
|
|
|
this._resetCurrentTransform(e);
|
|
},
|
|
|
|
/**
|
|
* Translates object by "setting" its left/top
|
|
* @private
|
|
* @param x {Number} pointer's x coordinate
|
|
* @param y {Number} 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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Scales object by invoking its scaleX/scaleY methods
|
|
* @private
|
|
* @param x {Number} pointer's x coordinate
|
|
* @param y {Number} pointer's y coordinate
|
|
* @param by {String} 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,
|
|
offset = this._offset,
|
|
target = t.target,
|
|
lockScalingX = target.get('lockScalingX'),
|
|
lockScalingY = target.get('lockScalingY');
|
|
|
|
if (lockScalingX && lockScalingY) return;
|
|
|
|
// Get the constraint point
|
|
var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY);
|
|
var localMouse = target.toLocalPoint(new fabric.Point(x - offset.left, y - offset.top), t.originX, t.originY);
|
|
|
|
this._setLocalMouse(localMouse, t);
|
|
|
|
// Actually scale the object
|
|
this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by);
|
|
|
|
// Make sure the constraints apply
|
|
target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by) {
|
|
var target = transform.target;
|
|
|
|
transform.newScaleX = target.scaleX;
|
|
transform.newScaleY = target.scaleY;
|
|
|
|
if (by === 'equally' && !lockScalingX && !lockScalingY) {
|
|
this._scaleObjectEqually(localMouse, target, transform);
|
|
}
|
|
else if (!by) {
|
|
transform.newScaleX = localMouse.x / (target.width + target.strokeWidth);
|
|
transform.newScaleY = localMouse.y / (target.height + target.strokeWidth);
|
|
|
|
lockScalingX || target.set('scaleX', transform.newScaleX);
|
|
lockScalingY || target.set('scaleY', transform.newScaleY);
|
|
}
|
|
else if (by === 'x' && !target.get('lockUniScaling')) {
|
|
transform.newScaleX = localMouse.x / (target.width + target.strokeWidth);
|
|
lockScalingX || target.set('scaleX', transform.newScaleX);
|
|
}
|
|
else if (by === 'y' && !target.get('lockUniScaling')) {
|
|
transform.newScaleY = localMouse.y / (target.height + target.strokeWidth);
|
|
lockScalingY || target.set('scaleY', transform.newScaleY);
|
|
}
|
|
|
|
this._flipObject(transform);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_scaleObjectEqually: function(localMouse, target, transform) {
|
|
|
|
var dist = localMouse.y + localMouse.x;
|
|
|
|
var lastDist = (target.height + (target.strokeWidth)) * transform.original.scaleY +
|
|
(target.width + (target.strokeWidth)) * transform.original.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) {
|
|
if (transform.newScaleX < 0) {
|
|
if (transform.originX === 'left') {
|
|
transform.originX = 'right';
|
|
}
|
|
else if (transform.originX === 'right') {
|
|
transform.originX = 'left';
|
|
}
|
|
}
|
|
|
|
if (transform.newScaleY < 0) {
|
|
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 x {Number} pointer's x coordinate
|
|
* @param y {Number} pointer's y coordinate
|
|
*/
|
|
_rotateObject: function (x, y) {
|
|
|
|
var t = this._currentTransform,
|
|
o = this._offset;
|
|
|
|
if (t.target.get('lockRotation')) return;
|
|
|
|
var lastAngle = atan2(t.ey - t.top - o.top, t.ex - t.left - o.left),
|
|
curAngle = atan2(y - t.top - o.top, x - t.left - o.left),
|
|
angle = radiansToDegrees(curAngle - lastAngle + t.theta);
|
|
|
|
// normalize angle to positive value
|
|
if (angle < 0) {
|
|
angle = 360 + angle;
|
|
}
|
|
|
|
t.target.angle = angle;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setCursor: function (value) {
|
|
this.upperCanvasEl.style.cursor = value;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_resetObjectTransform: function (target) {
|
|
target.scaleX = 1;
|
|
target.scaleY = 1;
|
|
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);
|
|
var 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(e, this._offset));
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
this._fireOverOutEvents(target);
|
|
return target;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_fireOverOutEvents: function(target) {
|
|
if (target) {
|
|
if (this._hoveredTarget !== target) {
|
|
this.fire('mouse:over', { target: target });
|
|
target.fire('mouseover');
|
|
if (this._hoveredTarget) {
|
|
this.fire('mouse:out', { target: this._hoveredTarget });
|
|
this._hoveredTarget.fire('mouseout');
|
|
}
|
|
this._hoveredTarget = target;
|
|
}
|
|
}
|
|
else if (this._hoveredTarget) {
|
|
this.fire('mouse:out', { target: this._hoveredTarget });
|
|
this._hoveredTarget.fire('mouseout');
|
|
this._hoveredTarget = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_searchPossibleTargets: function(e) {
|
|
|
|
// Cache all targets where their bounding box contains point.
|
|
var possibleTargets = [],
|
|
target,
|
|
pointer = this.getPointer(e);
|
|
|
|
for (var i = this._objects.length; i--; ) {
|
|
if (this._objects[i] &&
|
|
this._objects[i].visible &&
|
|
this._objects[i].evented &&
|
|
this.containsPoint(e, this._objects[i])) {
|
|
|
|
if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) {
|
|
possibleTargets[possibleTargets.length] = this._objects[i];
|
|
}
|
|
else {
|
|
target = this._objects[i];
|
|
this.relatedTarget = target;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var j = 0, len = possibleTargets.length; j < len; j++) {
|
|
pointer = this.getPointer(e);
|
|
var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y);
|
|
if (!isTransparent) {
|
|
target = possibleTargets[j];
|
|
this.relatedTarget = target;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return target;
|
|
},
|
|
|
|
/**
|
|
* Returns pointer coordinates relative to canvas.
|
|
* @param {Event} e
|
|
* @return {Object} object with "x" and "y" number values
|
|
*/
|
|
getPointer: function (e) {
|
|
var pointer = getPointer(e, this.upperCanvasEl);
|
|
return {
|
|
x: pointer.x - this._offset.left,
|
|
y: pointer.y - this._offset.top
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {HTMLElement|String} canvasEl Canvas element
|
|
* @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
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
_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 {Element} element
|
|
*/
|
|
_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
|
|
*/
|
|
_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.canvas = this;
|
|
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) {
|
|
this._drawGroupControls(ctx, activeGroup);
|
|
}
|
|
else {
|
|
this._drawObjectsControls(ctx);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_drawGroupControls: function(ctx, activeGroup) {
|
|
this._drawControls(ctx, activeGroup, 'Group');
|
|
},
|
|
|
|
/**
|
|
* @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._drawControls(ctx, this._objects[i], 'Object');
|
|
this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_drawControls: function(ctx, object, klass) {
|
|
ctx.save();
|
|
fabric[klass].prototype.transform.call(object, ctx);
|
|
object.drawBorders(ctx).drawControls(ctx);
|
|
ctx.restore();
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
})();
|