fabric.js/src/mixins/object_interactivity.mixin.js
2015-09-17 14:22:12 +02:00

401 lines
12 KiB
JavaScript

(function() {
var degreesToRadians = fabric.util.degreesToRadians,
//jscs:disable requireCamelCaseOrUpperCaseIdentifiers
isVML = function() { return typeof G_vmlCanvasManager !== 'undefined'; };
//jscs:enable requireCamelCaseOrUpperCaseIdentifiers
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
* The object interactivity controls.
* @private
*/
_controlsVisibility: null,
/**
* Determines which corner has been clicked
* @private
* @param {Object} pointer The pointer indicating the mouse position
* @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
*/
_findTargetCorner: function(pointer) {
if (!this.hasControls || !this.active) {
return false;
}
var ex = pointer.x,
ey = pointer.y,
xPoints,
lines;
this.__corner = 0;
for (var i in this.oCoords) {
if (!this.isControlVisible(i)) {
continue;
}
if (i === 'mtr' && !this.hasRotatingPoint) {
continue;
}
if (this.get('lockUniScaling') &&
(i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) {
continue;
}
lines = this._getImageLines(this.oCoords[i].corner);
// debugging
// canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2);
xPoints = this._findCrossPoints({ x: ex, y: ey }, lines);
if (xPoints !== 0 && xPoints % 2 === 1) {
this.__corner = i;
return i;
}
}
return false;
},
/**
* Sets the coordinates of the draggable boxes in the corners of
* the image used to scale/rotate it.
* @private
*/
_setCornerCoords: function() {
var coords = this.oCoords,
newTheta = degreesToRadians(45 - this.angle),
/* Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, */
/* 0.707106 stands for sqrt(2)/2 */
cornerHypotenuse = this.cornerSize * 0.707106,
cosHalfOffset = cornerHypotenuse * Math.cos(newTheta),
sinHalfOffset = cornerHypotenuse * Math.sin(newTheta),
x, y;
for (var point in coords) {
x = coords[point].x;
y = coords[point].y;
coords[point].corner = {
tl: {
x: x - sinHalfOffset,
y: y - cosHalfOffset
},
tr: {
x: x + cosHalfOffset,
y: y - sinHalfOffset
},
bl: {
x: x - cosHalfOffset,
y: y + sinHalfOffset
},
br: {
x: x + sinHalfOffset,
y: y + cosHalfOffset
}
};
}
},
/*
* Calculate object dimensions from its properties
* @private
*/
_getNonTransformedDimensions: function() {
var strokeWidth = this.strokeWidth,
w = this.width,
h = this.height,
addStrokeToW = true,
addStrokeToH = true;
if (this.type === 'line' && this.strokeLineCap === 'butt') {
addStrokeToH = w;
addStrokeToW = h;
}
if (addStrokeToH) {
h += h < 0 ? -strokeWidth : strokeWidth;
}
if (addStrokeToW) {
w += w < 0 ? -strokeWidth : strokeWidth;
}
return { x: w, y: h };
},
/*
* @private
*/
_getTransformedDimensions: function(skewX, skewY) {
if (typeof skewX === 'undefined') {
skewX = this.skewX;
}
if (typeof skewY === 'undefined') {
skewY = this.skewY;
}
var dimensions = this._getNonTransformedDimensions(),
dimX = dimensions.x /2, dimY = dimensions.y / 2,
points = [
{
x: -dimX,
y: -dimY
},
{
x: dimX,
y: -dimY
},
{
x: -dimX,
y: dimY
},
{
x: dimX,
y: dimY
}],
i, transformMatrix = this._calcDimensionsTransformMatrix(skewX, skewY),
bbox;
for (i = 0; i < points.length; i++) {
points[i] = fabric.util.transformPoint(points[i], transformMatrix);
}
bbox = fabric.util.makeBoundingBoxFromPoints(points);
return { x: bbox.width, y: bbox.height };
},
/*
* private
*/
_calculateCurrentDimensions: function() {
var vpt = this.getViewportTransform(),
dim = this._getTransformedDimensions(),
w = dim.x, h = dim.y;
w += 2 * this.padding;
h += 2 * this.padding;
return fabric.util.transformPoint(new fabric.Point(w, h), vpt, true);
},
/**
* Draws borders of an object's bounding box.
* Requires public properties: width, height
* Requires public options: padding, borderColor
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawBorders: function(ctx) {
if (!this.hasBorders) {
return this;
}
ctx.save();
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = this.borderColor;
ctx.lineWidth = 1 / this.borderScaleFactor;
var wh = this._calculateCurrentDimensions(),
width = wh.x,
height = wh.y;
if (this.group) {
width = width * this.group.scaleX;
height = height * this.group.scaleY;
}
ctx.strokeRect(
~~(-(width / 2)) - 0.5, // offset needed to make lines look sharper
~~(-(height / 2)) - 0.5,
~~(width) + 1, // double offset needed to make lines look sharper
~~(height) + 1
);
if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) {
var rotateHeight = -height / 2;
ctx.beginPath();
ctx.moveTo(0, rotateHeight);
ctx.lineTo(0, rotateHeight - this.rotatingPointOffset);
ctx.closePath();
ctx.stroke();
}
ctx.restore();
return this;
},
/**
* Draws corners of an object's bounding box.
* Requires public properties: width, height
* Requires public options: cornerSize, padding
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawControls: function(ctx) {
if (!this.hasControls) {
return this;
}
var wh = this._calculateCurrentDimensions(),
width = wh.x,
height = wh.y,
scaleOffset = this.cornerSize / 2,
left = -(width / 2) - scaleOffset,
top = -(height / 2) - scaleOffset,
methodName = this.transparentCorners ? 'strokeRect' : 'fillRect';
ctx.save();
ctx.lineWidth = 1;
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = ctx.fillStyle = this.cornerColor;
// top-left
this._drawControl('tl', ctx, methodName,
left,
top);
// top-right
this._drawControl('tr', ctx, methodName,
left + width,
top);
// bottom-left
this._drawControl('bl', ctx, methodName,
left,
top + height);
// bottom-right
this._drawControl('br', ctx, methodName,
left + width,
top + height);
if (!this.get('lockUniScaling')) {
// middle-top
this._drawControl('mt', ctx, methodName,
left + width/2,
top);
// middle-bottom
this._drawControl('mb', ctx, methodName,
left + width/2,
top + height);
// middle-right
this._drawControl('mr', ctx, methodName,
left + width,
top + height/2);
// middle-left
this._drawControl('ml', ctx, methodName,
left,
top + height/2);
}
// middle-top-rotate
if (this.hasRotatingPoint) {
this._drawControl('mtr', ctx, methodName,
left + width/2 ,
top - this.rotatingPointOffset);
}
ctx.restore();
return this;
},
/**
* @private
*/
_drawControl: function(control, ctx, methodName, left, top) {
if (!this.isControlVisible(control)) {
return;
}
var size = this.cornerSize;
isVML() || this.transparentCorners || ctx.clearRect(left, top, size, size);
ctx[methodName](left, top, size, size);
},
/**
* Returns true if the specified control is visible, false otherwise.
* @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'.
* @returns {Boolean} true if the specified control is visible, false otherwise
*/
isControlVisible: function(controlName) {
return this._getControlsVisibility()[controlName];
},
/**
* Sets the visibility of the specified control.
* @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'.
* @param {Boolean} visible true to set the specified control visible, false otherwise
* @return {fabric.Object} thisArg
* @chainable
*/
setControlVisible: function(controlName, visible) {
this._getControlsVisibility()[controlName] = visible;
return this;
},
/**
* Sets the visibility state of object controls.
* @param {Object} [options] Options object
* @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it
* @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it
* @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it
* @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it
* @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it
* @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it
* @param {Boolean} [options.tl] true to enable the top-left control, false to disable it
* @param {Boolean} [options.tr] true to enable the top-right control, false to disable it
* @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it
* @return {fabric.Object} thisArg
* @chainable
*/
setControlsVisibility: function(options) {
options || (options = { });
for (var p in options) {
this.setControlVisible(p, options[p]);
}
return this;
},
/**
* Returns the instance of the control visibility set for this object.
* @private
* @returns {Object}
*/
_getControlsVisibility: function() {
if (!this._controlsVisibility) {
this._controlsVisibility = {
tl: true,
tr: true,
br: true,
bl: true,
ml: true,
mt: true,
mr: true,
mb: true,
mtr: true
};
}
return this._controlsVisibility;
}
});
})();