(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; 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 */ fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ { /** * Retrieves object's {@link fabric.Object#clipTo|clipping function} * @method getClipTo * @memberOf fabric.Object.prototype * @return {Function} */ /** * Sets object's {@link fabric.Object#clipTo|clipping function} * @method setClipTo * @memberOf fabric.Object.prototype * @param {Function} clipTo Clipping function * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#transformMatrix|transformMatrix} * @method getTransformMatrix * @memberOf fabric.Object.prototype * @return {Array} transformMatrix */ /** * Sets object's {@link fabric.Object#transformMatrix|transformMatrix} * @method setTransformMatrix * @memberOf fabric.Object.prototype * @param {Array} transformMatrix * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#visible|visible} state * @method getVisible * @memberOf fabric.Object.prototype * @return {Boolean} True if visible */ /** * Sets object's {@link fabric.Object#visible|visible} state * @method setVisible * @memberOf fabric.Object.prototype * @param {Boolean} value visible value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#shadow|shadow} * @method getShadow * @memberOf fabric.Object.prototype * @return {Object} Shadow instance */ /** * Retrieves object's {@link fabric.Object#stroke|stroke} * @method getStroke * @memberOf fabric.Object.prototype * @return {String} stroke value */ /** * Sets object's {@link fabric.Object#stroke|stroke} * @method setStroke * @memberOf fabric.Object.prototype * @param {String} value stroke value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#strokeWidth|strokeWidth} * @method getStrokeWidth * @memberOf fabric.Object.prototype * @return {Number} strokeWidth value */ /** * Sets object's {@link fabric.Object#strokeWidth|strokeWidth} * @method setStrokeWidth * @memberOf fabric.Object.prototype * @param {Number} value strokeWidth value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#originX|originX} * @method getOriginX * @memberOf fabric.Object.prototype * @return {String} originX value */ /** * Sets object's {@link fabric.Object#originX|originX} * @method setOriginX * @memberOf fabric.Object.prototype * @param {String} value originX value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#originY|originY} * @method getOriginY * @memberOf fabric.Object.prototype * @return {String} originY value */ /** * Sets object's {@link fabric.Object#originY|originY} * @method setOriginY * @memberOf fabric.Object.prototype * @param {String} value originY value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#fill|fill} * @method getFill * @memberOf fabric.Object.prototype * @return {String} Fill value */ /** * Sets object's {@link fabric.Object#fill|fill} * @method setFill * @memberOf fabric.Object.prototype * @param {String} value Fill value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#opacity|opacity} * @method getOpacity * @memberOf fabric.Object.prototype * @return {Number} Opacity value (0-1) */ /** * Sets object's {@link fabric.Object#opacity|opacity} * @method setOpacity * @memberOf fabric.Object.prototype * @param {Number} value Opacity value (0-1) * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#angle|angle} (in degrees) * @method getAngle * @memberOf fabric.Object.prototype * @return {Number} */ /** * Retrieves object's {@link fabric.Object#top|top position} * @method getTop * @memberOf fabric.Object.prototype * @return {Number} Top value (in pixels) */ /** * Sets object's {@link fabric.Object#top|top position} * @method setTop * @memberOf fabric.Object.prototype * @param {Number} value Top value (in pixels) * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#left|left position} * @method getLeft * @memberOf fabric.Object.prototype * @return {Number} Left value (in pixels) */ /** * Sets object's {@link fabric.Object#left|left position} * @method setLeft * @memberOf fabric.Object.prototype * @param {Number} value Left value (in pixels) * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#scaleX|scaleX} value * @method getScaleX * @memberOf fabric.Object.prototype * @return {Number} scaleX value */ /** * Sets object's {@link fabric.Object#scaleX|scaleX} value * @method setScaleX * @memberOf fabric.Object.prototype * @param {Number} value scaleX value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#scaleY|scaleY} value * @method getScaleY * @memberOf fabric.Object.prototype * @return {Number} scaleY value */ /** * Sets object's {@link fabric.Object#scaleY|scaleY} value * @method setScaleY * @memberOf fabric.Object.prototype * @param {Number} value scaleY value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#flipX|flipX} value * @method getFlipX * @memberOf fabric.Object.prototype * @return {Boolean} flipX value */ /** * Sets object's {@link fabric.Object#flipX|flipX} value * @method setFlipX * @memberOf fabric.Object.prototype * @param {Boolean} value flipX value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#flipY|flipY} value * @method getFlipY * @memberOf fabric.Object.prototype * @return {Boolean} flipY value */ /** * Sets object's {@link fabric.Object#flipY|flipY} value * @method setFlipY * @memberOf fabric.Object.prototype * @param {Boolean} value flipY value * @return {fabric.Object} thisArg * @chainable */ /** * 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/40/ 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/40/ 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) * @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 pathgroups, 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, /** * 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. * since 1.7.5 * @type Boolean * @default false */ needsItsOwnCache: false, /** * List of properties to consider when checking if state * of an object is changed (fabric.Object#hasStateChanged) * as well as for history (undo/redo) purposes * @type Array */ stateProperties: ( 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + 'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' + 'angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor ' + 'skewX skewY' ).split(' '), /** * List of properties to consider when checking if cache needs refresh * @type Array */ cacheProperties: ( 'fill stroke strokeWidth strokeDashArray width height stroke strokeWidth strokeDashArray' + ' strokeLineCap strokeLineJoin strokeMiterLimit fillRule backgroundColor' ).split(' '), /** * Constructor * @param {Object} [options] Options object */ initialize: function(options) { options = options || { }; if (options) { this.setOptions(options); } if (this.objectCaching) { this._createCacheCanvas(); } }, /** * 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(); }, /** * 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 + 2, height: height + 2, 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(0, 5) === 'scale') { return false; } } var dims = this._getCacheCanvasDimensions(), width = dims.width, height = dims.height, zoomX = dims.zoomX, zoomY = dims.zoomY; if (width !== this.cacheWidth || height !== this.cacheHeight) { this._cacheCanvas.width = Math.ceil(width); this._cacheCanvas.height = Math.ceil(height); this._cacheContext.translate(width / 2, height / 2); this._cacheContext.scale(zoomX, zoomY); this.cacheWidth = width; this.cacheHeight = height; 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 === this.canvas._activeGroup) { 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.getAngle(), 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 }; }, /** * @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.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. }, /** * Sets sourcePath of an object * @param {String} value Value to set sourcePath to * @return {fabric.Object} thisArg * @chainable */ setSourcePath: function(value) { this.sourcePath = value; return this; }, /** * Retrieves viewportTransform from Object's canvas if possible * @method getViewportTransform * @memberOf fabric.Object.prototype * @return {Boolean} flipY value // TODO */ getViewportTransform: function() { if (this.canvas && this.canvas.viewportTransform) { return this.canvas.viewportTransform; } return fabric.iMatrix.concat(); }, /** * Renders an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ render: function(ctx, noTransform) { // do not render if width/height are zeros or object is not visible if ((this.width === 0 && this.height === 0) || !this.visible) { return; } if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { return; } ctx.save(); //setup fill rule for current object this._setupCompositeOperation(ctx); this.drawSelectionBackground(ctx); if (!noTransform) { this.transform(ctx); } this._setOpacity(ctx); this._setShadow(ctx); 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(noTransform)) { this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); this.drawObject(this._cacheContext, noTransform); this.dirty = false; } this.drawCacheOnCanvas(ctx); } else { this.drawObject(ctx, noTransform); if (noTransform && this.objectCaching && this.statefullCache) { this.saveState({ propertySet: 'cacheProperties' }); } } this.clipTo && ctx.restore(); ctx.restore(); }, /** * Decide if the object should cache or not. * 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() { return this.objectCaching && (!this.group || this.needsItsOwnCache || !this.group.isCaching()); }, /** * 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; }, /** * Execute the drawing operation for an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ drawObject: function(ctx, noTransform) { this._renderBackground(ctx); this._setStrokeStyles(ctx); this._setFillStyles(ctx); this._render(ctx, noTransform); }, /** * 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.cacheWidth / 2, -this.cacheHeight / 2); }, /** * Check if cache is dirty * @param {Boolean} skipCanvas skip canvas checks because this object is painted * on parent canvas. */ isCacheDirty: function(skipCanvas) { if (!skipCanvas && this._updateCacheCanvas()) { // in this case the context is already cleared. return true; } else { if (this.dirty || (this.statefullCache && this.hasStateChanged('cacheProperties'))) { if (!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) { ctx.globalAlpha *= this.opacity; }, _setStrokeStyles: function(ctx) { if (this.stroke) { ctx.lineWidth = this.strokeWidth; ctx.lineCap = this.strokeLineCap; ctx.lineJoin = this.strokeLineJoin; ctx.miterLimit = this.strokeMiterLimit; ctx.strokeStyle = this.stroke.toLive ? this.stroke.toLive(ctx, this) : this.stroke; } }, _setFillStyles: function(ctx) { if (this.fill) { ctx.fillStyle = this.fill.toLive ? this.fill.toLive(ctx, this) : this.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 */ _renderControls: function(ctx) { if (!this.active || (this.group && this.group !== this.canvas.getActiveGroup())) { return; } var vpt = this.getViewportTransform(), matrix = this.calcTransformMatrix(), options; 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 (this.group && this.group === this.canvas.getActiveGroup()) { ctx.rotate(degreesToRadians(options.angle)); this.drawBordersInGroup(ctx, options); } else { ctx.rotate(degreesToRadians(this.angle)); this.drawBorders(ctx); } this.drawControls(ctx); 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; if (transform) { ctx.transform.apply(ctx, transform); } var offsetX = -this.width / 2 + filler.offsetX || 0, offsetY = -this.height / 2 + filler.offsetY || 0; ctx.translate(offsetX, offsetY); }, /** * @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(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _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(); }, /** * Clones an instance, some objects are async, so using callback method will work for every object. * Using the direct return does not work for images and groups. * @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 * @return {fabric.Object} clone of an instance */ clone: function(callback, propertiesToInclude) { if (this.constructor.fromObject) { return this.constructor.fromObject(this.toObject(propertiesToInclude), callback); } return new fabric.Object(this.toObject(propertiesToInclude)); }, /** * Creates an instance of fabric.Image out of an object * @param {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.get('active'), left: this.getLeft(), top: this.getTop() }; this.set('active', false); this.setPositionByOrigin(new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 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.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, forceAsync, extraParam) { var klass = fabric[className]; object = clone(object, true); if (forceAsync) { 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); }); } else { var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); callback && callback(instance); return instance; } }; /** * Unique id used internally when creating SVG elements * @static * @memberOf fabric.Object * @type Number */ fabric.Object.__uid = 0; })(typeof exports !== 'undefined' ? exports : this);