mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-11 09:50:59 +00:00
1751 lines
55 KiB
JavaScript
1751 lines
55 KiB
JavaScript
(function(global) {
|
|
|
|
'use strict';
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
extend = fabric.util.object.extend,
|
|
clone = fabric.util.object.clone,
|
|
toFixed = fabric.util.toFixed,
|
|
capitalize = fabric.util.string.capitalize,
|
|
degreesToRadians = fabric.util.degreesToRadians,
|
|
supportsLineDash = fabric.StaticCanvas.supports('setLineDash'),
|
|
objectCaching = !fabric.isLikelyNode,
|
|
ALIASING_LIMIT = 2;
|
|
|
|
if (fabric.Object) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Root object class from which all 2d shape classes inherit from
|
|
* @class fabric.Object
|
|
* @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects}
|
|
* @see {@link fabric.Object#initialize} for constructor definition
|
|
*
|
|
* @fires added
|
|
* @fires removed
|
|
*
|
|
* @fires selected
|
|
* @fires deselected
|
|
* @fires modified
|
|
* @fires rotating
|
|
* @fires scaling
|
|
* @fires moving
|
|
* @fires skewing
|
|
*
|
|
* @fires mousedown
|
|
* @fires mouseup
|
|
* @fires mouseover
|
|
* @fires mouseout
|
|
* @fires mousewheel
|
|
* @fires mousedblclick
|
|
*/
|
|
fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ {
|
|
|
|
/**
|
|
* Type of an object (rect, circle, path, etc.).
|
|
* Note that this property is meant to be read-only and not meant to be modified.
|
|
* If you modify, certain parts of Fabric (such as JSON loading) won't work correctly.
|
|
* @type String
|
|
* @default
|
|
*/
|
|
type: 'object',
|
|
|
|
/**
|
|
* Horizontal origin of transformation of an object (one of "left", "right", "center")
|
|
* See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups
|
|
* @type String
|
|
* @default
|
|
*/
|
|
originX: 'left',
|
|
|
|
/**
|
|
* Vertical origin of transformation of an object (one of "top", "bottom", "center")
|
|
* See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups
|
|
* @type String
|
|
* @default
|
|
*/
|
|
originY: 'top',
|
|
|
|
/**
|
|
* Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom}
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
top: 0,
|
|
|
|
/**
|
|
* Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right}
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
left: 0,
|
|
|
|
/**
|
|
* Object width
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
width: 0,
|
|
|
|
/**
|
|
* Object height
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
height: 0,
|
|
|
|
/**
|
|
* Object scale factor (horizontal)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
scaleX: 1,
|
|
|
|
/**
|
|
* Object scale factor (vertical)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
scaleY: 1,
|
|
|
|
/**
|
|
* When true, an object is rendered as flipped horizontally
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
flipX: false,
|
|
|
|
/**
|
|
* When true, an object is rendered as flipped vertically
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
flipY: false,
|
|
|
|
/**
|
|
* Opacity of an object
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
opacity: 1,
|
|
|
|
/**
|
|
* Angle of rotation of an object (in degrees)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
angle: 0,
|
|
|
|
/**
|
|
* Angle of skew on x axes of an object (in degrees)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
skewX: 0,
|
|
|
|
/**
|
|
* Angle of skew on y axes of an object (in degrees)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
skewY: 0,
|
|
|
|
/**
|
|
* Size of object's controlling corners (in pixels)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
cornerSize: 13,
|
|
|
|
/**
|
|
* When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill)
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
transparentCorners: true,
|
|
|
|
/**
|
|
* Default cursor value used when hovering over this object on canvas
|
|
* @type String
|
|
* @default
|
|
*/
|
|
hoverCursor: null,
|
|
|
|
/**
|
|
* Default cursor value used when moving this object on canvas
|
|
* @type String
|
|
* @default
|
|
*/
|
|
moveCursor: null,
|
|
|
|
/**
|
|
* Padding between object and its controlling borders (in pixels)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
padding: 0,
|
|
|
|
/**
|
|
* Color of controlling borders of an object (when it's active)
|
|
* @type String
|
|
* @default
|
|
*/
|
|
borderColor: 'rgba(102,153,255,0.75)',
|
|
|
|
/**
|
|
* Array specifying dash pattern of an object's borders (hasBorder must be true)
|
|
* @since 1.6.2
|
|
* @type Array
|
|
*/
|
|
borderDashArray: null,
|
|
|
|
/**
|
|
* Color of controlling corners of an object (when it's active)
|
|
* @type String
|
|
* @default
|
|
*/
|
|
cornerColor: 'rgba(102,153,255,0.5)',
|
|
|
|
/**
|
|
* Color of controlling corners of an object (when it's active and transparentCorners false)
|
|
* @since 1.6.2
|
|
* @type String
|
|
* @default
|
|
*/
|
|
cornerStrokeColor: null,
|
|
|
|
/**
|
|
* Specify style of control, 'rect' or 'circle'
|
|
* @since 1.6.2
|
|
* @type String
|
|
*/
|
|
cornerStyle: 'rect',
|
|
|
|
/**
|
|
* Array specifying dash pattern of an object's control (hasBorder must be true)
|
|
* @since 1.6.2
|
|
* @type Array
|
|
*/
|
|
cornerDashArray: null,
|
|
|
|
/**
|
|
* When true, this object will use center point as the origin of transformation
|
|
* when being scaled via the controls.
|
|
* <b>Backwards incompatibility note:</b> This property replaces "centerTransform" (Boolean).
|
|
* @since 1.3.4
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
centeredScaling: false,
|
|
|
|
/**
|
|
* When true, this object will use center point as the origin of transformation
|
|
* when being rotated via the controls.
|
|
* <b>Backwards incompatibility note:</b> This property replaces "centerTransform" (Boolean).
|
|
* @since 1.3.4
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
centeredRotation: true,
|
|
|
|
/**
|
|
* Color of object's fill
|
|
* @type String
|
|
* @default
|
|
*/
|
|
fill: 'rgb(0,0,0)',
|
|
|
|
/**
|
|
* Fill rule used to fill an object
|
|
* accepted values are nonzero, evenodd
|
|
* <b>Backwards incompatibility note:</b> This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead)
|
|
* @type String
|
|
* @default
|
|
*/
|
|
fillRule: 'nonzero',
|
|
|
|
/**
|
|
* Composite rule used for canvas globalCompositeOperation
|
|
* @type String
|
|
* @default
|
|
*/
|
|
globalCompositeOperation: 'source-over',
|
|
|
|
/**
|
|
* Background color of an object.
|
|
* @type String
|
|
* @default
|
|
*/
|
|
backgroundColor: '',
|
|
|
|
/**
|
|
* Selection Background color of an object. colored layer behind the object when it is active.
|
|
* does not mix good with globalCompositeOperation methods.
|
|
* @type String
|
|
* @default
|
|
*/
|
|
selectionBackgroundColor: '',
|
|
|
|
/**
|
|
* When defined, an object is rendered via stroke and this property specifies its color
|
|
* @type String
|
|
* @default
|
|
*/
|
|
stroke: null,
|
|
|
|
/**
|
|
* Width of a stroke used to render this object
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
strokeWidth: 1,
|
|
|
|
/**
|
|
* Array specifying dash pattern of an object's stroke (stroke must be defined)
|
|
* @type Array
|
|
*/
|
|
strokeDashArray: null,
|
|
|
|
/**
|
|
* Line endings style of an object's stroke (one of "butt", "round", "square")
|
|
* @type String
|
|
* @default
|
|
*/
|
|
strokeLineCap: 'butt',
|
|
|
|
/**
|
|
* Corner style of an object's stroke (one of "bevil", "round", "miter")
|
|
* @type String
|
|
* @default
|
|
*/
|
|
strokeLineJoin: 'miter',
|
|
|
|
/**
|
|
* Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
strokeMiterLimit: 10,
|
|
|
|
/**
|
|
* Shadow object representing shadow of this shape
|
|
* @type fabric.Shadow
|
|
* @default
|
|
*/
|
|
shadow: null,
|
|
|
|
/**
|
|
* Opacity of object's controlling borders when object is active and moving
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
borderOpacityWhenMoving: 0.4,
|
|
|
|
/**
|
|
* Scale factor of object's controlling borders
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
borderScaleFactor: 1,
|
|
|
|
/**
|
|
* Transform matrix (similar to SVG's transform matrix)
|
|
* @type Array
|
|
*/
|
|
transformMatrix: null,
|
|
|
|
/**
|
|
* Minimum allowed scale value of an object
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
minScaleLimit: 0.01,
|
|
|
|
/**
|
|
* When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection).
|
|
* But events still fire on it.
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
selectable: true,
|
|
|
|
/**
|
|
* When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
evented: true,
|
|
|
|
/**
|
|
* When set to `false`, an object is not rendered on canvas
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
visible: true,
|
|
|
|
/**
|
|
* When set to `false`, object's controls are not displayed and can not be used to manipulate object
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
hasControls: true,
|
|
|
|
/**
|
|
* When set to `false`, object's controlling borders are not rendered
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
hasBorders: true,
|
|
|
|
/**
|
|
* When set to `false`, object's controlling rotating point will not be visible or selectable
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
hasRotatingPoint: true,
|
|
|
|
/**
|
|
* Offset for object's controlling rotating point (when enabled via `hasRotatingPoint`)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
rotatingPointOffset: 40,
|
|
|
|
/**
|
|
* When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
perPixelTargetFind: false,
|
|
|
|
/**
|
|
* When `false`, default object's values are not included in its serialization
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
includeDefaultValues: true,
|
|
|
|
/**
|
|
* Function that determines clipping of an object (context is passed as a first argument)
|
|
* Note that context origin is at the object's center point (not left/top corner)
|
|
* @deprecated since 2.0.0
|
|
* @type Function
|
|
*/
|
|
clipTo: null,
|
|
|
|
/**
|
|
* When `true`, object horizontal movement is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockMovementX: false,
|
|
|
|
/**
|
|
* When `true`, object vertical movement is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockMovementY: false,
|
|
|
|
/**
|
|
* When `true`, object rotation is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockRotation: false,
|
|
|
|
/**
|
|
* When `true`, object horizontal scaling is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockScalingX: false,
|
|
|
|
/**
|
|
* When `true`, object vertical scaling is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockScalingY: false,
|
|
|
|
/**
|
|
* When `true`, object non-uniform scaling is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockUniScaling: false,
|
|
|
|
/**
|
|
* When `true`, object horizontal skewing is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockSkewingX: false,
|
|
|
|
/**
|
|
* When `true`, object vertical skewing is locked
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockSkewingY: false,
|
|
|
|
/**
|
|
* When `true`, object cannot be flipped by scaling into negative values
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
lockScalingFlip: false,
|
|
|
|
/**
|
|
* When `true`, object is not exported in SVG or OBJECT/JSON
|
|
* since 1.6.3
|
|
* @type Boolean
|
|
* @default
|
|
*/
|
|
excludeFromExport: false,
|
|
|
|
/**
|
|
* When `true`, object is cached on an additional canvas.
|
|
* default to true
|
|
* since 1.7.0
|
|
* @type Boolean
|
|
* @default true
|
|
*/
|
|
objectCaching: objectCaching,
|
|
|
|
/**
|
|
* When `true`, object properties are checked for cache invalidation. In some particular
|
|
* situation you may want this to be disabled ( spray brush, very big, groups)
|
|
* or if your application does not allow you to modify properties for groups child you want
|
|
* to disable it for groups.
|
|
* default to false
|
|
* since 1.7.0
|
|
* @type Boolean
|
|
* @default false
|
|
*/
|
|
statefullCache: false,
|
|
|
|
/**
|
|
* When `true`, cache does not get updated during scaling. The picture will get blocky if scaled
|
|
* too much and will be redrawn with correct details at the end of scaling.
|
|
* this setting is performance and application dependant.
|
|
* default to true
|
|
* since 1.7.0
|
|
* @type Boolean
|
|
* @default true
|
|
*/
|
|
noScaleCache: true,
|
|
|
|
/**
|
|
* When set to `true`, object's cache will be rerendered next render call.
|
|
* since 1.7.0
|
|
* @type Boolean
|
|
* @default true
|
|
*/
|
|
dirty: true,
|
|
|
|
/**
|
|
* keeps the value of the last hovered coner during mouse move.
|
|
* 0 is no corner, or 'mt', 'ml', 'mtr' etc..
|
|
* It should be private, but there is no harm in using it as
|
|
* a read-only property.
|
|
* @type number|string|any
|
|
* @default 0
|
|
*/
|
|
__corner: 0,
|
|
|
|
/**
|
|
* 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 originX originY transformMatrix ' +
|
|
'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' +
|
|
'angle opacity fill globalCompositeOperation shadow clipTo visible backgroundColor ' +
|
|
'skewX skewY fillRule'
|
|
).split(' '),
|
|
|
|
/**
|
|
* List of properties to consider when checking if cache needs refresh
|
|
* @type Array
|
|
*/
|
|
cacheProperties: (
|
|
'fill stroke strokeWidth strokeDashArray width height' +
|
|
' strokeLineCap strokeLineJoin strokeMiterLimit backgroundColor'
|
|
).split(' '),
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {Object} [options] Options object
|
|
*/
|
|
initialize: function(options) {
|
|
if (options) {
|
|
this.setOptions(options);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a the canvas used to keep the cached copy of the object
|
|
* @private
|
|
*/
|
|
_createCacheCanvas: function() {
|
|
this._cacheProperties = {};
|
|
this._cacheCanvas = fabric.document.createElement('canvas');
|
|
this._cacheContext = this._cacheCanvas.getContext('2d');
|
|
this._updateCacheCanvas();
|
|
},
|
|
|
|
/**
|
|
* Limit the cache dimensions so that X * Y do not cross fabric.perfLimitSizeTotal
|
|
* and each side do not cross fabric.cacheSideLimit
|
|
* those numbers are configurable so that you can get as much detail as you want
|
|
* making bargain with performances.
|
|
* @param {Object} dims
|
|
* @param {Object} dims.width width of canvas
|
|
* @param {Object} dims.height height of canvas
|
|
* @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache
|
|
* @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache
|
|
* @return {Object}.width width of canvas
|
|
* @return {Object}.height height of canvas
|
|
* @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache
|
|
* @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache
|
|
*/
|
|
_limitCacheSize: function(dims) {
|
|
var perfLimitSizeTotal = fabric.perfLimitSizeTotal,
|
|
maximumSide = fabric.cacheSideLimit,
|
|
width = dims.width, height = dims.height,
|
|
ar = width / height, limitedDims = fabric.util.limitDimsByArea(ar, perfLimitSizeTotal, maximumSide),
|
|
capValue = fabric.util.capValue, max = fabric.maxCacheSideLimit, min = fabric.minCacheSideLimit,
|
|
x = capValue(min, limitedDims.x, max),
|
|
y = capValue(min, limitedDims.y, max);
|
|
if (width > x) {
|
|
dims.zoomX /= width / x;
|
|
dims.width = x;
|
|
}
|
|
else if (width < min) {
|
|
dims.width = min;
|
|
}
|
|
if (height > y) {
|
|
dims.zoomY /= height / y;
|
|
dims.height = y;
|
|
}
|
|
else if (height < min) {
|
|
dims.height = min;
|
|
}
|
|
return dims;
|
|
},
|
|
|
|
/**
|
|
* Return the dimension and the zoom level needed to create a cache canvas
|
|
* big enough to host the object to be cached.
|
|
* @private
|
|
* @return {Object}.width width of canvas
|
|
* @return {Object}.height height of canvas
|
|
* @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache
|
|
* @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache
|
|
*/
|
|
_getCacheCanvasDimensions: function() {
|
|
var zoom = this.canvas && this.canvas.getZoom() || 1,
|
|
objectScale = this.getObjectScaling(),
|
|
dim = this._getNonTransformedDimensions(),
|
|
retina = this.canvas && this.canvas._isRetinaScaling() ? fabric.devicePixelRatio : 1,
|
|
zoomX = objectScale.scaleX * zoom * retina,
|
|
zoomY = objectScale.scaleY * zoom * retina,
|
|
width = dim.x * zoomX,
|
|
height = dim.y * zoomY;
|
|
return {
|
|
width: width + ALIASING_LIMIT,
|
|
height: height + ALIASING_LIMIT,
|
|
zoomX: zoomX,
|
|
zoomY: zoomY
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Update width and height of the canvas for cache
|
|
* returns true or false if canvas needed resize.
|
|
* @private
|
|
* @return {Boolean} true if the canvas has been resized
|
|
*/
|
|
_updateCacheCanvas: function() {
|
|
if (this.noScaleCache && this.canvas && this.canvas._currentTransform) {
|
|
var action = this.canvas._currentTransform.action;
|
|
if (action.slice && action.slice(0, 5) === 'scale') {
|
|
return false;
|
|
}
|
|
}
|
|
var dims = this._limitCacheSize(this._getCacheCanvasDimensions()),
|
|
minCacheSize = fabric.minCacheSideLimit,
|
|
width = dims.width, height = dims.height,
|
|
zoomX = dims.zoomX, zoomY = dims.zoomY,
|
|
dimensionsChanged = width !== this.cacheWidth || height !== this.cacheHeight,
|
|
zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY,
|
|
shouldRedraw = dimensionsChanged || zoomChanged,
|
|
additionalWidth = 0, additionalHeight = 0, shouldResizeCanvas = false;
|
|
if (dimensionsChanged) {
|
|
var canvasWidth = this._cacheCanvas.width,
|
|
canvasHeight = this._cacheCanvas.height,
|
|
sizeGrowing = width > canvasWidth || height > canvasHeight,
|
|
sizeShrinking = (width < canvasWidth * 0.9 || height < canvasHeight * 0.9) &&
|
|
canvasWidth > minCacheSize && canvasHeight > minCacheSize;
|
|
shouldResizeCanvas = sizeGrowing || sizeShrinking;
|
|
if (sizeGrowing) {
|
|
additionalWidth = (width * 0.1) & ~1;
|
|
additionalHeight = (height * 0.1) & ~1;
|
|
}
|
|
}
|
|
if (shouldRedraw) {
|
|
if (shouldResizeCanvas) {
|
|
this._cacheCanvas.width = Math.max(Math.ceil(width) + additionalWidth, minCacheSize);
|
|
this._cacheCanvas.height = Math.max(Math.ceil(height) + additionalHeight, minCacheSize);
|
|
this.cacheTranslationX = (width + additionalWidth) / 2;
|
|
this.cacheTranslationY = (height + additionalHeight) / 2;
|
|
}
|
|
else {
|
|
this._cacheContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
this._cacheContext.clearRect(0, 0, this._cacheCanvas.width, this._cacheCanvas.height);
|
|
}
|
|
this.cacheWidth = width;
|
|
this.cacheHeight = height;
|
|
this._cacheContext.translate(this.cacheTranslationX, this.cacheTranslationY);
|
|
this._cacheContext.scale(zoomX, zoomY);
|
|
this.zoomX = zoomX;
|
|
this.zoomY = zoomY;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Sets object's properties from options
|
|
* @param {Object} [options] Options object
|
|
*/
|
|
setOptions: function(options) {
|
|
this._setOptions(options);
|
|
this._initGradient(options.fill, 'fill');
|
|
this._initGradient(options.stroke, 'stroke');
|
|
this._initClipping(options);
|
|
this._initPattern(options.fill, 'fill');
|
|
this._initPattern(options.stroke, 'stroke');
|
|
},
|
|
|
|
/**
|
|
* Transforms context when rendering an object
|
|
* @param {CanvasRenderingContext2D} ctx Context
|
|
* @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node
|
|
*/
|
|
transform: function(ctx, fromLeft) {
|
|
if (this.group && !this.group._transformDone) {
|
|
this.group.transform(ctx);
|
|
}
|
|
var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint();
|
|
ctx.translate(center.x, center.y);
|
|
this.angle && ctx.rotate(degreesToRadians(this.angle));
|
|
ctx.scale(
|
|
this.scaleX * (this.flipX ? -1 : 1),
|
|
this.scaleY * (this.flipY ? -1 : 1)
|
|
);
|
|
this.skewX && ctx.transform(1, 0, Math.tan(degreesToRadians(this.skewX)), 1, 0, 0);
|
|
this.skewY && ctx.transform(1, Math.tan(degreesToRadians(this.skewY)), 0, 1, 0, 0);
|
|
},
|
|
|
|
/**
|
|
* Returns an object representation of an instance
|
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
* @return {Object} Object representation of an instance
|
|
*/
|
|
toObject: function(propertiesToInclude) {
|
|
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS,
|
|
|
|
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,
|
|
stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke,
|
|
strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS),
|
|
strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : this.strokeDashArray,
|
|
strokeLineCap: this.strokeLineCap,
|
|
strokeLineJoin: this.strokeLineJoin,
|
|
strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS),
|
|
scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS),
|
|
scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS),
|
|
angle: toFixed(this.angle, NUM_FRACTION_DIGITS),
|
|
flipX: this.flipX,
|
|
flipY: this.flipY,
|
|
opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS),
|
|
shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow,
|
|
visible: this.visible,
|
|
clipTo: this.clipTo && String(this.clipTo),
|
|
backgroundColor: this.backgroundColor,
|
|
fillRule: this.fillRule,
|
|
globalCompositeOperation: this.globalCompositeOperation,
|
|
transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : null,
|
|
skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS),
|
|
skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS)
|
|
};
|
|
|
|
fabric.util.populateWithProperties(this, object, propertiesToInclude);
|
|
if (!this.includeDefaultValues) {
|
|
object = this._removeDefaultValues(object);
|
|
}
|
|
|
|
return object;
|
|
},
|
|
|
|
/**
|
|
* Returns (dataless) object representation of an instance
|
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
* @return {Object} Object representation of an instance
|
|
*/
|
|
toDatalessObject: function(propertiesToInclude) {
|
|
// will be overwritten by subclasses
|
|
return this.toObject(propertiesToInclude);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Object} object
|
|
*/
|
|
_removeDefaultValues: function(object) {
|
|
var prototype = fabric.util.getKlass(object.type).prototype,
|
|
stateProperties = prototype.stateProperties;
|
|
stateProperties.forEach(function(prop) {
|
|
if (object[prop] === prototype[prop]) {
|
|
delete object[prop];
|
|
}
|
|
var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' &&
|
|
Object.prototype.toString.call(prototype[prop]) === '[object Array]';
|
|
|
|
// basically a check for [] === []
|
|
if (isArray && object[prop].length === 0 && prototype[prop].length === 0) {
|
|
delete object[prop];
|
|
}
|
|
});
|
|
|
|
return object;
|
|
},
|
|
|
|
/**
|
|
* Returns a string representation of an instance
|
|
* @return {String}
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.' + capitalize(this.type) + '>';
|
|
},
|
|
|
|
/**
|
|
* Return the object scale factor counting also the group scaling
|
|
* @return {Object} object with scaleX and scaleY properties
|
|
*/
|
|
getObjectScaling: function() {
|
|
var scaleX = this.scaleX, scaleY = this.scaleY;
|
|
if (this.group) {
|
|
var scaling = this.group.getObjectScaling();
|
|
scaleX *= scaling.scaleX;
|
|
scaleY *= scaling.scaleY;
|
|
}
|
|
return { scaleX: scaleX, scaleY: scaleY };
|
|
},
|
|
|
|
/**
|
|
* Return the object opacity counting also the group property
|
|
* @return {Object} object with scaleX and scaleY properties
|
|
*/
|
|
getObjectOpacity: function() {
|
|
var opacity = this.opacity;
|
|
if (this.group) {
|
|
opacity *= this.group.getObjectOpacity();
|
|
}
|
|
return opacity;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {String} key
|
|
* @param {*} value
|
|
* @return {fabric.Object} thisArg
|
|
*/
|
|
_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 === 'shadow' && value && !(value instanceof fabric.Shadow)) {
|
|
value = new fabric.Shadow(value);
|
|
}
|
|
else if (key === 'dirty' && this.group) {
|
|
this.group.set('dirty', value);
|
|
}
|
|
|
|
this[key] = value;
|
|
|
|
if (this.cacheProperties.indexOf(key) > -1) {
|
|
if (this.group) {
|
|
this.group.set('dirty', true);
|
|
}
|
|
this.dirty = true;
|
|
}
|
|
|
|
if (this.group && this.stateProperties.indexOf(key) > -1 && this.group.isOnACache()) {
|
|
this.group.set('dirty', true);
|
|
}
|
|
|
|
if (key === 'width' || key === 'height') {
|
|
this.minScaleLimit = Math.min(0.1, 1 / Math.max(this.width, this.height));
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* This callback function is called by the parent group of an object every
|
|
* time a non-delegated property changes on the group. It is passed the key
|
|
* and value as parameters. Not adding in this function's signature to avoid
|
|
* Travis build error about unused variables.
|
|
*/
|
|
setOnGroup: function() {
|
|
// implemented by sub-classes, as needed.
|
|
},
|
|
|
|
/**
|
|
* This callback function is called every time _discardActiveObject or _setActiveObject
|
|
* try to to deselect this object. If the function returns true, the process is cancelled
|
|
* @param {Object} [options] options sent from the upper functions
|
|
* @param {Event} [options.e] event if the process is generated by an event
|
|
*/
|
|
onDeselect: function() {
|
|
// implemented by sub-classes, as needed.
|
|
},
|
|
|
|
|
|
/**
|
|
* This callback function is called every time _discardActiveObject or _setActiveObject
|
|
* try to to select this object. If the function returns true, the process is cancelled
|
|
* @param {Object} [options] options sent from the upper functions
|
|
* @param {Event} [options.e] event if the process is generated by an event
|
|
*/
|
|
onSelect: function() {
|
|
// implemented by sub-classes, as needed.
|
|
},
|
|
|
|
|
|
/**
|
|
* Retrieves viewportTransform from Object's canvas if possible
|
|
* @method getViewportTransform
|
|
* @memberOf fabric.Object.prototype
|
|
* @return {Boolean}
|
|
*/
|
|
getViewportTransform: function() {
|
|
if (this.canvas && this.canvas.viewportTransform) {
|
|
return this.canvas.viewportTransform;
|
|
}
|
|
return fabric.iMatrix.concat();
|
|
},
|
|
|
|
/*
|
|
* @private
|
|
* return if the object would be visible in rendering
|
|
* @memberOf fabric.Object.prototype
|
|
* @return {Boolean}
|
|
*/
|
|
isNotVisible: function() {
|
|
return this.opacity === 0 || (this.width === 0 && this.height === 0) || !this.visible;
|
|
},
|
|
|
|
/**
|
|
* Renders an object on a specified context
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
render: function(ctx) {
|
|
// do not render if width/height are zeros or object is not visible
|
|
if (this.isNotVisible()) {
|
|
return;
|
|
}
|
|
if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) {
|
|
return;
|
|
}
|
|
ctx.save();
|
|
this._setupCompositeOperation(ctx);
|
|
this.drawSelectionBackground(ctx);
|
|
this.transform(ctx);
|
|
this._setOpacity(ctx);
|
|
this._setShadow(ctx, this);
|
|
if (this.transformMatrix) {
|
|
ctx.transform.apply(ctx, this.transformMatrix);
|
|
}
|
|
this.clipTo && fabric.util.clipContext(this, ctx);
|
|
if (this.shouldCache()) {
|
|
if (!this._cacheCanvas) {
|
|
this._createCacheCanvas();
|
|
}
|
|
if (this.isCacheDirty()) {
|
|
this.statefullCache && this.saveState({ propertySet: 'cacheProperties' });
|
|
this.drawObject(this._cacheContext);
|
|
this.dirty = false;
|
|
}
|
|
this.drawCacheOnCanvas(ctx);
|
|
}
|
|
else {
|
|
this.dirty = false;
|
|
this.drawObject(ctx);
|
|
if (this.objectCaching && this.statefullCache) {
|
|
this.saveState({ propertySet: 'cacheProperties' });
|
|
}
|
|
}
|
|
this.clipTo && ctx.restore();
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* When set to `true`, force the object to have its own cache, even if it is inside a group
|
|
* it may be needed when your object behave in a particular way on the cache and always needs
|
|
* its own isolated canvas to render correctly.
|
|
* Created to be overridden
|
|
* since 1.7.12
|
|
* @returns false
|
|
*/
|
|
needsItsOwnCache: function() {
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Decide if the object should cache or not. Create its own cache level
|
|
* objectCaching is a global flag, wins over everything
|
|
* needsItsOwnCache should be used when the object drawing method requires
|
|
* a cache step. None of the fabric classes requires it.
|
|
* Generally you do not cache objects in groups because the group outside is cached.
|
|
* @return {Boolean}
|
|
*/
|
|
shouldCache: function() {
|
|
this.ownCaching = this.objectCaching &&
|
|
(!this.group || this.needsItsOwnCache() || !this.group.isOnACache());
|
|
return this.ownCaching;
|
|
},
|
|
|
|
/**
|
|
* Check if this object or a child object will cast a shadow
|
|
* used by Group.shouldCache to know if child has a shadow recursively
|
|
* @return {Boolean}
|
|
*/
|
|
willDrawShadow: function() {
|
|
return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0);
|
|
},
|
|
|
|
/**
|
|
* Execute the drawing operation for an object on a specified context
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
drawObject: function(ctx) {
|
|
this._renderBackground(ctx);
|
|
this._setStrokeStyles(ctx, this);
|
|
this._setFillStyles(ctx, this);
|
|
this._render(ctx);
|
|
},
|
|
|
|
/**
|
|
* Paint the cached copy of the object on the target context.
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
drawCacheOnCanvas: function(ctx) {
|
|
ctx.scale(1 / this.zoomX, 1 / this.zoomY);
|
|
ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY);
|
|
},
|
|
|
|
/**
|
|
* Check if cache is dirty
|
|
* @param {Boolean} skipCanvas skip canvas checks because this object is painted
|
|
* on parent canvas.
|
|
*/
|
|
isCacheDirty: function(skipCanvas) {
|
|
if (this.isNotVisible()) {
|
|
return false;
|
|
}
|
|
if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) {
|
|
// in this case the context is already cleared.
|
|
return true;
|
|
}
|
|
else {
|
|
if (this.dirty || (this.statefullCache && this.hasStateChanged('cacheProperties'))) {
|
|
if (this._cacheCanvas && !skipCanvas) {
|
|
var width = this.cacheWidth / this.zoomX;
|
|
var height = this.cacheHeight / this.zoomY;
|
|
this._cacheContext.clearRect(-width / 2, -height / 2, width, height);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Draws a background for the object big as its untrasformed dimensions
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderBackground: function(ctx) {
|
|
if (!this.backgroundColor) {
|
|
return;
|
|
}
|
|
var dim = this._getNonTransformedDimensions();
|
|
ctx.fillStyle = this.backgroundColor;
|
|
|
|
ctx.fillRect(
|
|
-dim.x / 2,
|
|
-dim.y / 2,
|
|
dim.x,
|
|
dim.y
|
|
);
|
|
// if there is background color no other shadows
|
|
// should be casted
|
|
this._removeShadow(ctx);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_setOpacity: function(ctx) {
|
|
if (this.group && !this.group.transformDone) {
|
|
ctx.globalAlpha = this.getObjectOpacity();
|
|
}
|
|
else {
|
|
ctx.globalAlpha *= this.opacity;
|
|
}
|
|
},
|
|
|
|
_setStrokeStyles: function(ctx, decl) {
|
|
if (decl.stroke) {
|
|
ctx.lineWidth = decl.strokeWidth;
|
|
ctx.lineCap = decl.strokeLineCap;
|
|
ctx.lineJoin = decl.strokeLineJoin;
|
|
ctx.miterLimit = decl.strokeMiterLimit;
|
|
ctx.strokeStyle = decl.stroke.toLive
|
|
? decl.stroke.toLive(ctx, this)
|
|
: decl.stroke;
|
|
}
|
|
},
|
|
|
|
_setFillStyles: function(ctx, decl) {
|
|
if (decl.fill) {
|
|
ctx.fillStyle = decl.fill.toLive
|
|
? decl.fill.toLive(ctx, this)
|
|
: decl.fill;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* Sets line dash
|
|
* @param {CanvasRenderingContext2D} ctx Context to set the dash line on
|
|
* @param {Array} dashArray array representing dashes
|
|
* @param {Function} alternative function to call if browaser does not support lineDash
|
|
*/
|
|
_setLineDash: function(ctx, dashArray, alternative) {
|
|
if (!dashArray) {
|
|
return;
|
|
}
|
|
// Spec requires the concatenation of two copies the dash list when the number of elements is odd
|
|
if (1 & dashArray.length) {
|
|
dashArray.push.apply(dashArray, dashArray);
|
|
}
|
|
if (supportsLineDash) {
|
|
ctx.setLineDash(dashArray);
|
|
}
|
|
else {
|
|
alternative && alternative(ctx);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Renders controls and borders for the object
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {Object} [styleOverride] properties to override the object style
|
|
*/
|
|
_renderControls: function(ctx, styleOverride) {
|
|
var vpt = this.getViewportTransform(),
|
|
matrix = this.calcTransformMatrix(),
|
|
options, drawBorders, drawControls;
|
|
styleOverride = styleOverride || { };
|
|
drawBorders = typeof styleOverride.hasBorders !== 'undefined' ? styleOverride.hasBorders : this.hasBorders;
|
|
drawControls = typeof styleOverride.hasControls !== 'undefined' ? styleOverride.hasControls : this.hasControls;
|
|
matrix = fabric.util.multiplyTransformMatrices(vpt, matrix);
|
|
options = fabric.util.qrDecompose(matrix);
|
|
ctx.save();
|
|
ctx.translate(options.translateX, options.translateY);
|
|
ctx.lineWidth = 1 * this.borderScaleFactor;
|
|
if (!this.group) {
|
|
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
}
|
|
if (styleOverride.forActiveSelection) {
|
|
ctx.rotate(degreesToRadians(options.angle));
|
|
drawBorders && this.drawBordersInGroup(ctx, options, styleOverride);
|
|
}
|
|
else {
|
|
ctx.rotate(degreesToRadians(this.angle));
|
|
drawBorders && this.drawBorders(ctx, styleOverride);
|
|
}
|
|
drawControls && this.drawControls(ctx, styleOverride);
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_setShadow: function(ctx) {
|
|
if (!this.shadow) {
|
|
return;
|
|
}
|
|
|
|
var multX = (this.canvas && this.canvas.viewportTransform[0]) || 1,
|
|
multY = (this.canvas && this.canvas.viewportTransform[3]) || 1,
|
|
scaling = this.getObjectScaling();
|
|
if (this.canvas && this.canvas._isRetinaScaling()) {
|
|
multX *= fabric.devicePixelRatio;
|
|
multY *= fabric.devicePixelRatio;
|
|
}
|
|
ctx.shadowColor = this.shadow.color;
|
|
ctx.shadowBlur = this.shadow.blur * (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4;
|
|
ctx.shadowOffsetX = this.shadow.offsetX * multX * scaling.scaleX;
|
|
ctx.shadowOffsetY = this.shadow.offsetY * multY * scaling.scaleY;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_removeShadow: function(ctx) {
|
|
if (!this.shadow) {
|
|
return;
|
|
}
|
|
|
|
ctx.shadowColor = '';
|
|
ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {Object} filler fabric.Pattern or fabric.Gradient
|
|
*/
|
|
_applyPatternGradientTransform: function(ctx, filler) {
|
|
if (!filler.toLive) {
|
|
return;
|
|
}
|
|
var transform = filler.gradientTransform || filler.patternTransform;
|
|
var offsetX = -this.width / 2 + filler.offsetX || 0,
|
|
offsetY = -this.height / 2 + filler.offsetY || 0;
|
|
ctx.translate(offsetX, offsetY);
|
|
if (transform) {
|
|
ctx.transform.apply(ctx, transform);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderFill: function(ctx) {
|
|
if (!this.fill) {
|
|
return;
|
|
}
|
|
|
|
ctx.save();
|
|
this._applyPatternGradientTransform(ctx, this.fill);
|
|
if (this.fillRule === 'evenodd') {
|
|
ctx.fill('evenodd');
|
|
}
|
|
else {
|
|
ctx.fill();
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
_renderStroke: function(ctx) {
|
|
if (!this.stroke || this.strokeWidth === 0) {
|
|
return;
|
|
}
|
|
|
|
if (this.shadow && !this.shadow.affectStroke) {
|
|
this._removeShadow(ctx);
|
|
}
|
|
|
|
ctx.save();
|
|
this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke);
|
|
this._applyPatternGradientTransform(ctx, this.stroke);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* This function is an helper for svg import. it returns the center of the object in the svg
|
|
* untransformed coordinates
|
|
* @private
|
|
* @return {Object} center point from element coordinates
|
|
*/
|
|
_findCenterFromElement: function() {
|
|
return { x: this.left + this.width / 2, y: this.top + this.height / 2 };
|
|
},
|
|
|
|
/**
|
|
* This function is an helper for svg import. it removes the transform matrix
|
|
* and set to object properties that fabricjs can handle
|
|
* untransformed coordinates
|
|
* @private
|
|
* @chainable
|
|
* @return {thisArg}
|
|
*/
|
|
_removeTransformMatrix: function() {
|
|
var center = this._findCenterFromElement();
|
|
if (this.transformMatrix) {
|
|
var options = fabric.util.qrDecompose(this.transformMatrix);
|
|
this.flipX = false;
|
|
this.flipY = false;
|
|
this.set('scaleX', options.scaleX);
|
|
this.set('scaleY', options.scaleY);
|
|
this.angle = options.angle;
|
|
this.skewX = options.skewX;
|
|
this.skewY = 0;
|
|
center = fabric.util.transformPoint(center, this.transformMatrix);
|
|
}
|
|
this.transformMatrix = null;
|
|
this.setPositionByOrigin(center, 'center', 'center');
|
|
},
|
|
|
|
/**
|
|
* Clones an instance, using a callback method will work for every object.
|
|
* @param {Function} callback Callback is invoked with a clone as a first argument
|
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
*/
|
|
clone: function(callback, propertiesToInclude) {
|
|
var objectForm = this.toObject(propertiesToInclude);
|
|
if (this.constructor.fromObject) {
|
|
this.constructor.fromObject(objectForm, callback);
|
|
}
|
|
else {
|
|
fabric.Object._fromObject('Object', objectForm, callback);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Creates an instance of fabric.Image out of an object
|
|
* @param {Function} callback callback, invoked with an instance as a first argument
|
|
* @param {Object} [options] for clone as image, passed to toDataURL
|
|
* @param {Boolean} [options.enableRetinaScaling] enable retina scaling for the cloned image
|
|
* @return {fabric.Object} thisArg
|
|
*/
|
|
cloneAsImage: function(callback, options) {
|
|
var dataUrl = this.toDataURL(options);
|
|
fabric.util.loadImage(dataUrl, function(img) {
|
|
if (callback) {
|
|
callback(new fabric.Image(img));
|
|
}
|
|
});
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Converts an object into a data-url-like string
|
|
* @param {Object} options Options object
|
|
* @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png"
|
|
* @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg.
|
|
* @param {Number} [options.multiplier=1] Multiplier to scale by
|
|
* @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14
|
|
* @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14
|
|
* @param {Number} [options.width] Cropping width. Introduced in v1.2.14
|
|
* @param {Number} [options.height] Cropping height. Introduced in v1.2.14
|
|
* @param {Boolean} [options.enableRetina] Enable retina scaling for clone image. Introduce in 1.6.4
|
|
* @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format
|
|
*/
|
|
toDataURL: function(options) {
|
|
options || (options = { });
|
|
|
|
var el = fabric.util.createCanvasElement(),
|
|
boundingRect = this.getBoundingRect();
|
|
|
|
el.width = boundingRect.width;
|
|
el.height = boundingRect.height;
|
|
fabric.util.wrapElement(el, 'div');
|
|
var canvas = new fabric.StaticCanvas(el, { enableRetinaScaling: options.enableRetinaScaling });
|
|
// to avoid common confusion https://github.com/kangax/fabric.js/issues/806
|
|
if (options.format === 'jpg') {
|
|
options.format = 'jpeg';
|
|
}
|
|
|
|
if (options.format === 'jpeg') {
|
|
canvas.backgroundColor = '#fff';
|
|
}
|
|
|
|
var origParams = {
|
|
active: this.active,
|
|
left: this.left,
|
|
top: this.top
|
|
};
|
|
|
|
this.set('active', false);
|
|
this.setPositionByOrigin(new fabric.Point(canvas.width / 2, canvas.height / 2), 'center', 'center');
|
|
|
|
var originalCanvas = this.canvas;
|
|
canvas.add(this);
|
|
var data = canvas.toDataURL(options);
|
|
|
|
this.set(origParams).setCoords();
|
|
this.canvas = originalCanvas;
|
|
|
|
canvas.dispose();
|
|
canvas = null;
|
|
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* Returns true if specified type is identical to the type of an instance
|
|
* @param {String} type Type to check against
|
|
* @return {Boolean}
|
|
*/
|
|
isType: function(type) {
|
|
return this.type === type;
|
|
},
|
|
|
|
/**
|
|
* Returns complexity of an instance
|
|
* @return {Number} complexity of this instance (is 1 unless subclassed)
|
|
*/
|
|
complexity: function() {
|
|
return 1;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* <b>Backwards incompatibility note:</b> This method was named "setGradientFill" until v1.1.0
|
|
* @param {String} property Property name 'stroke' or 'fill'
|
|
* @param {Object} [options] Options object
|
|
* @param {String} [options.type] Type of gradient 'radial' or 'linear'
|
|
* @param {Number} [options.x1=0] x-coordinate of start point
|
|
* @param {Number} [options.y1=0] y-coordinate of start point
|
|
* @param {Number} [options.x2=0] x-coordinate of end point
|
|
* @param {Number} [options.y2=0] y-coordinate of end point
|
|
* @param {Number} [options.r1=0] Radius of start point (only for radial gradients)
|
|
* @param {Number} [options.r2=0] Radius of end point (only for radial gradients)
|
|
* @param {Object} [options.colorStops] Color stops object eg. {0: 'ff0000', 1: '000000'}
|
|
* @param {Object} [options.gradientTransform] transforMatrix for gradient
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
* @see {@link http://jsfiddle.net/fabricjs/58y8b/|jsFiddle demo}
|
|
* @example <caption>Set linear gradient</caption>
|
|
* object.setGradient('fill', {
|
|
* type: 'linear',
|
|
* x1: -object.width / 2,
|
|
* y1: 0,
|
|
* x2: object.width / 2,
|
|
* y2: 0,
|
|
* colorStops: {
|
|
* 0: 'red',
|
|
* 0.5: '#005555',
|
|
* 1: 'rgba(0,0,255,0.5)'
|
|
* }
|
|
* });
|
|
* canvas.renderAll();
|
|
* @example <caption>Set radial gradient</caption>
|
|
* object.setGradient('fill', {
|
|
* type: 'radial',
|
|
* x1: 0,
|
|
* y1: 0,
|
|
* x2: 0,
|
|
* y2: 0,
|
|
* r1: object.width / 2,
|
|
* r2: 10,
|
|
* colorStops: {
|
|
* 0: 'red',
|
|
* 0.5: '#005555',
|
|
* 1: 'rgba(0,0,255,0.5)'
|
|
* }
|
|
* });
|
|
* canvas.renderAll();
|
|
*/
|
|
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;
|
|
}
|
|
|
|
gradient.gradientTransform = options.gradientTransform;
|
|
fabric.Gradient.prototype.addColorStop.call(gradient, options.colorStops);
|
|
|
|
return this.set(property, fabric.Gradient.forObject(this, gradient));
|
|
},
|
|
|
|
/**
|
|
* Sets pattern fill of an object
|
|
* @param {Object} options Options object
|
|
* @param {(String|HTMLImageElement)} options.source Pattern source
|
|
* @param {String} [options.repeat=repeat] Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat)
|
|
* @param {Number} [options.offsetX=0] Pattern horizontal offset from object's left/top corner
|
|
* @param {Number} [options.offsetY=0] Pattern vertical offset from object's left/top corner
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
* @see {@link http://jsfiddle.net/fabricjs/QT3pa/|jsFiddle demo}
|
|
* @example <caption>Set pattern</caption>
|
|
* fabric.util.loadImage('http://fabricjs.com/assets/escheresque_ste.png', function(img) {
|
|
* object.setPatternFill({
|
|
* source: img,
|
|
* repeat: 'repeat'
|
|
* });
|
|
* canvas.renderAll();
|
|
* });
|
|
*/
|
|
setPatternFill: function(options) {
|
|
return this.set('fill', new fabric.Pattern(options));
|
|
},
|
|
|
|
/**
|
|
* Sets {@link fabric.Object#shadow|shadow} of an object
|
|
* @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)")
|
|
* @param {String} [options.color=rgb(0,0,0)] Shadow color
|
|
* @param {Number} [options.blur=0] Shadow blur
|
|
* @param {Number} [options.offsetX=0] Shadow horizontal offset
|
|
* @param {Number} [options.offsetY=0] Shadow vertical offset
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
* @see {@link http://jsfiddle.net/fabricjs/7gvJG/|jsFiddle demo}
|
|
* @example <caption>Set shadow with string notation</caption>
|
|
* object.setShadow('2px 2px 10px rgba(0,0,0,0.2)');
|
|
* canvas.renderAll();
|
|
* @example <caption>Set shadow with object notation</caption>
|
|
* object.setShadow({
|
|
* color: 'red',
|
|
* blur: 10,
|
|
* offsetX: 20,
|
|
* offsetY: 20
|
|
* });
|
|
* canvas.renderAll();
|
|
*/
|
|
setShadow: function(options) {
|
|
return this.set('shadow', options ? new fabric.Shadow(options) : null);
|
|
},
|
|
|
|
/**
|
|
* Sets "color" of an instance (alias of `set('fill', …)`)
|
|
* @param {String} color Color value
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
setColor: function(color) {
|
|
this.set('fill', color);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets "angle" of an instance
|
|
* @param {Number} angle Angle value (in degrees)
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
setAngle: function(angle) {
|
|
var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation;
|
|
|
|
if (shouldCenterOrigin) {
|
|
this._setOriginToCenter();
|
|
}
|
|
|
|
this.set('angle', angle);
|
|
|
|
if (shouldCenterOrigin) {
|
|
this._resetOrigin();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Centers object horizontally on canvas to which it was added last.
|
|
* You might need to call `setCoords` on an object after centering, to update controls area.
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
centerH: function () {
|
|
this.canvas && this.canvas.centerObjectH(this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Centers object horizontally on current viewport of canvas to which it was added last.
|
|
* You might need to call `setCoords` on an object after centering, to update controls area.
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
viewportCenterH: function () {
|
|
this.canvas && this.canvas.viewportCenterObjectH(this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Centers object vertically on canvas to which it was added last.
|
|
* You might need to call `setCoords` on an object after centering, to update controls area.
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
centerV: function () {
|
|
this.canvas && this.canvas.centerObjectV(this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Centers object vertically on current viewport of canvas to which it was added last.
|
|
* You might need to call `setCoords` on an object after centering, to update controls area.
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
viewportCenterV: function () {
|
|
this.canvas && this.canvas.viewportCenterObjectV(this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Centers object vertically and horizontally on canvas to which is was added last
|
|
* You might need to call `setCoords` on an object after centering, to update controls area.
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
center: function () {
|
|
this.canvas && this.canvas.centerObject(this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Centers object on current viewport of canvas to which it was added last.
|
|
* You might need to call `setCoords` on an object after centering, to update controls area.
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
viewportCenter: function () {
|
|
this.canvas && this.canvas.viewportCenterObject(this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Removes object from canvas to which it was added last
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
remove: function() {
|
|
this.canvas && this.canvas.remove(this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns coordinates of a pointer relative to an object
|
|
* @param {Event} e Event to operate upon
|
|
* @param {Object} [pointer] Pointer to operate upon (instead of event)
|
|
* @return {Object} Coordinates of a pointer (x, y)
|
|
*/
|
|
getLocalPointer: function(e, pointer) {
|
|
pointer = pointer || this.canvas.getPointer(e);
|
|
var pClicked = new fabric.Point(pointer.x, pointer.y),
|
|
objectLeftTop = this._getLeftTopCoords();
|
|
if (this.angle) {
|
|
pClicked = fabric.util.rotatePoint(
|
|
pClicked, objectLeftTop, degreesToRadians(-this.angle));
|
|
}
|
|
return {
|
|
x: pClicked.x - objectLeftTop.x,
|
|
y: pClicked.y - objectLeftTop.y
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Sets canvas globalCompositeOperation for specific object
|
|
* custom composition operation for the particular object can be specifed using globalCompositeOperation property
|
|
* @param {CanvasRenderingContext2D} ctx Rendering canvas context
|
|
*/
|
|
_setupCompositeOperation: function (ctx) {
|
|
if (this.globalCompositeOperation) {
|
|
ctx.globalCompositeOperation = this.globalCompositeOperation;
|
|
}
|
|
}
|
|
});
|
|
|
|
fabric.util.createAccessors && fabric.util.createAccessors(fabric.Object);
|
|
|
|
/**
|
|
* Alias for {@link fabric.Object.prototype.setAngle}
|
|
* @alias rotate -> setAngle
|
|
* @memberOf fabric.Object
|
|
*/
|
|
fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle;
|
|
|
|
extend(fabric.Object.prototype, fabric.Observable);
|
|
|
|
/**
|
|
* Defines the number of fraction digits to use when serializing object values.
|
|
* You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc.
|
|
* @static
|
|
* @memberOf fabric.Object
|
|
* @constant
|
|
* @type Number
|
|
*/
|
|
fabric.Object.NUM_FRACTION_DIGITS = 2;
|
|
|
|
fabric.Object._fromObject = function(className, object, callback, extraParam) {
|
|
var klass = fabric[className];
|
|
object = clone(object, true);
|
|
fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) {
|
|
if (typeof patterns[0] !== 'undefined') {
|
|
object.fill = patterns[0];
|
|
}
|
|
if (typeof patterns[1] !== 'undefined') {
|
|
object.stroke = patterns[1];
|
|
}
|
|
var instance = extraParam ? new klass(object[extraParam], object) : new klass(object);
|
|
callback && callback(instance);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Unique id used internally when creating SVG elements
|
|
* @static
|
|
* @memberOf fabric.Object
|
|
* @type Number
|
|
*/
|
|
fabric.Object.__uid = 0;
|
|
|
|
})(typeof exports !== 'undefined' ? exports : this);
|