fabric.js/src/object.class.js

1083 lines
28 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,
degreesToRadians = fabric.util.degreesToRadians;
if (fabric.Object) {
return;
}
/**
* Root object class from which all 2d shape classes inherit from
* @class fabric.Object
*/
fabric.Object = fabric.util.createClass(/** @lends fabric.Object.prototype */ {
/**
* Type of an object (rect, circle, path, etc.)
* @type String
*/
type: 'object',
/**
* Horizontal origin of transformation of an object (one of "left", "right", "center")
* @type String
*/
originX: 'center',
/**
* Vertical origin of transformation of an object (one of "top", "bottom", "center")
* @type String
*/
originY: 'center',
/**
* Top position of an object. Note that by default it's relative to object center. You can change this by setting originY={top/center/bottom}
* @type Number
*/
top: 0,
/**
* Left position of an object. Note that by default it's relative to object center. You can change this by setting originX={left/center/right}
* @type Number
*/
left: 0,
/**
* Object width
* @type Number
*/
width: 0,
/**
* Object height
* @type Number
*/
height: 0,
/**
* Object scale factor (horizontal)
* @type Number
*/
scaleX: 1,
/**
* Object scale factor (vertical)
* @type Number
*/
scaleY: 1,
/**
* When true, an object is rendered as flipped horizontally
* @type Boolean
*/
flipX: false,
/**
* When true, an object is rendered as flipped vertically
* @type Boolean
*/
flipY: false,
/**
* Opacity of an object
* @type Number
*/
opacity: 1,
/**
* Angle of rotation of an object (in degrees)
* @type Number
*/
angle: 0,
/**
* Size of object's corners (in pixels)
* @type Number
*/
cornerSize: 12,
/**
* When true, object's corners are rendered as transparent inside (i.e. stroke instead of fill)
* @type Boolean
*/
transparentCorners: true,
/**
* Padding between object and its borders (in pixels)
* @type Number
*/
padding: 0,
/**
* Border color of an object (when it's active)
* @type String
*/
borderColor: 'rgba(102,153,255,0.75)',
/**
* Corner color of an object (when it's active)
* @type String
*/
cornerColor: 'rgba(102,153,255,0.5)',
/**
* Color of object's fill
* @type String
*/
fill: 'rgb(0,0,0)',
/**
* Fill rule used to fill an object
* @type String
*/
fillRule: 'source-over',
/**
* Overlay fill (takes precedence over fill value)
* @type String
*/
overlayFill: null,
/**
* When `true`, an object is rendered via stroke and this property specifies its color
* @type String
*/
stroke: null,
/**
* Width of a stroke used to render this object
* @type Number
*/
strokeWidth: 1,
/**
* Array specifying dash pattern of an object's stroke
* @type Array
*/
strokeDashArray: null,
/**
* Shadow object representing shadow of this shape
* @type fabric.Shadow
*/
shadow: null,
/**
* Border opacity when object is active and moving
* @type Number
*/
borderOpacityWhenMoving: 0.4,
/**
* Border scale factor
* @type Number
*/
borderScaleFactor: 1,
/**
* Transform matrix (similar to SVG's transform matrix)
* @type Array
*/
transformMatrix: null,
/**
* Minimum allowed scale value of an object
* @type Number
*/
minScaleLimit: 0.01,
/**
* When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection)
* @type Boolean
*/
selectable: true,
/**
* When set to `false`, an object is not rendered on canvas
* @type Boolean
*/
visible: true,
/**
* When set to `false`, object's controls are not displayed and can not be used to manipulate object
* @type Boolean
*/
hasControls: true,
/**
* When set to `false`, object's borders are not rendered
* @type Boolean
*/
hasBorders: true,
/**
* When set to `false`, object's rotating point will not be visible or selectable
* @type Boolean
*/
hasRotatingPoint: true,
/**
* Offset for object's rotating point (when enabled via `hasRotatingPoint`)
* @type Number
*/
rotatingPointOffset: 40,
/**
* When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box
* @type Boolean
*/
perPixelTargetFind: false,
/**
* When `false`, default object's values are not included in its serialization
* @type Boolean
*/
includeDefaultValues: true,
/**
* Function that determines clipping of an object (context is passed as a first argument)
* @type Function
*/
clipTo: null,
/**
* When `true`, object horizontal movement is locked
* @type Boolean
*/
lockMovementX: false,
/**
* When `true`, object vertical movement is locked
* @type Boolean
*/
lockMovementY: false,
/**
* When `true`, object rotation is locked
* @type Boolean
*/
lockRotation: false,
/**
* When `true`, object horizontal scaling is locked
* @type Boolean
*/
lockScalingX: false,
/**
* When `true`, object vertical scaling is locked
* @type Boolean
*/
lockScalingY: false,
/**
* When `true`, object non-uniform scaling is locked
* @type Boolean
*/
lockUniScaling: false,
/**
* 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
* @type Array
*/
stateProperties: (
'top left width height scaleX scaleY flipX flipY ' +
'angle opacity cornerSize fill overlayFill originX originY ' +
'stroke strokeWidth strokeDashArray fillRule ' +
'borderScaleFactor transformMatrix selectable shadow visible'
).split(' '),
/**
* Constructor
* @param {Object} [options] Options object
*/
initialize: function(options) {
if (options) {
this.setOptions(options);
}
},
/**
* @private
*/
_initGradient: function(options) {
if (options.fill && options.fill.colorStops && !(options.fill instanceof fabric.Gradient)) {
this.set('fill', new fabric.Gradient(options.fill));
}
},
/**
* @private
*/
_initPattern: function(options) {
if (options.fill && options.fill.source && !(options.fill instanceof fabric.Pattern)) {
this.set('fill', new fabric.Pattern(options.fill));
}
if (options.stroke && options.stroke.source && !(options.stroke instanceof fabric.Pattern)) {
this.set('stroke', new fabric.Pattern(options.stroke));
}
},
/**
* @private
*/
_initShadow: function(options) {
if (options.shadow && !(options.shadow instanceof fabric.Shadow)) {
this.setShadow(options.shadow);
}
},
/**
* Sets object's properties from options
* @param {Object} [options]
*/
setOptions: function(options) {
for (var prop in options) {
this.set(prop, options[prop]);
}
this._initGradient(options);
this._initPattern(options);
this._initShadow(options);
},
/**
* Transforms context when rendering an object
* @param {CanvasRenderingContext2D} ctx Context
*/
transform: function(ctx) {
ctx.globalAlpha = this.opacity;
var center = this.getCenterPoint();
ctx.translate(center.x, center.y);
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
* @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,
originX: this.originX,
originY: this.originY,
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 && this.stroke.toObject) ? this.stroke.toObject() : 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,
shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow,
visible: this.visible
};
if (!this.includeDefaultValues) {
object = this._removeDefaultValues(object);
}
fabric.util.populateWithProperties(this, object, propertiesToInclude);
return object;
},
/**
* Returns (dataless) object representation of an instance
* @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
* @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 && this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) : 'none'), "; ",
"opacity: ", (typeof this.opacity !== 'undefined' ? this.opacity : '1'), ";",
(this.visible ? '' : " visibility: hidden;")
].join("");
},
/**
* Returns transform-string for svg-export
* @return {String}
*/
getSvgTransform: function() {
var angle = this.getAngle();
var center = this.getCenterPoint();
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
var translatePart = "translate(" +
toFixed(center.x, NUM_FRACTION_DIGITS) +
" " +
toFixed(center.y, 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
*/
_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 a string representation of an instance
* @return {String}
*/
toString: function() {
return "#<fabric." + capitalize(this.type) + ">";
},
/**
* Basic getter
* @param {String} property
* @return {Any} value of a property
*/
get: function(property) {
return this[property];
},
/**
* Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
* @param {String} name
* @param {Object|Function} value (if function, the value is passed into it and its return value is used as a new one)
* @return {fabric.Object} 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
* @param key
* @param value
*/
_set: function(key, value) {
var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY');
if (shouldConstrainValue) {
value = this._constrainScale(value);
}
if (key === 'scaleX' && value < 0) {
this.flipX = !this.flipX;
value *= -1;
}
else if (key === 'scaleY' && value < 0) {
this.flipY = !this.flipY;
value *= -1;
}
else if (key === 'width' || key === 'height') {
this.minScaleLimit = toFixed(Math.min(0.1, 1/Math.max(this.width, this.height)), 2);
}
this[key] = value;
return this;
},
/**
* Toggles specified property from `true` to `false` or from `false` to `true`
* @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
* @param {String} value
* @return {fabric.Object} thisArg
* @chainable
*/
setSourcePath: function(value) {
this.sourcePath = value;
return this;
},
/**
* Renders an object on a specified context
* @param {CanvasRenderingContext2D} ctx context to render on
* @param {Boolean} [noTransform] When true, context is not transformed
*/
render: function(ctx, noTransform) {
// do not render if width/height are zeros or object is not visible
if (this.width === 0 || this.height === 0 || !this.visible) 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;
if (this.stroke && this.stroke.toLive) {
ctx.strokeStyle = this.stroke.toLive(ctx);
}
else {
ctx.strokeStyle = this.stroke;
}
}
if (this.overlayFill) {
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
ctx.fillStyle = this.fill.toLive
? this.fill.toLive(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._setShadow(ctx);
this.clipTo && fabric.util.clipContext(this, ctx);
this._render(ctx, noTransform);
this.clipTo && ctx.restore();
this._removeShadow(ctx);
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.drawControls(ctx);
}
ctx.restore();
},
/**
* @private
*/
_setShadow: function(ctx) {
if (!this.shadow) return;
ctx.shadowColor = this.shadow.color;
ctx.shadowBlur = this.shadow.blur;
ctx.shadowOffsetX = this.shadow.offsetX;
ctx.shadowOffsetY = this.shadow.offsetY;
},
/**
* @private
*/
_removeShadow: function(ctx) {
ctx.shadowColor = '';
ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
},
/**
* @private
*/
_renderFill: function(ctx) {
if (!this.fill) return;
if (this.fill.toLive) {
ctx.save();
ctx.translate(
-this.width / 2 + this.fill.offsetX || 0,
-this.height / 2 + this.fill.offsetY || 0);
}
ctx.fill();
if (this.fill.toLive) {
ctx.restore();
}
},
/**
* Clones an instance
* @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
* @param callback {Function} callback, invoked with an instance as a first argument
* @return {fabric.Object} thisArg
* @chainable
*/
cloneAsImage: function(callback) {
if (fabric.Image) {
var orig = {
angle: this.getAngle(),
flipX: this.getFlipX(),
flipY: this.getFlipY()
};
// normalize angle
this.set({ angle: 0, flipX: false, flipY: false });
this.toDataURL(function(dataUrl) {
fabric.util.loadImage(dataUrl, function(img) {
if (callback) {
callback(new fabric.Image(img), orig);
}
});
});
}
return this;
},
/**
* Converts an object into a data-url-like string
* @param callback {Function} callback that recieves resulting data-url string
*/
toDataURL: function(callback) {
var el = fabric.util.createCanvasElement();
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.set('active', false);
canvas.add(clone);
var data = canvas.toDataURL();
canvas.dispose();
canvas = clone = null;
callback && callback(data);
}
},
/**
* Returns true if specified type is identical to the type of an instance
* @param type {String} type to check against
* @return {Boolean}
*/
isType: function(type) {
return this.type === type;
},
/**
* Makes object's color grayscale
* @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
* @return {Number} complexity
*/
complexity: function() {
return 0;
},
/**
* Returns a JSON representation of an instance
* @param {Array} propertiesToInclude Any properties that you might want to additionally include in the output
* @return {Object} JSON
*/
toJSON: function(propertiesToInclude) {
// delegate, not alias
return this.toObject(propertiesToInclude);
},
/**
* Sets gradient (fill or stroke) of an object
* @param {String} property Property name 'stroke' or 'fill'
* @param {Object} [options] Options object
*/
setGradient: function(property, options) {
options || (options = { });
var gradient = {colorStops: []};
gradient.type = options.type || (options.r1 || options.r2 ? 'radial' : 'linear');
gradient.coords = {
x1: options.x1,
y1: options.y1,
x2: options.x2,
y2: options.y2
};
if (options.r1 || options.r2) {
gradient.coords.r1 = options.r1;
gradient.coords.r2 = options.r2;
}
for (var position in options.colorStops) {
var color = new fabric.Color(options.colorStops[position]);
gradient.colorStops.push({offset: position, color: color.toRgb(), opacity: color.getAlpha()});
}
this.set(property, fabric.Gradient.forObject(this, gradient));
},
/**
* Sets pattern fill of an object
* @param {Object} [options] Options object
* @return {fabric.Object} thisArg
* @chainable
*/
setPatternFill: function(options) {
return this.set('fill', new fabric.Pattern(options));
},
/**
* Sets shadow of an object
* @param {Object} [options] Options object
* @return {fabric.Object} thisArg
* @chainable
*/
setShadow: function(options) {
return this.set('shadow', new fabric.Shadow(options));
},
/**
* Animates object's properties
* @param {String|Object} property to animate (if string) or properties to animate (if object)
* @param {Number|Object} value to animate property to (if string was given first) or options object
* @return {fabric.Object} thisArg
* @chainable
*
* 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') {
var propsToAnimate = [ ], prop, skipCallbacks;
for (prop in arguments[0]) {
propsToAnimate.push(prop);
}
for (var i = 0, len = propsToAnimate.length; i<len; i++) {
prop = propsToAnimate[i];
skipCallbacks = i !== len - 1;
this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks);
}
}
else {
this._animate.apply(this, arguments);
}
return this;
},
/**
* @private
* @param {String} property
* @param {String} to
* @param {Object} [options]
* @param {Boolean} [skipCallbacks]
*/
_animate: function(property, to, options, skipCallbacks) {
var obj = this, propPair;
to = to.toString();
if (!options) {
options = { };
}
else {
options = fabric.util.object.clone(options);
}
if (~property.indexOf('.')) {
propPair = property.split('.');
}
var currentValue = propPair
? this.get(propPair[0])[propPair[1]]
: this.get(property);
if (!('from' in options)) {
options.from = currentValue;
}
if (~to.indexOf('=')) {
to = currentValue + 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) {
if (propPair) {
obj[propPair[0]][propPair[1]] = value;
}
else {
obj.set(property, value);
}
if (skipCallbacks) return;
options.onChange && options.onChange();
},
onComplete: function() {
if (skipCallbacks) return;
obj.setCoords();
options.onComplete && options.onComplete();
}
});
},
/**
* Centers object horizontally on canvas to which it was added last
* @return {fabric.Object} thisArg
*/
centerH: function () {
this.canvas.centerObjectH(this);
return this;
},
/**
* Centers object vertically on canvas to which it was added last
* @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
* @return {fabric.Object} thisArg
* @chainable
*/
center: function () {
return this.centerH().centerV();
},
/**
* Removes object from canvas to which it was added last
* @return {fabric.Object} thisArg
* @chainable
*/
remove: function() {
return this.canvas.remove(this);
},
/**
* Moves an object to the bottom of the stack of drawn objects
* @return {fabric.Object} thisArg
* @chainable
*/
sendToBack: function() {
if (this.group) {
fabric.StaticCanvas.prototype.sendToBack.call(this.group, this);
}
else {
this.canvas.sendToBack(this);
}
return this;
},
/**
* Moves an object to the top of the stack of drawn objects
* @return {fabric.Object} thisArg
* @chainable
*/
bringToFront: function() {
if (this.group) {
fabric.StaticCanvas.prototype.bringToFront.call(this.group, this);
}
else {
this.canvas.bringToFront(this);
}
return this;
},
/**
* Moves an object one level down in stack of drawn objects
* @return {fabric.Object} thisArg
* @chainable
*/
sendBackwards: function() {
if (this.group) {
fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this);
}
else {
this.canvas.sendBackwards(this);
}
return this;
},
/**
* Moves an object one level up in stack of drawn objects
* @return {fabric.Object} thisArg
* @chainable
*/
bringForward: function() {
if (this.group) {
fabric.StaticCanvas.prototype.bringForward.call(this.group, this);
}
else {
this.canvas.bringForward(this);
}
return this;
},
/**
* Moves an object to specified level in stack of drawn objects
* @param {Number} index New position of object
* @return {fabric.Object} thisArg
* @chainable
*/
moveTo: function(index) {
if (this.group) {
fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index);
}
else {
this.canvas.moveTo(this, index);
}
return this;
}
});
fabric.util.createAccessors(fabric.Object);
/**
* Alias for {@link fabric.Object.prototype.setAngle}
* @alias rotate -> setAngle
*/
fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle;
extend(fabric.Object.prototype, fabric.Observable);
/**
* Defines the number of fraction digits when serializing object values. You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc.
* @static
* @constant
* @type Number
*/
fabric.Object.NUM_FRACTION_DIGITS = 2;
/**
* @static
* @type Number
*/
fabric.Object.__uid = 0;
})(typeof exports !== 'undefined' ? exports : this);