(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. * Backwards incompatibility note: 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. * Backwards incompatibility note: 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 * Backwards incompatibility note: 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 '#'; }, /** * 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 * Backwards incompatibility note: 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 Set linear gradient * 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 Set radial gradient * 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 Set pattern * 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 Set shadow with string notation * object.setShadow('2px 2px 10px rgba(0,0,0,0.2)'); * canvas.renderAll(); * @example Set shadow with object notation * 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);