(function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, clone = fabric.util.object.clone, toFixed = fabric.util.toFixed, supportsLineDash = fabric.StaticCanvas.supports('setLineDash'), NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; if (fabric.Text) { fabric.warn('fabric.Text is already defined'); return; } var stateProperties = fabric.Object.prototype.stateProperties.concat(); stateProperties.push( 'fontFamily', 'fontWeight', 'fontSize', 'text', 'textDecoration', 'textAlign', 'fontStyle', 'lineHeight', 'textBackgroundColor' ); /** * Text class * @class fabric.Text * @extends fabric.Object * @return {fabric.Text} thisArg * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} * @see {@link fabric.Text#initialize} for constructor definition */ fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { /** * Properties which when set cause object to change dimensions * @type Object * @private */ _dimensionAffectingProps: { fontSize: true, fontWeight: true, fontFamily: true, fontStyle: true, lineHeight: true, stroke: true, strokeWidth: true, text: true, textAlign: true }, /** * @private */ _reNewline: /\r?\n/, /** * Use this regular expression to filter for whitespace that is not a new line. * Mostly used when text is 'justify' aligned. * @private */ _reSpacesAndTabs: /[ \t\r]+/g, /** * Retrieves object's fontSize * @method getFontSize * @memberOf fabric.Text.prototype * @return {String} Font size (in pixels) */ /** * Sets object's fontSize * Does not update the object .width and .height, * call ._initDimensions() to update the values. * @method setFontSize * @memberOf fabric.Text.prototype * @param {Number} fontSize Font size (in pixels) * @return {fabric.Text} * @chainable */ /** * Retrieves object's fontWeight * @method getFontWeight * @memberOf fabric.Text.prototype * @return {(String|Number)} Font weight */ /** * Sets object's fontWeight * Does not update the object .width and .height, * call ._initDimensions() to update the values. * @method setFontWeight * @memberOf fabric.Text.prototype * @param {(Number|String)} fontWeight Font weight * @return {fabric.Text} * @chainable */ /** * Retrieves object's fontFamily * @method getFontFamily * @memberOf fabric.Text.prototype * @return {String} Font family */ /** * Sets object's fontFamily * Does not update the object .width and .height, * call ._initDimensions() to update the values. * @method setFontFamily * @memberOf fabric.Text.prototype * @param {String} fontFamily Font family * @return {fabric.Text} * @chainable */ /** * Retrieves object's text * @method getText * @memberOf fabric.Text.prototype * @return {String} text */ /** * Sets object's text * Does not update the object .width and .height, * call ._initDimensions() to update the values. * @method setText * @memberOf fabric.Text.prototype * @param {String} text Text * @return {fabric.Text} * @chainable */ /** * Retrieves object's textDecoration * @method getTextDecoration * @memberOf fabric.Text.prototype * @return {String} Text decoration */ /** * Sets object's textDecoration * @method setTextDecoration * @memberOf fabric.Text.prototype * @param {String} textDecoration Text decoration * @return {fabric.Text} * @chainable */ /** * Retrieves object's fontStyle * @method getFontStyle * @memberOf fabric.Text.prototype * @return {String} Font style */ /** * Sets object's fontStyle * Does not update the object .width and .height, * call ._initDimensions() to update the values. * @method setFontStyle * @memberOf fabric.Text.prototype * @param {String} fontStyle Font style * @return {fabric.Text} * @chainable */ /** * Retrieves object's lineHeight * @method getLineHeight * @memberOf fabric.Text.prototype * @return {Number} Line height */ /** * Sets object's lineHeight * @method setLineHeight * @memberOf fabric.Text.prototype * @param {Number} lineHeight Line height * @return {fabric.Text} * @chainable */ /** * Retrieves object's textAlign * @method getTextAlign * @memberOf fabric.Text.prototype * @return {String} Text alignment */ /** * Sets object's textAlign * @method setTextAlign * @memberOf fabric.Text.prototype * @param {String} textAlign Text alignment * @return {fabric.Text} * @chainable */ /** * Retrieves object's textBackgroundColor * @method getTextBackgroundColor * @memberOf fabric.Text.prototype * @return {String} Text background color */ /** * Sets object's textBackgroundColor * @method setTextBackgroundColor * @memberOf fabric.Text.prototype * @param {String} textBackgroundColor Text background color * @return {fabric.Text} * @chainable */ /** * Type of an object * @type String * @default */ type: 'text', /** * Font size (in pixels) * @type Number * @default */ fontSize: 40, /** * Font weight (e.g. bold, normal, 400, 600, 800) * @type {(Number|String)} * @default */ fontWeight: 'normal', /** * Font family * @type String * @default */ fontFamily: 'Times New Roman', /** * Text decoration Possible values: "", "underline", "overline" or "line-through". * @type String * @default */ textDecoration: '', /** * Text alignment. Possible values: "left", "center", "right" or "justify". * @type String * @default */ textAlign: 'left', /** * Font style . Possible values: "", "normal", "italic" or "oblique". * @type String * @default */ fontStyle: '', /** * Line height * @type Number * @default */ lineHeight: 1.16, /** * Background color of text lines * @type String * @default */ textBackgroundColor: '', /** * List of properties to consider when checking if * state of an object is changed ({@link fabric.Object#hasStateChanged}) * as well as for history (undo/redo) purposes * @type Array */ stateProperties: stateProperties, /** * When defined, an object is rendered via stroke and this property specifies its color. * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 * @type String * @default */ stroke: null, /** * Shadow object representing shadow of this shape. * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 * @type fabric.Shadow * @default */ shadow: null, /** * @private */ _fontSizeFraction: 0.25, /** * Text Line proportion to font Size (in pixels) * @type Number * @default */ _fontSizeMult: 1.13, /** * Constructor * @param {String} text Text string * @param {Object} [options] Options object * @return {fabric.Text} thisArg */ initialize: function(text, options) { options = options || { }; this.text = text; this.__skipDimension = true; this.setOptions(options); this.__skipDimension = false; this._initDimensions(); }, /** * Initialize text dimensions. Render all text on given context * or on a offscreen canvas to get the text width with measureText. * Updates this.width and this.height with the proper values. * Does not return dimensions. * @param {CanvasRenderingContext2D} [ctx] Context to render on * @private */ _initDimensions: function(ctx) { if (this.__skipDimension) { return; } if (!ctx) { ctx = fabric.util.createCanvasElement().getContext('2d'); this._setTextStyles(ctx); } this._textLines = this._splitTextIntoLines(); this._clearCache(); this.width = this._getTextWidth(ctx); this.height = this._getTextHeight(ctx); }, /** * Returns string representation of an instance * @return {String} String representation of text object */ toString: function() { return '#'; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { this.clipTo && fabric.util.clipContext(this, ctx); this._setOpacity(ctx); this._setShadow(ctx); this._setupCompositeOperation(ctx); this._renderTextBackground(ctx); this._setStrokeStyles(ctx); this._setFillStyles(ctx); this._renderText(ctx); this._renderTextDecoration(ctx); this.clipTo && ctx.restore(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderText: function(ctx) { this._translateForTextAlign(ctx); this._renderTextFill(ctx); this._renderTextStroke(ctx); this._translateForTextAlign(ctx, true); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Boolean} back Indicates if translate back or forward */ _translateForTextAlign: function(ctx, back) { if (this.textAlign !== 'left' && this.textAlign !== 'justify') { var sign = back ? -1 : 1; ctx.translate(this.textAlign === 'center' ? (sign * this.width / 2) : sign * this.width, 0); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _setTextStyles: function(ctx) { ctx.textBaseline = 'alphabetic'; if (!this.skipTextAlign) { ctx.textAlign = this.textAlign; } ctx.font = this._getFontDeclaration(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @return {Number} Height of fabric.Text object */ _getTextHeight: function() { return this._textLines.length * this._getHeightOfLine(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @return {Number} Maximum width of fabric.Text object */ _getTextWidth: function(ctx) { var maxWidth = this._getLineWidth(ctx, 0); for (var i = 1, len = this._textLines.length; i < len; i++) { var currentLineWidth = this._getLineWidth(ctx, i); if (currentLineWidth > maxWidth) { maxWidth = currentLineWidth; } } return maxWidth; }, /* * Calculate object dimensions from its properties * @override * @private */ _getNonTransformedDimensions: function() { return { x: this.width, y: this.height }; }, /** * @private * @param {String} method Method name ("fillText" or "strokeText") * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} chars Chars to render * @param {Number} left Left position of text * @param {Number} top Top position of text */ _renderChars: function(method, ctx, chars, left, top) { // remove Text word from method var var shortM = method.slice(0, -4); if (this[shortM].toLive) { var offsetX = -this.width / 2 + this[shortM].offsetX || 0, offsetY = -this.height / 2 + this[shortM].offsetY || 0; ctx.save(); ctx.translate(offsetX, offsetY); left -= offsetX; top -= offsetY; } ctx[method](chars, left, top); this[shortM].toLive && ctx.restore(); }, /** * @private * @param {String} method Method name ("fillText" or "strokeText") * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line Text to render * @param {Number} left Left position of text * @param {Number} top Top position of text * @param {Number} lineIndex Index of a line in a text */ _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // lift the line by quarter of fontSize top -= this.fontSize * this._fontSizeFraction; // short-circuit var lineWidth = this._getLineWidth(ctx, lineIndex); if (this.textAlign !== 'justify' || this.width < lineWidth) { this._renderChars(method, ctx, line, left, top, lineIndex); return; } // stretch the line var words = line.split(/\s+/), charOffset = 0, wordsWidth = this._getWidthOfWords(ctx, line, lineIndex, 0), widthDiff = this.width - wordsWidth, numSpaces = words.length - 1, spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, leftOffset = 0, word; for (var i = 0, len = words.length; i < len; i++) { while (line[charOffset] === ' ' && charOffset < line.length) { charOffset++; } word = words[i]; this._renderChars(method, ctx, word, left + leftOffset, top, lineIndex, charOffset); leftOffset += this._getWidthOfWords(ctx, word, lineIndex, charOffset) + spaceWidth; charOffset += word.length; } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} line */ _getWidthOfWords: function (ctx, line) { return ctx.measureText(line.replace(/\s+/g, '')).width; }, /** * @private * @return {Number} Left offset */ _getLeftOffset: function() { return -this.width / 2; }, /** * @private * @return {Number} Top offset */ _getTopOffset: function() { return -this.height / 2; }, /** * Returns true because text has no style */ isEmptyStyles: function() { return true; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextFill: function(ctx) { if (!this.fill && this.isEmptyStyles()) { return; } var lineHeights = 0; for (var i = 0, len = this._textLines.length; i < len; i++) { var heightOfLine = this._getHeightOfLine(ctx, i), maxHeight = heightOfLine / this.lineHeight; this._renderTextLine( 'fillText', ctx, this._textLines[i], this._getLeftOffset(), this._getTopOffset() + lineHeights + maxHeight, i ); lineHeights += heightOfLine; } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextStroke: function(ctx) { if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) { return; } var lineHeights = 0; if (this.shadow && !this.shadow.affectStroke) { this._removeShadow(ctx); } ctx.save(); if (this.strokeDashArray) { // Spec requires the concatenation of two copies the dash list when the number of elements is odd if (1 & this.strokeDashArray.length) { this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); } supportsLineDash && ctx.setLineDash(this.strokeDashArray); } ctx.beginPath(); for (var i = 0, len = this._textLines.length; i < len; i++) { var heightOfLine = this._getHeightOfLine(ctx, i), maxHeight = heightOfLine / this.lineHeight; this._renderTextLine( 'strokeText', ctx, this._textLines[i], this._getLeftOffset(), this._getTopOffset() + lineHeights + maxHeight, i ); lineHeights += heightOfLine; } ctx.closePath(); ctx.restore(); }, _getHeightOfLine: function() { return this.fontSize * this._fontSizeMult * this.lineHeight; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _renderTextBackground: function(ctx) { this._renderTextBoxBackground(ctx); this._renderTextLinesBackground(ctx); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextBoxBackground: function(ctx) { if (!this.backgroundColor) { return; } ctx.fillStyle = this.backgroundColor; ctx.fillRect( this._getLeftOffset(), this._getTopOffset(), this.width, this.height ); // if there is background color no other shadows // should be casted this._removeShadow(ctx); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextLinesBackground: function(ctx) { if (!this.textBackgroundColor) { return; } var lineTopOffset = 0, heightOfLine, lineWidth, lineLeftOffset; ctx.fillStyle = this.textBackgroundColor; for (var i = 0, len = this._textLines.length; i < len; i++) { heightOfLine = this._getHeightOfLine(ctx, i); lineWidth = this._getLineWidth(ctx, i); if (lineWidth > 0) { lineLeftOffset = this._getLineLeftOffset(lineWidth); ctx.fillRect( this._getLeftOffset() + lineLeftOffset, this._getTopOffset() + lineTopOffset, lineWidth, heightOfLine / this.lineHeight ); } lineTopOffset += heightOfLine; } // if there is text background color no // other shadows should be casted this._removeShadow(ctx); }, /** * @private * @param {Number} lineWidth Width of text line * @return {Number} Line left offset */ _getLineLeftOffset: function(lineWidth) { if (this.textAlign === 'center') { return (this.width - lineWidth) / 2; } if (this.textAlign === 'right') { return this.width - lineWidth; } return 0; }, /** * @private */ _clearCache: function() { this.__lineWidths = [ ]; this.__lineHeights = [ ]; }, /** * @private */ _shouldClearCache: function() { var shouldClear = false; if (this._forceClearCache) { this._forceClearCache = false; return true; } for (var prop in this._dimensionAffectingProps) { if (this['__' + prop] !== this[prop]) { this['__' + prop] = this[prop]; shouldClear = true; } } return shouldClear; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex line number * @return {Number} Line width */ _getLineWidth: function(ctx, lineIndex) { if (this.__lineWidths[lineIndex]) { return this.__lineWidths[lineIndex] === -1 ? this.width : this.__lineWidths[lineIndex]; } var width, wordCount, line = this._textLines[lineIndex]; if (line === '') { width = 0; } else { width = this._measureLine(ctx, lineIndex); } this.__lineWidths[lineIndex] = width; if (width && this.textAlign === 'justify') { wordCount = line.split(/\s+/); if (wordCount.length > 1) { this.__lineWidths[lineIndex] = -1; } } return width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex line number * @return {Number} Line width */ _measureLine: function(ctx, lineIndex) { return ctx.measureText(this._textLines[lineIndex]).width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextDecoration: function(ctx) { if (!this.textDecoration) { return; } var halfOfVerticalBox = this.height / 2, _this = this, offsets = []; /** @ignore */ function renderLinesAtOffset(offsets) { var i, lineHeight = 0, len, j, oLen, lineWidth, lineLeftOffset, heightOfLine; for (i = 0, len = _this._textLines.length; i < len; i++) { lineWidth = _this._getLineWidth(ctx, i), lineLeftOffset = _this._getLineLeftOffset(lineWidth), heightOfLine = _this._getHeightOfLine(ctx, i); for (j = 0, oLen = offsets.length; j < oLen; j++) { ctx.fillRect( _this._getLeftOffset() + lineLeftOffset, lineHeight + (_this._fontSizeMult - 1 + offsets[j] ) * _this.fontSize - halfOfVerticalBox, lineWidth, _this.fontSize / 15); } lineHeight += heightOfLine; } } if (this.textDecoration.indexOf('underline') > -1) { offsets.push(0.85); // 1 - 3/16 } if (this.textDecoration.indexOf('line-through') > -1) { offsets.push(0.43); } if (this.textDecoration.indexOf('overline') > -1) { offsets.push(-0.12); } if (offsets.length > 0) { renderLinesAtOffset(offsets); } }, /** * @private */ _getFontDeclaration: function() { return [ // node-canvas needs "weight style", while browsers need "style weight" (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), this.fontSize + 'px', (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) ].join(' '); }, /** * Renders text instance on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on */ render: function(ctx, noTransform) { // do not render if object is not visible if (!this.visible) { return; } ctx.save(); this._setTextStyles(ctx); if (this._shouldClearCache()) { this._initDimensions(ctx); } this.drawSelectionBackground(ctx); if (!noTransform) { this.transform(ctx); } if (this.transformMatrix) { ctx.transform.apply(ctx, this.transformMatrix); } if (this.group && this.group.type === 'path-group') { ctx.translate(this.left, this.top); } this._render(ctx); ctx.restore(); }, /** * Returns the text as an array of lines. * @returns {Array} Lines in the text */ _splitTextIntoLines: function() { return this.text.split(this._reNewline); }, /** * Returns 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 object = extend(this.callSuper('toObject', propertiesToInclude), { text: this.text, fontSize: this.fontSize, fontWeight: this.fontWeight, fontFamily: this.fontFamily, fontStyle: this.fontStyle, lineHeight: this.lineHeight, textDecoration: this.textDecoration, textAlign: this.textAlign, textBackgroundColor: this.textBackgroundColor }); if (!this.includeDefaultValues) { this._removeDefaultValues(object); } return object; }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = this._createBaseSVGMarkup(), offsets = this._getSVGLeftTopOffsets(this.ctx), textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); this._wrapSVGTextAndBg(markup, textAndBg); return reviver ? reviver(markup.join('')) : markup.join(''); }, /** * @private */ _getSVGLeftTopOffsets: function(ctx) { var lineTop = this._getHeightOfLine(ctx, 0), textLeft = -this.width / 2, textTop = 0; return { textLeft: textLeft + (this.group && this.group.type === 'path-group' ? this.left : 0), textTop: textTop + (this.group && this.group.type === 'path-group' ? -this.top : 0), lineTop: lineTop }; }, /** * @private */ _wrapSVGTextAndBg: function(markup, textAndBg) { var noShadow = true, filter = this.getSvgFilter(), style = filter === '' ? '' : ' style="' + filter + '"'; markup.push( '\t\n', textAndBg.textBgRects.join(''), '\t\t\n', textAndBg.textSpans.join(''), '\t\t\n', '\t\n' ); }, /** * @private * @param {Number} textTopOffset Text top offset * @param {Number} textLeftOffset Text left offset * @return {Object} */ _getSVGTextAndBg: function(textTopOffset, textLeftOffset) { var textSpans = [ ], textBgRects = [ ], height = 0; // bounding-box background this._setSVGBg(textBgRects); // text and text-background for (var i = 0, len = this._textLines.length; i < len; i++) { if (this.textBackgroundColor) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset, textTopOffset, height); } this._setSVGTextLineText(i, textSpans, height, textLeftOffset, textTopOffset, textBgRects); height += this._getHeightOfLine(this.ctx, i); } return { textSpans: textSpans, textBgRects: textBgRects }; }, _setSVGTextLineText: function(i, textSpans, height, textLeftOffset, textTopOffset) { var yPos = this.fontSize * (this._fontSizeMult - this._fontSizeFraction) - textTopOffset + height - this.height / 2; if (this.textAlign === 'justify') { // i call from here to do not intefere with IText this._setSVGTextLineJustifed(i, textSpans, yPos, textLeftOffset); return; } textSpans.push( '\t\t\t elements since setting opacity // on containing one doesn't work in Illustrator this._getFillAttributes(this.fill), '>', fabric.util.string.escapeXml(this._textLines[i]), '\n' ); }, _setSVGTextLineJustifed: function(i, textSpans, yPos, textLeftOffset) { var ctx = fabric.util.createCanvasElement().getContext('2d'); this._setTextStyles(ctx); var line = this._textLines[i], words = line.split(/\s+/), wordsWidth = this._getWidthOfWords(ctx, line), widthDiff = this.width - wordsWidth, numSpaces = words.length - 1, spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, word, attributes = this._getFillAttributes(this.fill), len; textLeftOffset += this._getLineLeftOffset(this._getLineWidth(ctx, i)); for (i = 0, len = words.length; i < len; i++) { word = words[i]; textSpans.push( '\t\t\t elements since setting opacity // on containing one doesn't work in Illustrator attributes, '>', fabric.util.string.escapeXml(word), '\n' ); textLeftOffset += this._getWidthOfWords(ctx, word) + spaceWidth; } }, _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, textTopOffset, height) { textBgRects.push( '\t\t\n'); }, _setSVGBg: function(textBgRects) { if (this.backgroundColor) { textBgRects.push( '\t\t\n'); } }, /** * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 * * @private * @param {Any} value * @return {String} */ _getFillAttributes: function(value) { var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { return 'fill="' + value + '"'; } return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; }, /* _TO_SVG_END_ */ /** * Sets specified property to a specified value * @param {String} key * @param {Any} value * @return {fabric.Text} thisArg * @chainable */ _set: function(key, value) { this.callSuper('_set', key, value); if (key in this._dimensionAffectingProps) { this._initDimensions(); this.setCoords(); } }, /** * Returns complexity of an instance * @return {Number} complexity */ complexity: function() { return 1; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) * @static * @memberOf fabric.Text * @see: http://www.w3.org/TR/SVG/text.html#TextElement */ fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( 'x y dx dy font-family font-style font-weight font-size text-decoration text-anchor'.split(' ')); /** * Default SVG font size * @static * @memberOf fabric.Text */ fabric.Text.DEFAULT_SVG_FONT_SIZE = 16; /** * Returns fabric.Text instance from an SVG element (not yet implemented) * @static * @memberOf fabric.Text * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @return {fabric.Text} Instance of fabric.Text */ fabric.Text.fromElement = function(element, options) { if (!element) { return null; } var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); options.top = options.top || 0; options.left = options.left || 0; if ('dx' in parsedAttributes) { options.left += parsedAttributes.dx; } if ('dy' in parsedAttributes) { options.top += parsedAttributes.dy; } if (!('fontSize' in options)) { options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; } if (!options.originX) { options.originX = 'left'; } var textContent = ''; // The XML is not properly parsed in IE9 so a workaround to get // textContent is through firstChild.data. Another workaround would be // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does) if (!('textContent' in element)) { if ('firstChild' in element && element.firstChild !== null) { if ('data' in element.firstChild && element.firstChild.data !== null) { textContent = element.firstChild.data; } } } else { textContent = element.textContent; } textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '); var text = new fabric.Text(textContent, options), /* Adjust positioning: x/y attributes in SVG correspond to the bottom-left corner of text bounding box top/left properties in Fabric correspond to center point of text bounding box */ offX = 0; if (text.originX === 'left') { offX = text.getWidth() / 2; } if (text.originX === 'right') { offX = -text.getWidth() / 2; } text.set({ left: text.getLeft() + offX, top: text.getTop() - text.getHeight() / 2 + text.fontSize * (0.18 + text._fontSizeFraction) /* 0.3 is the old lineHeight */ }); return text; }; /* _FROM_SVG_END_ */ /** * Returns fabric.Text instance from an object representation * @static * @memberOf fabric.Text * @param {Object} object Object to create an instance from * @return {fabric.Text} Instance of fabric.Text */ fabric.Text.fromObject = function(object) { return new fabric.Text(object.text, clone(object)); }; fabric.util.createAccessors(fabric.Text); })(typeof exports !== 'undefined' ? exports : this);