fabric.js/src/object.class.js
2012-12-19 19:36:42 +04:00

1760 lines
48 KiB
JavaScript

(function(global) {
"use strict";
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
toFixed = fabric.util.toFixed,
capitalize = fabric.util.string.capitalize,
getPointer = fabric.util.getPointer,
degreesToRadians = fabric.util.degreesToRadians;
if (fabric.Object) {
return;
}
/**
* Root object class from which all 2d shape classes inherit from
* @class Object
* @memberOf fabric
*/
fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ {
/**
* Type of an object (rect, circle, path, etc)
* @property
* @type String
*/
type: 'object',
/**
* Object top offset
* @property
* @type Number
*/
top: 0,
/**
* Object left offset
* @property
* @type Number
*/
left: 0,
/**
* Object width
* @property
* @type Number
*/
width: 0,
/**
* Object height
* @property
* @type Number
*/
height: 0,
/**
* Object scale factor (horizontal)
* @property
* @type Number
*/
scaleX: 1,
/**
* Object scale factor (vertical)
* @property
* @type Number
*/
scaleY: 1,
/**
* When true, an object is rendered as flipped horizontally
* @property
* @type Boolean
*/
flipX: false,
/**
* When true, an object is rendered as flipped vertically
* @property
* @type Boolean
*/
flipY: false,
/**
* Opacity of an object
* @property
* @type Number
*/
opacity: 1,
/**
* Angle of rotation of an object
* @property
* @type Number
*/
angle: 0,
/**
* Size of object's corners
* @property
* @type Number
*/
cornersize: 12,
/**
* When true, object's corners are rendered as transparent inside (i.e. stroke instead of fill)
* @property
* @type Boolean
*/
transparentCorners: true,
/**
* Padding between object and its borders
* @property
* @type Number
*/
padding: 0,
/**
* Border color of an object (when it's active)
* @property
* @type String
*/
borderColor: 'rgba(102,153,255,0.75)',
/**
* Corner color of an object (when it's active)
* @property
* @type String
*/
cornerColor: 'rgba(102,153,255,0.5)',
/**
* Color of object's fill
* @property
* @type String
*/
fill: 'rgb(0,0,0)',
/**
* Fill rule used to fill an object
* @property
* @type String
*/
fillRule: 'source-over',
/**
* Overlay fill (takes precedence over fill value)
* @property
* @type String
*/
overlayFill: null,
/**
* When `true`, an object is rendered via stroke
* @property
* @type String
*/
stroke: null,
/**
* Width of a stroke used to render this object
* @property
* @type Number
*/
strokeWidth: 1,
/**
* Array specifying dash pattern of an object's stroke
* @property
* @type Array
*/
strokeDashArray: null,
/**
* Opacity of a border when moving an object
* @property
* @type Number
*/
borderOpacityWhenMoving: 0.4,
/**
* Border scale factor
* @property
* @type Number
*/
borderScaleFactor: 1,
/**
* Transform matrix
* @property
* @type Array
*/
transformMatrix: null,
/**
* When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection)
* @property
* @type Boolean
*/
selectable: true,
/**
* When set to `false`, object's controls are not displayed and can not be used to manipulate object
* @property
* @type Boolean
*/
hasControls: true,
/**
* When set to `false`, object's borders are not rendered
* @property
* @type Boolean
*/
hasBorders: true,
/**
* When set to `false`, object's rotating point will not be visible or selectable
* @property
* @type Boolean
*/
hasRotatingPoint: false,
/**
* Offset for object's rotating point (when enabled)
* @property
* @type Number
*/
rotatingPointOffset: 40,
/**
* When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box
* @property
* @type Boolean
*/
perPixelTargetFind: false,
/**
* When false, default values are not included in serialization result
* @property
* @type Boolean
*/
includeDefaultValues: true,
/**
* List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged);
* as well as for history (undo/redo) purposes
* @property
* @type Array
*/
stateProperties: (
'top left width height scaleX scaleY flipX flipY ' +
'angle opacity cornersize fill overlayFill ' +
'stroke strokeWidth strokeDashArray fillRule ' +
'borderScaleFactor transformMatrix selectable'
).split(' '),
/**
* Constructor
* @method initialize
* @param {Object} [options] Options object
*/
initialize: function(options) {
if (options) {
this.setOptions(options);
}
},
/**
* @private
* @method _initGradient
*/
_initGradient: function(options) {
if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) {
this.set('fill', new fabric.Gradient(options.fill));
}
},
/**
* Sets object's properties from options
* @method setOptions
* @param {Object} [options]
*/
setOptions: function(options) {
for (var prop in options) {
this.set(prop, options[prop]);
}
this._initGradient(options);
},
/**
* Transforms context when rendering an object
* @method transform
* @param {CanvasRenderingContext2D} ctx Context
*/
transform: function(ctx) {
ctx.globalAlpha = this.opacity;
ctx.translate(this.left, this.top);
ctx.rotate(degreesToRadians(this.angle));
ctx.scale(
this.scaleX * (this.flipX ? -1 : 1),
this.scaleY * (this.flipY ? -1 : 1)
);
},
/**
* Returns an object representation of an instance
* @method toObject
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
var object = {
type: this.type,
left: toFixed(this.left, NUM_FRACTION_DIGITS),
top: toFixed(this.top, NUM_FRACTION_DIGITS),
width: toFixed(this.width, NUM_FRACTION_DIGITS),
height: toFixed(this.height, NUM_FRACTION_DIGITS),
fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill,
overlayFill: this.overlayFill,
stroke: this.stroke,
strokeWidth: this.strokeWidth,
strokeDashArray: this.strokeDashArray,
scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS),
scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS),
angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS),
flipX: this.flipX,
flipY: this.flipY,
opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS),
selectable: this.selectable,
hasControls: this.hasControls,
hasBorders: this.hasBorders,
hasRotatingPoint: this.hasRotatingPoint,
transparentCorners: this.transparentCorners,
perPixelTargetFind: this.perPixelTargetFind
};
if (!this.includeDefaultValues) {
object = this._removeDefaultValues(object);
}
fabric.util.populateWithProperties(this, object, propertiesToInclude);
return object;
},
/**
* Returns (dataless) object representation of an instance
* @method toDatalessObject
* @param {Array} [propertiesToInclude]
* @return {Object} object representation of an instance
*/
toDatalessObject: function(propertiesToInclude) {
// will be overwritten by subclasses
return this.toObject(propertiesToInclude);
},
/**
* Returns styles-string for svg-export
* @method getSvgStyles
* @return {String}
*/
getSvgStyles: function() {
return [
"stroke: ", (this.stroke ? this.stroke : 'none'), "; ",
"stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ",
"stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "),
"fill: ", (this.fill ? this.fill : 'none'), "; ",
"opacity: ", (this.opacity ? this.opacity : '1'), ";"
].join("");
},
/**
* Returns transform-string for svg-export
* @method getSvgTransform
* @return {String}
*/
getSvgTransform: function() {
var angle = this.getAngle();
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
var translatePart = "translate(" +
toFixed(this.left, NUM_FRACTION_DIGITS) +
" " +
toFixed(this.top, NUM_FRACTION_DIGITS) +
")";
var anglePart = angle !== 0
? (" rotate(" + toFixed(angle, NUM_FRACTION_DIGITS) + ")")
: '';
var scalePart = (this.scaleX === 1 && this.scaleY === 1)
? '' :
(" scale(" +
toFixed(this.scaleX, NUM_FRACTION_DIGITS) +
" " +
toFixed(this.scaleY, NUM_FRACTION_DIGITS) +
")");
var flipXPart = this.flipX ? "matrix(-1 0 0 1 0 0) " : "";
var flipYPart = this.flipY ? "matrix(1 0 0 -1 0 0)" : "";
return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join('');
},
/**
* @private
* @method _removeDefaultValues
*/
_removeDefaultValues: function(object) {
var defaultOptions = fabric.Object.prototype.options;
if (defaultOptions) {
this.stateProperties.forEach(function(prop) {
if (object[prop] === defaultOptions[prop]) {
delete object[prop];
}
});
}
return object;
},
/**
* Returns true if an object is in its active state
* @return {Boolean} true if an object is in its active state
*/
isActive: function() {
return !!this.active;
},
/**
* Sets state of an object - `true` makes it active, `false` - inactive
* @param {Boolean} active
* @return {fabric.Object} thisArg
* @chainable
*/
setActive: function(active) {
this.active = !!active;
return this;
},
/**
* Returns a string representation of an instance
* @return {String}
*/
toString: function() {
return "#<fabric." + capitalize(this.type) + ">";
},
/**
* Sets property to a given value
* @method set
* @param {String} name
* @param {Object|Function} value
* @return {fabric.Group} thisArg
* @chainable
*/
set: function(key, value) {
if (typeof key === 'object') {
for (var prop in key) {
this._set(prop, key[prop]);
}
}
else {
if (typeof value === 'function') {
this._set(key, value(this.get(key)));
}
else {
this._set(key, value);
}
}
return this;
},
/**
* @private
* @method _set
* @param key
* @param value
*/
_set: function(key, value) {
var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') &&
value < fabric.Object.MIN_SCALE_LIMIT;
if (shouldConstrainValue) {
value = fabric.Object.MIN_SCALE_LIMIT;
}
this[key] = value;
},
/**
* Toggles specified property from `true` to `false` or from `false` to `true`
* @method toggle
* @param {String} property property to toggle
* @return {fabric.Object} thisArg
* @chainable
*/
toggle: function(property) {
var value = this.get(property);
if (typeof value === 'boolean') {
this.set(property, !value);
}
return this;
},
/**
* Sets sourcePath of an object
* @method setSourcePath
* @param {String} value
* @return {fabric.Object} thisArg
* @chainable
*/
setSourcePath: function(value) {
this.sourcePath = value;
return this;
},
/**
* Basic getter
* @method get
* @param {String} property
* @return {Any} value of a property
*/
get: function(property) {
return this[property];
},
/**
* Renders an object on a specified context
* @method render
* @param {CanvasRenderingContext2D} ctx context to render on
* @param {Boolean} noTransform
*/
render: function(ctx, noTransform) {
// do not render if width or height are zeros
if (this.width === 0 || this.height === 0) return;
ctx.save();
var m = this.transformMatrix;
if (m && !this.group) {
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
if (!noTransform) {
this.transform(ctx);
}
if (this.stroke || this.strokeDashArray) {
ctx.lineWidth = this.strokeWidth;
ctx.strokeStyle = this.stroke;
}
if (this.overlayFill) {
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
ctx.fillStyle = this.fill.toLiveGradient
? this.fill.toLiveGradient(ctx)
: this.fill;
}
if (m && this.group) {
ctx.translate(-this.group.width/2, -this.group.height/2);
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
this._render(ctx, noTransform);
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.drawCorners(ctx);
}
ctx.restore();
},
/**
* Returns width of an object
* @method getWidth
* @return {Number} width value
*/
getWidth: function() {
return this.width * this.scaleX;
},
/**
* Returns height of an object
* @method getHeight
* @return {Number} height value
*/
getHeight: function() {
return this.height * this.scaleY;
},
/**
* Scales an object (equally by x and y)
* @method scale
* @param value {Number} scale factor
* @return {fabric.Object} thisArg
* @chainable
*/
scale: function(value) {
this.scaleX = value;
this.scaleY = value;
this.setCoords();
return this;
},
/**
* Scales an object to a given width, with respect to bounding box (scaling by x/y equally)
* @method scaleToWidth
* @param value {Number} new width value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToWidth: function(value) {
// adjust to bounding rect factor so that rotated shapes would fit as well
var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth();
return this.scale(value / this.width / boundingRectFactor);
},
/**
* Scales an object to a given height, with respect to bounding box (scaling by x/y equally)
* @method scaleToHeight
* @param value {Number} new height value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToHeight: function(value) {
// adjust to bounding rect factor so that rotated shapes would fit as well
var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight();
return this.scale(value / this.height / boundingRectFactor);
},
/**
* Sets corner position coordinates based on current angle, width and height
* @method setCoords
* @return {fabric.Object} thisArg
* @chainable
*/
setCoords: function() {
var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0,
padding = this.padding,
theta = degreesToRadians(this.angle);
this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2;
this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2;
//If width is negative, make postive. Fixes path selection issue
if(this.currentWidth < 0){
this.currentWidth = Math.abs(this.currentWidth);
}
var _hypotenuse = Math.sqrt(
Math.pow(this.currentWidth / 2, 2) +
Math.pow(this.currentHeight / 2, 2));
var _angle = Math.atan(this.currentHeight / this.currentWidth);
// offset added for rotate and scale actions
var offsetX = Math.cos(_angle + theta) * _hypotenuse,
offsetY = Math.sin(_angle + theta) * _hypotenuse,
sinTh = Math.sin(theta),
cosTh = Math.cos(theta);
var tl = {
x: this.left - offsetX,
y: this.top - offsetY
};
var tr = {
x: tl.x + (this.currentWidth * cosTh),
y: tl.y + (this.currentWidth * sinTh)
};
var br = {
x: tr.x - (this.currentHeight * sinTh),
y: tr.y + (this.currentHeight * cosTh)
};
var bl = {
x: tl.x - (this.currentHeight * sinTh),
y: tl.y + (this.currentHeight * cosTh)
};
var ml = {
x: tl.x - (this.currentHeight/2 * sinTh),
y: tl.y + (this.currentHeight/2 * cosTh)
};
var mt = {
x: tl.x + (this.currentWidth/2 * cosTh),
y: tl.y + (this.currentWidth/2 * sinTh)
};
var mr = {
x: tr.x - (this.currentHeight/2 * sinTh),
y: tr.y + (this.currentHeight/2 * cosTh)
};
var mb = {
x: bl.x + (this.currentWidth/2 * cosTh),
y: bl.y + (this.currentWidth/2 * sinTh)
};
var mtr = {
x: tl.x + (this.currentWidth/2 * cosTh),
y: tl.y + (this.currentWidth/2 * sinTh)
};
// debugging
// setTimeout(function() {
// canvas.contextTop.fillStyle = 'green';
// canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
// canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
// canvas.contextTop.fillRect(br.x, br.y, 3, 3);
// canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
// canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
// canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
// canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
// canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
// }, 50);
// clockwise
this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr };
// set coordinates of the draggable boxes in the corners used to scale/rotate the image
this._setCornerCoords();
return this;
},
/**
* Returns width of an object's bounding rectangle
* @method getBoundingRectWidth
* @return {Number} width value
*/
getBoundingRectWidth: function() {
this.oCoords || this.setCoords();
var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x];
var minX = fabric.util.array.min(xCoords);
var maxX = fabric.util.array.max(xCoords);
return Math.abs(minX - maxX);
},
/**
* Returns height of an object's bounding rectangle
* @method getBoundingRectHeight
* @return {Number} height value
*/
getBoundingRectHeight: function() {
this.oCoords || this.setCoords();
var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y];
var minY = fabric.util.array.min(yCoords);
var maxY = fabric.util.array.max(yCoords);
return Math.abs(minY - maxY);
},
/**
* Draws borders of an object's bounding box.
* Requires public properties: width, height
* Requires public options: padding, borderColor
* @method drawBorders
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawBorders: function(ctx) {
if (!this.hasBorders) return;
var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT,
padding = this.padding,
padding2 = padding * 2,
strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0;
ctx.save();
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = this.borderColor;
var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX),
scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleY);
ctx.lineWidth = 1 / this.borderScaleFactor;
ctx.scale(scaleX, scaleY);
var w = this.getWidth(),
h = this.getHeight();
ctx.strokeRect(
~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper
~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5,
~~(w + padding2 + strokeWidth * this.scaleX),
~~(h + padding2 + strokeWidth * this.scaleY)
);
if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) {
var rotateHeight = (
this.flipY
? h + (strokeWidth * this.scaleY) + (padding * 2)
: -h - (strokeWidth * this.scaleY) - (padding * 2)
) / 2;
ctx.beginPath();
ctx.moveTo(0, rotateHeight);
ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset));
ctx.closePath();
ctx.stroke();
}
ctx.restore();
return this;
},
/**
* @private
* @method _renderDashedStroke
*/
_renderDashedStroke: function(ctx) {
if (1 & this.strokeDashArray.length /* if odd number of items */) {
/* duplicate items */
this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray);
}
var i = 0,
x = -this.width/2, y = -this.height/2,
_this = this,
padding = this.padding,
dashedArrayLength = this.strokeDashArray.length;
ctx.save();
ctx.beginPath();
/** @ignore */
function renderSide(xMultiplier, yMultiplier) {
var lineLength = 0,
lengthDiff = 0,
sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2;
while (lineLength < sideLength) {
var lengthOfSubPath = _this.strokeDashArray[i++];
lineLength += lengthOfSubPath;
if (lineLength > sideLength) {
lengthDiff = lineLength - sideLength;
}
// track coords
if (xMultiplier) {
x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0);
}
else {
y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0);
}
ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y);
if (i >= dashedArrayLength) {
i = 0;
}
}
}
renderSide(1, 0);
renderSide(0, 1);
renderSide(-1, 0);
renderSide(0, -1);
ctx.stroke();
ctx.closePath();
ctx.restore();
},
/**
* Draws corners of an object's bounding box.
* Requires public properties: width, height, scaleX, scaleY
* Requires public options: cornersize, padding
* @method drawCorners
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawCorners: function(ctx) {
if (!this.hasControls) return;
var size = this.cornersize,
size2 = size / 2,
strokeWidth2 = this.strokeWidth / 2,
left = -(this.width / 2),
top = -(this.height / 2),
_left,
_top,
sizeX = size / this.scaleX,
sizeY = size / this.scaleY,
paddingX = this.padding / this.scaleX,
paddingY = this.padding / this.scaleY,
scaleOffsetY = size2 / this.scaleY,
scaleOffsetX = size2 / this.scaleX,
scaleOffsetSizeX = (size2 - size) / this.scaleX,
scaleOffsetSizeY = (size2 - size) / this.scaleY,
height = this.height,
width = this.width,
methodName = this.transparentCorners ? 'strokeRect' : 'fillRect';
ctx.save();
ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY);
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = ctx.fillStyle = this.cornerColor;
// top-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// top-right
_left = left + width - scaleOffsetX + strokeWidth2 + paddingX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// bottom-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// bottom-right
_left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
if (!this.get('lockUniScaling')) {
// middle-top
_left = left + width/2 - scaleOffsetX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-bottom
_left = left + width/2 - scaleOffsetX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-right
_left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
_top = top + height/2 - scaleOffsetY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top + height/2 - scaleOffsetY;
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
}
// middle-top-rotate
if (this.hasRotatingPoint) {
_left = left + width/2 - scaleOffsetX;
_top = this.flipY ?
(top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY)
: (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY);
ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
}
ctx.restore();
return this;
},
/**
* Clones an instance
* @method clone
* @param {Function} callback Callback is invoked with a clone as a first argument
* @param {Array} propertiesToInclude
* @return {fabric.Object} clone of an instance
*/
clone: function(callback, propertiesToInclude) {
if (this.constructor.fromObject) {
return this.constructor.fromObject(this.toObject(propertiesToInclude), callback);
}
return new fabric.Object(this.toObject(propertiesToInclude));
},
/**
* Creates an instance of fabric.Image out of an object
* @method cloneAsImage
* @param callback {Function} callback, invoked with an instance as a first argument
* @return {fabric.Object} thisArg
* @chainable
*/
cloneAsImage: function(callback) {
if (fabric.Image) {
var i = new Image();
/** @ignore */
i.onload = function() {
if (callback) {
callback(new fabric.Image(i), orig);
}
i = i.onload = null;
};
var orig = {
angle: this.get('angle'),
flipX: this.get('flipX'),
flipY: this.get('flipY')
};
// normalize angle
this.set('angle', 0).set('flipX', false).set('flipY', false);
this.toDataURL(function(dataURL) {
i.src = dataURL;
});
}
return this;
},
/**
* Converts an object into a data-url-like string
* @method toDataURL
* @return {String} string of data
*/
toDataURL: function(callback) {
var el = fabric.document.createElement('canvas');
if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(el);
}
el.width = this.getBoundingRectWidth();
el.height = this.getBoundingRectHeight();
fabric.util.wrapElement(el, 'div');
var canvas = new fabric.Canvas(el);
canvas.backgroundColor = 'transparent';
canvas.renderAll();
if (this.constructor.async) {
this.clone(proceed);
}
else {
proceed(this.clone());
}
function proceed(clone) {
clone.left = el.width / 2;
clone.top = el.height / 2;
clone.setActive(false);
canvas.add(clone);
var data = canvas.toDataURL('png');
canvas.dispose();
canvas = clone = null;
callback && callback(data);
}
},
/**
* Returns true if object state (one of its state properties) was changed
* @method hasStateChanged
* @return {Boolean} true if instance' state has changed
*/
hasStateChanged: function() {
return this.stateProperties.some(function(prop) {
return this[prop] !== this.originalState[prop];
}, this);
},
/**
* Saves state of an object
* @method saveState
* @return {fabric.Object} thisArg
* @chainable
*/
saveState: function() {
this.stateProperties.forEach(function(prop) {
this.originalState[prop] = this.get(prop);
}, this);
return this;
},
/**
* Setups state of an object
* @method setupState
*/
setupState: function() {
this.originalState = { };
this.saveState();
},
/**
* Returns true if object intersects with an area formed by 2 points
* @method intersectsWithRect
* @param {Object} selectionTL
* @param {Object} selectionBR
* @return {Boolean}
*/
intersectsWithRect: function(selectionTL, selectionBR) {
var oCoords = this.oCoords,
tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br = new fabric.Point(oCoords.br.x, oCoords.br.y);
var intersection = fabric.Intersection.intersectPolygonRectangle(
[tl, tr, br, bl],
selectionTL,
selectionBR
);
return (intersection.status === 'Intersection');
},
/**
* Returns true if object intersects with another object
* @method intersectsWithObject
* @param {Object} other Object to test
* @return {Boolean}
*/
intersectsWithObject: function(other) {
// extracts coords
function getCoords(oCoords) {
return {
tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br: new fabric.Point(oCoords.br.x, oCoords.br.y)
};
}
var thisCoords = getCoords(this.oCoords),
otherCoords = getCoords(other.oCoords);
var intersection = fabric.Intersection.intersectPolygonPolygon(
[thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl],
[otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl]
);
return (intersection.status === 'Intersection');
},
/**
* Returns true if object is fully contained within area of another object
* @method isContainedWithinObject
* @param {Object} other Object to test
* @return {Boolean}
*/
isContainedWithinObject: function(other) {
return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br);
},
/**
* Returns true if object is fully contained within area formed by 2 points
* @method isContainedWithinRect
* @param {Object} selectionTL
* @param {Object} selectionBR
* @return {Boolean}
*/
isContainedWithinRect: function(selectionTL, selectionBR) {
var oCoords = this.oCoords,
tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y);
return tl.x > selectionTL.x
&& tr.x < selectionBR.x
&& tl.y > selectionTL.y
&& bl.y < selectionBR.y;
},
/**
* Returns true if specified type is identical to the type of an instance
* @method isType
* @param type {String} type to check against
* @return {Boolean}
*/
isType: function(type) {
return this.type === type;
},
/**
* Determines which one of the four corners has been clicked
* @method _findTargetCorner
* @private
* @param e {Event} event object
* @param offset {Object} canvas offset
* @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
*/
_findTargetCorner: function(e, offset) {
if (!this.hasControls || !this.active) return false;
var pointer = getPointer(e),
ex = pointer.x - offset.left,
ey = pointer.y - offset.top,
xpoints,
lines;
for (var i in this.oCoords) {
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, i);
// 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(ex, ey, lines);
if (xpoints % 2 === 1 && xpoints !== 0) {
this.__corner = i;
return i;
}
}
return false;
},
/**
* Helper method to determine how many cross points are between the 4 image edges
* and the horizontal line determined by the position of our mouse when clicked on canvas
* @method _findCrossPoints
* @private
* @param ex {Number} x coordinate of the mouse
* @param ey {Number} y coordinate of the mouse
* @param oCoords {Object} Coordinates of the image being evaluated
*/
_findCrossPoints: function(ex, ey, oCoords) {
var b1, b2, a1, a2, xi, yi,
xcount = 0,
iLine;
for (var lineKey in oCoords) {
iLine = oCoords[lineKey];
// optimisation 1: line below dot. no cross
if ((iLine.o.y < ey) && (iLine.d.y < ey)) {
continue;
}
// optimisation 2: line above dot. no cross
if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) {
continue;
}
// optimisation 3: vertical line case
if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) {
xi = iLine.o.x;
yi = ey;
}
// calculate the intersection point
else {
b1 = 0;
b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x);
a1 = ey-b1*ex;
a2 = iLine.o.y-b2*iLine.o.x;
xi = - (a1-a2)/(b1-b2);
yi = a1+b1*xi;
}
// dont count xi < ex cases
if (xi >= ex) {
xcount += 1;
}
// optimisation 4: specific for square images
if (xcount === 2) {
break;
}
}
return xcount;
},
/**
* Method that returns an object with the image lines in it given the coordinates of the corners
* @method _getImageLines
* @private
* @param oCoords {Object} coordinates of the image corners
*/
_getImageLines: function(oCoords) {
return {
topline: {
o: oCoords.tl,
d: oCoords.tr
},
rightline: {
o: oCoords.tr,
d: oCoords.br
},
bottomline: {
o: oCoords.br,
d: oCoords.bl
},
leftline: {
o: oCoords.bl,
d: oCoords.tl
}
};
},
/**
* Sets the coordinates of the draggable boxes in the corners of
* the image used to scale/rotate it.
* @method _setCornerCoords
* @private
*/
_setCornerCoords: function() {
var coords = this.oCoords,
theta = degreesToRadians(this.angle),
newTheta = degreesToRadians(45 - this.angle),
cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2,
cosHalfOffset = cornerHypotenuse * Math.cos(newTheta),
sinHalfOffset = cornerHypotenuse * Math.sin(newTheta),
sinTh = Math.sin(theta),
cosTh = Math.cos(theta);
coords.tl.corner = {
tl: {
x: coords.tl.x - sinHalfOffset,
y: coords.tl.y - cosHalfOffset
},
tr: {
x: coords.tl.x + cosHalfOffset,
y: coords.tl.y - sinHalfOffset
},
bl: {
x: coords.tl.x - cosHalfOffset,
y: coords.tl.y + sinHalfOffset
},
br: {
x: coords.tl.x + sinHalfOffset,
y: coords.tl.y + cosHalfOffset
}
};
coords.tr.corner = {
tl: {
x: coords.tr.x - sinHalfOffset,
y: coords.tr.y - cosHalfOffset
},
tr: {
x: coords.tr.x + cosHalfOffset,
y: coords.tr.y - sinHalfOffset
},
br: {
x: coords.tr.x + sinHalfOffset,
y: coords.tr.y + cosHalfOffset
},
bl: {
x: coords.tr.x - cosHalfOffset,
y: coords.tr.y + sinHalfOffset
}
};
coords.bl.corner = {
tl: {
x: coords.bl.x - sinHalfOffset,
y: coords.bl.y - cosHalfOffset
},
bl: {
x: coords.bl.x - cosHalfOffset,
y: coords.bl.y + sinHalfOffset
},
br: {
x: coords.bl.x + sinHalfOffset,
y: coords.bl.y + cosHalfOffset
},
tr: {
x: coords.bl.x + cosHalfOffset,
y: coords.bl.y - sinHalfOffset
}
};
coords.br.corner = {
tr: {
x: coords.br.x + cosHalfOffset,
y: coords.br.y - sinHalfOffset
},
bl: {
x: coords.br.x - cosHalfOffset,
y: coords.br.y + sinHalfOffset
},
br: {
x: coords.br.x + sinHalfOffset,
y: coords.br.y + cosHalfOffset
},
tl: {
x: coords.br.x - sinHalfOffset,
y: coords.br.y - cosHalfOffset
}
};
coords.ml.corner = {
tl: {
x: coords.ml.x - sinHalfOffset,
y: coords.ml.y - cosHalfOffset
},
tr: {
x: coords.ml.x + cosHalfOffset,
y: coords.ml.y - sinHalfOffset
},
bl: {
x: coords.ml.x - cosHalfOffset,
y: coords.ml.y + sinHalfOffset
},
br: {
x: coords.ml.x + sinHalfOffset,
y: coords.ml.y + cosHalfOffset
}
};
coords.mt.corner = {
tl: {
x: coords.mt.x - sinHalfOffset,
y: coords.mt.y - cosHalfOffset
},
tr: {
x: coords.mt.x + cosHalfOffset,
y: coords.mt.y - sinHalfOffset
},
bl: {
x: coords.mt.x - cosHalfOffset,
y: coords.mt.y + sinHalfOffset
},
br: {
x: coords.mt.x + sinHalfOffset,
y: coords.mt.y + cosHalfOffset
}
};
coords.mr.corner = {
tl: {
x: coords.mr.x - sinHalfOffset,
y: coords.mr.y - cosHalfOffset
},
tr: {
x: coords.mr.x + cosHalfOffset,
y: coords.mr.y - sinHalfOffset
},
bl: {
x: coords.mr.x - cosHalfOffset,
y: coords.mr.y + sinHalfOffset
},
br: {
x: coords.mr.x + sinHalfOffset,
y: coords.mr.y + cosHalfOffset
}
};
coords.mb.corner = {
tl: {
x: coords.mb.x - sinHalfOffset,
y: coords.mb.y - cosHalfOffset
},
tr: {
x: coords.mb.x + cosHalfOffset,
y: coords.mb.y - sinHalfOffset
},
bl: {
x: coords.mb.x - cosHalfOffset,
y: coords.mb.y + sinHalfOffset
},
br: {
x: coords.mb.x + sinHalfOffset,
y: coords.mb.y + cosHalfOffset
}
};
coords.mtr.corner = {
tl: {
x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset)
},
tr: {
x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset)
},
bl: {
x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset)
},
br: {
x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset)
}
};
},
/**
* Makes object's color grayscale
* @method toGrayscale
* @return {fabric.Object} thisArg
*/
toGrayscale: function() {
var fillValue = this.get('fill');
if (fillValue) {
this.set('overlayFill', new fabric.Color(fillValue).toGrayscale().toRgb());
}
return this;
},
/**
* Returns complexity of an instance
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return 0;
},
/**
* Returns a JSON representation of an instance
* @method toJSON
* @param {Array} propertiesToInclude
* @return {String} json
*/
toJSON: function(propertiesToInclude) {
// delegate, not alias
return this.toObject(propertiesToInclude);
},
/**
* Sets gradient fill of an object
* @method setGradientFill
*/
setGradientFill: function(options) {
this.set('fill', fabric.Gradient.forObject(this, options));
},
/**
* Animates object's properties
* @method animate
*
* As object — multiple properties
*
* object.animate({ left: ..., top: ... });
* object.animate({ left: ..., top: ... }, { duration: ... });
*
* As string — one property
*
* object.animate('left', ...);
* object.animate('left', { duration: ... });
*
*/
animate: function() {
if (arguments[0] && typeof arguments[0] === 'object') {
for (var prop in arguments[0]) {
this._animate(prop, arguments[0][prop], arguments[1]);
}
}
else {
this._animate.apply(this, arguments);
}
return this;
},
/**
* @private
* @method _animate
*/
_animate: function(property, to, options) {
var obj = this;
to = to.toString();
options || (options = { });
if (!('from' in options)) {
options.from = this.get(property);
}
if (~to.indexOf('=')) {
to = this.get(property) + parseFloat(to.replace('=', ''));
}
else {
to = parseFloat(to);
}
fabric.util.animate({
startValue: options.from,
endValue: to,
byValue: options.by,
easing: options.easing,
duration: options.duration,
onChange: function(value) {
obj.set(property, value);
options.onChange && options.onChange();
},
onComplete: function() {
obj.setCoords();
options.onComplete && options.onComplete();
}
});
},
/**
* Centers object horizontally on canvas to which it was added last
* @method centerH
* @return {fabric.Object} thisArg
*/
centerH: function () {
this.canvas.centerObjectH(this);
return this;
},
/**
* Centers object vertically on canvas to which it was added last
* @method centerV
* @return {fabric.Object} thisArg
* @chainable
*/
centerV: function () {
this.canvas.centerObjectV(this);
return this;
},
/**
* Centers object vertically and horizontally on canvas to which is was added last
* @method center
* @return {fabric.Object} thisArg
* @chainable
*/
center: function () {
return this.centerH().centerV();
},
/**
* Removes object from canvas to which it was added last
* @method remove
* @return {fabric.Object} thisArg
* @chainable
*/
remove: function() {
return this.canvas.remove(this);
},
/**
* Moves an object to the bottom of the stack of drawn objects
* @method sendToBack
* @return {fabric.Object} thisArg
* @chainable
*/
sendToBack: function() {
this.canvas.sendToBack(this);
return this;
},
/**
* Moves an object to the top of the stack of drawn objects
* @method bringToFront
* @return {fabric.Object} thisArg
* @chainable
*/
bringToFront: function() {
this.canvas.bringToFront(this);
return this;
},
/**
* Moves an object one level down in stack of drawn objects
* @method sendBackwards
* @return {fabric.Object} thisArg
* @chainable
*/
sendBackwards: function() {
this.canvas.sendBackwards(this);
return this;
},
/**
* Moves an object one level up in stack of drawn objects
* @method bringForward
* @return {fabric.Object} thisArg
* @chainable
*/
bringForward: function() {
this.canvas.bringForward(this);
return this;
}
});
var proto = fabric.Object.prototype;
for (var i = proto.stateProperties.length; i--; ) {
var propName = proto.stateProperties[i],
capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1),
setterName = 'set' + capitalizedPropName,
getterName = 'get' + capitalizedPropName;
// using `new Function` for better introspection
if (!proto[getterName]) {
proto[getterName] = (function(property) {
return new Function('return this.get("' + property + '")');
})(propName);
}
if (!proto[setterName]) {
proto[setterName] = (function(property) {
return new Function('value', 'return this.set("' + property + '", value)');
})(propName);
}
}
/**
* Alias for {@link fabric.Object.prototype.setAngle}
* @alias rotate -> setAngle
*/
fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle;
extend(fabric.Object.prototype, fabric.Observable);
extend(fabric.Object, {
/**
* @static
* @constant
* @type Number
*/
NUM_FRACTION_DIGITS: 2,
/**
* Minimum allowed scale value of an object
* @static
* @constant
* @type Number
*/
MIN_SCALE_LIMIT: 0.1
});
})(typeof exports !== 'undefined' ? exports : this);