mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-05-09 14:24:44 +00:00
1096 lines
No EOL
32 KiB
JavaScript
1096 lines
No EOL
32 KiB
JavaScript
(function() {
|
|
|
|
var extend = fabric.util.object.extend,
|
|
getPointer = fabric.util.getPointer,
|
|
addListener = fabric.util.addListener,
|
|
removeListener = fabric.util.removeListener,
|
|
cursorMap = {
|
|
'tr': 'ne-resize',
|
|
'br': 'se-resize',
|
|
'bl': 'sw-resize',
|
|
'tl': 'nw-resize',
|
|
'ml': 'w-resize',
|
|
'mt': 'n-resize',
|
|
'mr': 'e-resize',
|
|
'mb': 's-resize'
|
|
},
|
|
|
|
utilMin = fabric.util.array.min,
|
|
utilMax = fabric.util.array.max,
|
|
|
|
sqrt = Math.sqrt,
|
|
pow = Math.pow,
|
|
atan2 = Math.atan2,
|
|
abs = Math.abs,
|
|
min = Math.min,
|
|
max = Math.max,
|
|
|
|
STROKE_OFFSET = 0.5;
|
|
|
|
/**
|
|
* @class fabric.Canvas
|
|
* @constructor
|
|
* @extends fabric.StaticCanvas
|
|
* @param {HTMLElement | String} el <canvas> element to initialize instance on
|
|
* @param {Object} [options] Options object
|
|
*/
|
|
fabric.Canvas = function(el, options) {
|
|
options || (options = { });
|
|
|
|
this._initStatic(el, options);
|
|
this._initInteractive();
|
|
|
|
fabric.Canvas.activeInstance = this;
|
|
};
|
|
|
|
function ProtoProxy(){ }
|
|
ProtoProxy.prototype = fabric.StaticCanvas.prototype;
|
|
fabric.Canvas.prototype = new ProtoProxy;
|
|
|
|
var InteractiveMethods = /** @scope fabric.Canvas.prototype */ {
|
|
|
|
/**
|
|
* Indicates that canvas is interactive. This property should not be changed.
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
interactive: true,
|
|
|
|
/**
|
|
* Indicates whether group selection should be enabled
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
selection: true,
|
|
|
|
/**
|
|
* Color of selection
|
|
* @property
|
|
* @type String
|
|
*/
|
|
selectionColor: 'rgba(100, 100, 255, 0.3)', // blue
|
|
|
|
/**
|
|
* Color of the border of selection (usually slightly darker than color of selection itself)
|
|
* @property
|
|
* @type String
|
|
*/
|
|
selectionBorderColor: 'rgba(255, 255, 255, 0.3)',
|
|
|
|
/**
|
|
* Width of a line used in object/group selection
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
selectionLineWidth: 1,
|
|
|
|
/**
|
|
* Color of the line used in free drawing mode
|
|
* @property
|
|
* @type String
|
|
*/
|
|
freeDrawingColor: 'rgb(0, 0, 0)',
|
|
|
|
/**
|
|
* Width of a line used in free drawing mode
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
freeDrawingLineWidth: 1,
|
|
|
|
/**
|
|
* Default cursor value used when hovering over an object on canvas
|
|
* @constant
|
|
* @type String
|
|
*/
|
|
HOVER_CURSOR: 'move',
|
|
|
|
/**
|
|
* Default cursor value used for the entire canvas
|
|
* @constant
|
|
* @type String
|
|
*/
|
|
CURSOR: 'default',
|
|
|
|
/**
|
|
* Default element class that's given to wrapper (div) element of canvas
|
|
* @constant
|
|
* @type String
|
|
*/
|
|
CONTAINER_CLASS: 'canvas-container',
|
|
|
|
_initInteractive: function() {
|
|
this._currentTransform = null;
|
|
this._groupSelector = null;
|
|
this._freeDrawingXPoints = [ ];
|
|
this._freeDrawingYPoints = [ ];
|
|
this._initWrapperElement();
|
|
this._createUpperCanvas();
|
|
this._initEvents();
|
|
this.calcOffset();
|
|
},
|
|
|
|
/**
|
|
* Adds mouse listeners to canvas
|
|
* @method _initEvents
|
|
* @private
|
|
* See configuration documentation for more details.
|
|
*/
|
|
_initEvents: function () {
|
|
var _this = this;
|
|
|
|
this._onMouseDown = function (e) {
|
|
_this.__onMouseDown(e);
|
|
|
|
addListener(fabric.document, 'mouseup', _this._onMouseUp);
|
|
fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp);
|
|
|
|
addListener(fabric.document, 'mousemove', _this._onMouseMove);
|
|
fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove);
|
|
|
|
removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove);
|
|
fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove);
|
|
};
|
|
|
|
this._onMouseUp = function (e) {
|
|
_this.__onMouseUp(e);
|
|
|
|
removeListener(fabric.document, 'mouseup', _this._onMouseUp);
|
|
fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp);
|
|
|
|
removeListener(fabric.document, 'mousemove', _this._onMouseMove);
|
|
fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove);
|
|
|
|
addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove);
|
|
fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove);
|
|
};
|
|
|
|
this._onMouseMove = function (e) {
|
|
e.preventDefault && e.preventDefault();
|
|
_this.__onMouseMove(e);
|
|
};
|
|
|
|
this._onResize = function (e) {
|
|
_this.calcOffset();
|
|
};
|
|
|
|
|
|
addListener(fabric.window, 'resize', this._onResize);
|
|
|
|
if (fabric.isTouchSupported) {
|
|
addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
|
|
addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
|
|
}
|
|
else {
|
|
addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
|
|
addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Method that defines the actions when mouse is released on canvas.
|
|
* The method resets the currentTransform parameters, store the image corner
|
|
* position in the image object and render the canvas on top.
|
|
* @method __onMouseUp
|
|
* @param {Event} e Event object fired on mouseup
|
|
*
|
|
*/
|
|
__onMouseUp: function (e) {
|
|
|
|
if (this.isDrawingMode && this._isCurrentlyDrawing) {
|
|
this._finalizeDrawingPath();
|
|
return;
|
|
}
|
|
|
|
if (this._currentTransform) {
|
|
|
|
var transform = this._currentTransform,
|
|
target = transform.target;
|
|
|
|
if (target._scaling) {
|
|
target._scaling = false;
|
|
}
|
|
|
|
// determine the new coords everytime the image changes its position
|
|
var i = this._objects.length;
|
|
while (i--) {
|
|
this._objects[i].setCoords();
|
|
}
|
|
|
|
// only fire :modified event if target coordinates were changed during mousedown-mouseup
|
|
if (this.stateful && target.hasStateChanged()) {
|
|
target.isMoving = false;
|
|
this.fire('object:modified', { target: target });
|
|
}
|
|
}
|
|
|
|
this._currentTransform = null;
|
|
|
|
if (this._groupSelector) {
|
|
// group selection was completed, determine its bounds
|
|
this._findSelectedObjects(e);
|
|
}
|
|
var activeGroup = this.getActiveGroup();
|
|
if (activeGroup) {
|
|
activeGroup.setObjectsCoords();
|
|
activeGroup.set('isMoving', false);
|
|
this._setCursor(this.CURSOR);
|
|
}
|
|
|
|
// clear selection
|
|
this._groupSelector = null;
|
|
this.renderAll();
|
|
|
|
this._setCursorFromEvent(e, target);
|
|
|
|
// fix for FF
|
|
this._setCursor('');
|
|
|
|
var _this = this;
|
|
setTimeout(function () {
|
|
_this._setCursorFromEvent(e, target);
|
|
}, 50);
|
|
|
|
this.fire('mouse:up', { target: target, e: e });
|
|
},
|
|
|
|
/**
|
|
* Method that defines the actions when mouse is clic ked on canvas.
|
|
* The method inits the currentTransform parameters and renders all the
|
|
* canvas so the current image can be placed on the top canvas and the rest
|
|
* in on the container one.
|
|
* @method __onMouseDown
|
|
* @param e {Event} Event object fired on mousedown
|
|
*
|
|
*/
|
|
__onMouseDown: function (e) {
|
|
|
|
// accept only left clicks
|
|
var isLeftClick = 'which' in e ? e.which == 1 : e.button == 1;
|
|
if (!isLeftClick && !fabric.isTouchSupported) return;
|
|
|
|
if (this.isDrawingMode) {
|
|
this._prepareForDrawing(e);
|
|
|
|
// capture coordinates immediately; this allows to draw dots (when movement never occurs)
|
|
this._captureDrawingPath(e);
|
|
|
|
return;
|
|
}
|
|
|
|
// ignore if some object is being transformed at this moment
|
|
if (this._currentTransform) return;
|
|
|
|
var target = this.findTarget(e),
|
|
pointer = this.getPointer(e),
|
|
activeGroup = this.getActiveGroup(),
|
|
corner;
|
|
|
|
if (this._shouldClearSelection(e)) {
|
|
|
|
this._groupSelector = {
|
|
ex: pointer.x,
|
|
ey: pointer.y,
|
|
top: 0,
|
|
left: 0
|
|
};
|
|
|
|
this.deactivateAllWithDispatch();
|
|
}
|
|
else {
|
|
// determine if it's a drag or rotate case
|
|
// rotate and scale will happen at the same time
|
|
this.stateful && target.saveState();
|
|
|
|
if (corner = target._findTargetCorner(e, this._offset)) {
|
|
this.onBeforeScaleRotate(target);
|
|
}
|
|
|
|
this._setupCurrentTransform(e, target);
|
|
|
|
var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject());
|
|
if (shouldHandleGroupLogic) {
|
|
this._handleGroupLogic(e, target);
|
|
}
|
|
else {
|
|
if (target !== this.getActiveGroup()) {
|
|
this.deactivateAll();
|
|
}
|
|
this.setActiveObject(target, e);
|
|
}
|
|
}
|
|
// we must renderAll so that active image is placed on the top canvas
|
|
this.renderAll();
|
|
|
|
this.fire('mouse:down', { target: target, e: e });
|
|
},
|
|
|
|
/**
|
|
* Method that defines the actions when mouse is hovering the canvas.
|
|
* The currentTransform parameter will definde whether the user is rotating/scaling/translating
|
|
* an image or neither of them (only hovering). A group selection is also possible and would cancel
|
|
* all any other type of action.
|
|
* In case of an image transformation only the top canvas will be rendered.
|
|
* @method __onMouseMove
|
|
* @param e {Event} Event object fired on mousemove
|
|
*
|
|
*/
|
|
__onMouseMove: function (e) {
|
|
|
|
if (this.isDrawingMode) {
|
|
if (this._isCurrentlyDrawing) {
|
|
this._captureDrawingPath(e);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var groupSelector = this._groupSelector;
|
|
|
|
// We initially clicked in an empty area, so we draw a box for multiple selection.
|
|
if (groupSelector !== null) {
|
|
var pointer = getPointer(e);
|
|
groupSelector.left = pointer.x - this._offset.left - groupSelector.ex;
|
|
groupSelector.top = pointer.y - this._offset.top - groupSelector.ey;
|
|
this.renderTop();
|
|
}
|
|
else if (!this._currentTransform) {
|
|
|
|
// alias style to elimintate unnecessary lookup
|
|
var style = this.upperCanvasEl.style;
|
|
|
|
// Here we are hovering the canvas then we will determine
|
|
// what part of the pictures we are hovering to change the caret symbol.
|
|
// We won't do that while dragging or rotating in order to improve the
|
|
// performance.
|
|
var target = this.findTarget(e);
|
|
|
|
if (!target) {
|
|
// image/text was hovered-out from, we remove its borders
|
|
for (var i = this._objects.length; i--; ) {
|
|
if (this._objects[i] && !this._objects[i].active) {
|
|
this._objects[i].setActive(false);
|
|
}
|
|
}
|
|
style.cursor = this.CURSOR;
|
|
}
|
|
else {
|
|
// set proper cursor
|
|
this._setCursorFromEvent(e, target);
|
|
if (target.isActive()) {
|
|
// display corners when hovering over an image
|
|
target.setCornersVisibility && target.setCornersVisibility(true);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// object is being transformed (scaled/rotated/moved/etc.)
|
|
var pointer = getPointer(e),
|
|
x = pointer.x,
|
|
y = pointer.y;
|
|
|
|
this._currentTransform.target.isMoving = true;
|
|
|
|
if (this._currentTransform.action === 'rotate') {
|
|
// rotate object only if shift key is not pressed
|
|
// and if it is not a group we are transforming
|
|
|
|
if (!e.shiftKey) {
|
|
this._rotateObject(x, y);
|
|
|
|
this.fire('object:rotating', {
|
|
target: this._currentTransform.target
|
|
});
|
|
}
|
|
|
|
this._scaleObject(x, y);
|
|
this.fire('object:scaling', {
|
|
target: this._currentTransform.target
|
|
});
|
|
}
|
|
else if (this._currentTransform.action === 'scaleX') {
|
|
this._scaleObject(x, y, 'x');
|
|
|
|
this.fire('object:scaling', {
|
|
target: this._currentTransform.target
|
|
});
|
|
}
|
|
else if (this._currentTransform.action === 'scaleY') {
|
|
this._scaleObject(x, y, 'y');
|
|
|
|
this.fire('object:scaling', {
|
|
target: this._currentTransform.target
|
|
});
|
|
}
|
|
else {
|
|
this._translateObject(x, y);
|
|
|
|
this.fire('object:moving', {
|
|
target: this._currentTransform.target
|
|
});
|
|
}
|
|
// only commit here. when we are actually moving the pictures
|
|
this.renderAll();
|
|
}
|
|
this.fire('mouse:move', { target: target, e: e });
|
|
},
|
|
|
|
/**
|
|
* Applies one implementation of 'point inside polygon' algorithm
|
|
* @method containsPoint
|
|
* @param e { Event } event object
|
|
* @param target { fabric.Object } object to test against
|
|
* @return {Boolean} true if point contains within area of given object
|
|
*/
|
|
containsPoint: function (e, target) {
|
|
var pointer = this.getPointer(e),
|
|
xy = this._normalizePointer(target, pointer),
|
|
x = xy.x,
|
|
y = xy.y;
|
|
|
|
// http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html
|
|
// http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
|
|
|
|
// we iterate through each object. If target found, return it.
|
|
var iLines = target._getImageLines(target.oCoords),
|
|
xpoints = target._findCrossPoints(x, y, iLines);
|
|
|
|
// if xcount is odd then we clicked inside the object
|
|
// For the specific case of square images xcount === 1 in all true cases
|
|
if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _normalizePointer
|
|
*/
|
|
_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 };
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _shouldClearSelection
|
|
*/
|
|
_shouldClearSelection: function (e) {
|
|
var target = this.findTarget(e),
|
|
activeGroup = this.getActiveGroup();
|
|
return (
|
|
!target || (
|
|
target &&
|
|
activeGroup &&
|
|
!activeGroup.contains(target) &&
|
|
activeGroup !== target &&
|
|
!e.shiftKey
|
|
)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _setupCurrentTransform
|
|
*/
|
|
_setupCurrentTransform: function (e, target) {
|
|
var action = 'drag',
|
|
corner,
|
|
pointer = getPointer(e);
|
|
|
|
if (corner = target._findTargetCorner(e, this._offset)) {
|
|
action = (corner === 'ml' || corner === 'mr')
|
|
? 'scaleX'
|
|
: (corner === 'mt' || corner === 'mb')
|
|
? 'scaleY'
|
|
: 'rotate';
|
|
}
|
|
|
|
this._currentTransform = {
|
|
target: target,
|
|
action: action,
|
|
scaleX: target.scaleX,
|
|
scaleY: target.scaleY,
|
|
offsetX: pointer.x - target.left,
|
|
offsetY: pointer.y - target.top,
|
|
ex: pointer.x,
|
|
ey: pointer.y,
|
|
left: target.left,
|
|
top: target.top,
|
|
theta: target.theta,
|
|
width: target.width * target.scaleX
|
|
};
|
|
|
|
this._currentTransform.original = {
|
|
left: target.left,
|
|
top: target.top
|
|
};
|
|
},
|
|
|
|
_handleGroupLogic: function (e, target) {
|
|
if (target.isType('group')) {
|
|
// if it's a group, find target again, this time skipping group
|
|
target = this.findTarget(e, true);
|
|
// if even object is not found, bail out
|
|
if (!target || target.isType('group')) {
|
|
return;
|
|
}
|
|
}
|
|
var activeGroup = this.getActiveGroup();
|
|
if (activeGroup) {
|
|
if (activeGroup.contains(target)) {
|
|
activeGroup.remove(target);
|
|
target.setActive(false);
|
|
if (activeGroup.size() === 1) {
|
|
// remove group alltogether if after removal it only contains 1 object
|
|
this.discardActiveGroup();
|
|
}
|
|
}
|
|
else {
|
|
activeGroup.add(target);
|
|
}
|
|
this.fire('selection:created', { target: activeGroup, e: e });
|
|
activeGroup.setActive(true);
|
|
}
|
|
else {
|
|
// group does not exist
|
|
if (this._activeObject) {
|
|
// only if there's an active object
|
|
if (target !== this._activeObject) {
|
|
// and that object is not the actual target
|
|
var group = new fabric.Group([ this._activeObject,target ]);
|
|
this.setActiveGroup(group);
|
|
activeGroup = this.getActiveGroup();
|
|
}
|
|
}
|
|
// activate target object in any case
|
|
target.setActive(true);
|
|
}
|
|
|
|
if (activeGroup) {
|
|
activeGroup.saveCoords();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _prepareForDrawing
|
|
*/
|
|
_prepareForDrawing: function(e) {
|
|
|
|
this._isCurrentlyDrawing = true;
|
|
|
|
this.discardActiveObject().renderAll();
|
|
|
|
var pointer = this.getPointer(e);
|
|
|
|
this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0;
|
|
|
|
this._freeDrawingXPoints.push(pointer.x);
|
|
this._freeDrawingYPoints.push(pointer.y);
|
|
|
|
this.contextTop.beginPath();
|
|
this.contextTop.moveTo(pointer.x, pointer.y);
|
|
this.contextTop.strokeStyle = this.freeDrawingColor;
|
|
this.contextTop.lineWidth = this.freeDrawingLineWidth;
|
|
this.contextTop.lineCap = this.contextTop.lineJoin = 'round';
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _captureDrawingPath
|
|
*/
|
|
_captureDrawingPath: function(e) {
|
|
var pointer = this.getPointer(e);
|
|
|
|
this._freeDrawingXPoints.push(pointer.x);
|
|
this._freeDrawingYPoints.push(pointer.y);
|
|
|
|
this.contextTop.lineTo(pointer.x, pointer.y);
|
|
this.contextTop.stroke();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _finalizeDrawingPath
|
|
*/
|
|
_finalizeDrawingPath: function() {
|
|
|
|
this.contextTop.closePath();
|
|
|
|
this._isCurrentlyDrawing = false;
|
|
|
|
var minX = utilMin(this._freeDrawingXPoints),
|
|
minY = utilMin(this._freeDrawingYPoints),
|
|
maxX = utilMax(this._freeDrawingXPoints),
|
|
maxY = utilMax(this._freeDrawingYPoints),
|
|
ctx = this.contextTop,
|
|
path = [ ],
|
|
xPoint,
|
|
yPoint,
|
|
xPoints = this._freeDrawingXPoints,
|
|
yPoints = this._freeDrawingYPoints;
|
|
|
|
path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' ');
|
|
|
|
for (var i = 1; xPoint = xPoints[i], yPoint = yPoints[i]; i++) {
|
|
path.push('L ', xPoint - minX, ' ', yPoint - minY, ' ');
|
|
}
|
|
|
|
// TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path,
|
|
// and instead fire something like "drawing:completed" event with path string
|
|
|
|
path = path.join('');
|
|
|
|
if (path === "M 0 0 L 0 0 ") {
|
|
// do not create 0 width/height paths, as they are rendered inconsistently across browsers
|
|
// Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing
|
|
return;
|
|
}
|
|
|
|
var p = new fabric.Path(path);
|
|
|
|
p.fill = null;
|
|
p.stroke = this.freeDrawingColor;
|
|
p.strokeWidth = this.freeDrawingLineWidth;
|
|
this.add(p);
|
|
p.set("left", minX + (maxX - minX) / 2).set("top", minY + (maxY - minY) / 2).setCoords();
|
|
this.renderAll();
|
|
this.fire('path:created', { path: p });
|
|
},
|
|
|
|
/**
|
|
* Translates object by "setting" its left/top
|
|
* @method _translateObject
|
|
* @param x {Number} pointer's x coordinate
|
|
* @param y {Number} pointer's y coordinate
|
|
*/
|
|
_translateObject: function (x, y) {
|
|
var target = this._currentTransform.target;
|
|
target.lockMovementX || target.set('left', x - this._currentTransform.offsetX);
|
|
target.lockMovementY || target.set('top', y - this._currentTransform.offsetY);
|
|
},
|
|
|
|
/**
|
|
* Scales object by invoking its scaleX/scaleY methods
|
|
* @method _scaleObject
|
|
* @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;
|
|
|
|
if (target.lockScalingX && target.lockScalingY) return;
|
|
|
|
var lastLen = sqrt(pow(t.ey - t.top - offset.top, 2) + pow(t.ex - t.left - offset.left, 2)),
|
|
curLen = sqrt(pow(y - t.top - offset.top, 2) + pow(x - t.left - offset.left, 2));
|
|
|
|
target._scaling = true;
|
|
|
|
if (!by) {
|
|
target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen);
|
|
target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen);
|
|
}
|
|
else if (by === 'x' && !target.lockUniScaling) {
|
|
target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen);
|
|
}
|
|
else if (by === 'y' && !target.lockUniScaling) {
|
|
target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Rotates object by invoking its rotate method
|
|
* @method _rotateObject
|
|
* @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.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);
|
|
|
|
t.target.set('theta', (curAngle - lastAngle) + t.theta);
|
|
},
|
|
|
|
/**
|
|
* @method _setCursor
|
|
*/
|
|
_setCursor: function (value) {
|
|
this.upperCanvasEl.style.cursor = value;
|
|
},
|
|
|
|
/**
|
|
* Sets the cursor depending on where the canvas is being hovered.
|
|
* Note: very buggy in Opera
|
|
* @method _setCursorFromEvent
|
|
* @param e {Event} Event object
|
|
* @param target {Object} Object that the mouse is hovering, if so.
|
|
*/
|
|
_setCursorFromEvent: function (e, target) {
|
|
var s = this.upperCanvasEl.style;
|
|
if (!target) {
|
|
s.cursor = this.CURSOR;
|
|
return false;
|
|
}
|
|
else {
|
|
var activeGroup = this.getActiveGroup();
|
|
// only show proper corner when group selection is not active
|
|
var corner = !!target._findTargetCorner
|
|
&& (!activeGroup || !activeGroup.contains(target))
|
|
&& target._findTargetCorner(e, this._offset);
|
|
|
|
if (!corner) {
|
|
s.cursor = this.HOVER_CURSOR;
|
|
}
|
|
else {
|
|
if (corner in cursorMap) {
|
|
s.cursor = cursorMap[corner];
|
|
}
|
|
else {
|
|
s.cursor = this.CURSOR;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* @method _drawSelection
|
|
* @private
|
|
*/
|
|
_drawSelection: function () {
|
|
var groupSelector = this._groupSelector,
|
|
left = groupSelector.left,
|
|
top = groupSelector.top,
|
|
aleft = abs(left),
|
|
atop = abs(top);
|
|
|
|
this.contextTop.fillStyle = this.selectionColor;
|
|
|
|
this.contextTop.fillRect(
|
|
groupSelector.ex - ((left > 0) ? 0 : -left),
|
|
groupSelector.ey - ((top > 0) ? 0 : -top),
|
|
aleft,
|
|
atop
|
|
);
|
|
|
|
this.contextTop.lineWidth = this.selectionLineWidth;
|
|
this.contextTop.strokeStyle = this.selectionBorderColor;
|
|
|
|
this.contextTop.strokeRect(
|
|
groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft),
|
|
groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop),
|
|
aleft,
|
|
atop
|
|
);
|
|
},
|
|
|
|
_findSelectedObjects: function (e) {
|
|
var target,
|
|
targetRegion,
|
|
group = [ ],
|
|
x1 = this._groupSelector.ex,
|
|
y1 = this._groupSelector.ey,
|
|
x2 = x1 + this._groupSelector.left,
|
|
y2 = y1 + this._groupSelector.top,
|
|
currentObject,
|
|
selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)),
|
|
selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2));
|
|
|
|
for (var i = 0, len = this._objects.length; i < len; ++i) {
|
|
currentObject = this._objects[i];
|
|
|
|
if (!currentObject) continue;
|
|
|
|
if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) ||
|
|
currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) {
|
|
|
|
if (this.selection && currentObject.selectable) {
|
|
currentObject.setActive(true);
|
|
group.push(currentObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
// do not create group for 1 element only
|
|
if (group.length === 1) {
|
|
this.setActiveObject(group[0], e);
|
|
}
|
|
else if (group.length > 1) {
|
|
var group = new fabric.Group(group);
|
|
this.setActiveGroup(group);
|
|
group.saveCoords();
|
|
this.fire('selection:created', { target: group });
|
|
}
|
|
|
|
this.renderAll();
|
|
},
|
|
|
|
/**
|
|
* Method that determines what object we are clicking on
|
|
* @method findTarget
|
|
* @param {Event} e mouse event
|
|
* @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through
|
|
*/
|
|
findTarget: function (e, skipGroup) {
|
|
|
|
var target,
|
|
pointer = this.getPointer(e);
|
|
|
|
// first check current group (if one exists)
|
|
var activeGroup = this.getActiveGroup();
|
|
|
|
if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) {
|
|
target = activeGroup;
|
|
return target;
|
|
}
|
|
|
|
// then check all of the objects on canvas
|
|
for (var i = this._objects.length; i--; ) {
|
|
if (this._objects[i] && this.containsPoint(e, this._objects[i])) {
|
|
target = this._objects[i];
|
|
this.relatedTarget = target;
|
|
break;
|
|
}
|
|
}
|
|
if (target && target.selectable) {
|
|
return target;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns pointer coordinates relative to canvas.
|
|
* @method getPointer
|
|
* @return {Object} object with "x" and "y" number values
|
|
*/
|
|
getPointer: function (e) {
|
|
var pointer = getPointer(e);
|
|
return {
|
|
x: pointer.x - this._offset.left,
|
|
y: pointer.y - this._offset.top
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @method _createUpperCanvas
|
|
* @param {HTMLElement|String} canvasEl Canvas element
|
|
* @throws {CANVAS_INIT_ERROR} If canvas can not be initialized
|
|
*/
|
|
_createUpperCanvas: function () {
|
|
this.upperCanvasEl = this._createCanvasElement();
|
|
this.upperCanvasEl.className = 'upper-canvas';
|
|
|
|
this.wrapperEl.appendChild(this.upperCanvasEl);
|
|
|
|
this._applyCanvasStyle(this.upperCanvasEl);
|
|
this.contextTop = this.upperCanvasEl.getContext('2d');
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _initWrapperElement
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
_initWrapperElement: function () {
|
|
this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', {
|
|
'class': this.CONTAINER_CLASS
|
|
});
|
|
fabric.util.setStyle(this.wrapperEl, {
|
|
width: this.getWidth() + 'px',
|
|
height: this.getHeight() + 'px',
|
|
position: 'relative'
|
|
});
|
|
fabric.util.makeElementUnselectable(this.wrapperEl);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _applyCanvasStyle
|
|
* @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);
|
|
},
|
|
|
|
/**
|
|
* Returns topmost canvas context
|
|
* @method getContext
|
|
* @return {CanvasRenderingContext2D}
|
|
*/
|
|
getContext: function () {
|
|
return this.contextTop;
|
|
},
|
|
|
|
/**
|
|
* Sets given object as active
|
|
* @method setActiveObject
|
|
* @param object {fabric.Object} Object to set as an active one
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
setActiveObject: function (object, e) {
|
|
if (this._activeObject) {
|
|
this._activeObject.setActive(false);
|
|
}
|
|
this._activeObject = object;
|
|
object.setActive(true);
|
|
|
|
this.renderAll();
|
|
|
|
this.fire('object:selected', { target: object, e: e });
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns currently active object
|
|
* @method getActiveObject
|
|
* @return {fabric.Object} active object
|
|
*/
|
|
getActiveObject: function () {
|
|
return this._activeObject;
|
|
},
|
|
|
|
/**
|
|
* Discards currently active object
|
|
* @method discardActiveObject
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
discardActiveObject: function () {
|
|
if (this._activeObject) {
|
|
this._activeObject.setActive(false);
|
|
}
|
|
this._activeObject = null;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets active group to a speicified one
|
|
* @method setActiveGroup
|
|
* @param {fabric.Group} group Group to set as a current one
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
setActiveGroup: function (group) {
|
|
this._activeGroup = group;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns currently active group
|
|
* @method getActiveGroup
|
|
* @return {fabric.Group} Current group
|
|
*/
|
|
getActiveGroup: function () {
|
|
return this._activeGroup;
|
|
},
|
|
|
|
/**
|
|
* Removes currently active group
|
|
* @method discardActiveGroup
|
|
* @return {fabric.Canvas} thisArg
|
|
*/
|
|
discardActiveGroup: function () {
|
|
var g = this.getActiveGroup();
|
|
if (g) {
|
|
g.destroy();
|
|
}
|
|
return this.setActiveGroup(null);
|
|
},
|
|
|
|
/**
|
|
* Deactivates all objects by calling their setActive(false)
|
|
* @method deactivateAll
|
|
* @return {fabric.Canvas} thisArg
|
|
*/
|
|
deactivateAll: function () {
|
|
var allObjects = this.getObjects(),
|
|
i = 0,
|
|
len = allObjects.length;
|
|
for ( ; i < len; i++) {
|
|
allObjects[i].setActive(false);
|
|
}
|
|
this.discardActiveGroup();
|
|
this.discardActiveObject();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Deactivates all objects and dispatches appropriate events
|
|
* @method deactivateAllWithDispatch
|
|
* @return {fabric.Canvas} thisArg
|
|
*/
|
|
deactivateAllWithDispatch: function () {
|
|
var activeObject = this.getActiveGroup() || this.getActiveObject();
|
|
if (activeObject) {
|
|
this.fire('before:selection:cleared', { target: activeObject });
|
|
}
|
|
this.deactivateAll();
|
|
if (activeObject) {
|
|
this.fire('selection:cleared');
|
|
}
|
|
return this;
|
|
}
|
|
};
|
|
|
|
fabric.Canvas.prototype.toString = fabric.StaticCanvas.prototype.toString;
|
|
extend(fabric.Canvas.prototype, InteractiveMethods);
|
|
|
|
// iterating manually to workaround 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) {
|
|
fabric.Canvas.prototype._setCursorFromEvent = function() { };
|
|
}
|
|
|
|
/**
|
|
* @class fabric.Element
|
|
* @alias fabric.Canvas
|
|
* @deprecated
|
|
* @constructor
|
|
*/
|
|
fabric.Element = fabric.Canvas;
|
|
})(); |