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