(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'); if (fabric.Text) { fabric.warn('fabric.Text is already defined'); return; } var dimensionAffectingProps = { fontSize: true, fontWeight: true, fontFamily: true, textDecoration: true, fontStyle: true, lineHeight: true, stroke: true, strokeWidth: true, text: true }; var stateProperties = fabric.Object.prototype.stateProperties.concat(); stateProperties.push( 'fontFamily', 'fontWeight', 'fontSize', 'path', 'text', 'textDecoration', 'textShadow', 'textAlign', 'fontStyle', 'lineHeight', 'stroke', 'strokeWidth', 'backgroundColor', 'textBackgroundColor', 'useNative' ); /** * Text class * @class fabric.Text * @extends fabric.Object * @return {fabric.Text} thisArg */ fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { /** * Font size (in pixels) * @type Number * @default */ fontSize: 40, /** * Font weight (e.g. bold, normal, 400, 600, 800) * @type Number * @default */ fontWeight: 'normal', /** * Font family * @type String * @default */ fontFamily: 'Times New Roman', /** * Text decoration (e.g. underline, overline) * @type String * @default */ textDecoration: '', /** * Text shadow * @type String | null * @default */ textShadow: '', /** * Text alignment. Possible values: "left", "center", or "right". * @type String * @default */ textAlign: 'left', /** * Font style (e.g. italic) * @type String * @default */ fontStyle: '', /** * Line height * @type Number * @default */ lineHeight: 1.3, /** * Stroke style. When specified, text is rendered with stroke * @type String * @default */ stroke: '', /** * Stroke width * @type Number * @default */ strokeWidth: 1, /** * Background color of an entire text box * @type String * @default */ backgroundColor: '', /** * Background color of text lines * @type String * @default */ textBackgroundColor: '', /** * URL of a font file, when using Cufon * @type String | null * @default */ path: null, /** * Type of an object * @type String * @default */ type: 'text', /** * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) * @type Boolean * @default */ useNative: true, /** * 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, /** * Constructor * @param {String} text * @param {Object} [options] * @return {fabric.Text} thisArg */ initialize: function(text, options) { options = options || { }; this.text = text; this.__skipDimension = true; this.setOptions(options); this.__skipDimension = false; this._initDimensions(); this.setCoords(); }, /** * Renders text object on offscreen canvas, so that it would get dimensions * @private */ _initDimensions: function() { if (this.__skipDimension) return; var canvasEl = fabric.util.createCanvasElement(); this._render(canvasEl.getContext('2d')); }, /** * 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) { var isInPathGroup = this.group && this.group.type !== 'group'; if (isInPathGroup && !this.transformMatrix) { ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); } else if (isInPathGroup && this.transformMatrix) { ctx.translate(-this.group.width/2, -this.group.height/2); } if (typeof Cufon === 'undefined' || this.useNative === true) { this._renderViaNative(ctx); } else { this._renderViaCufon(ctx); } }, /** * @private */ _renderViaCufon: function(ctx) { var o = Cufon.textOptions || (Cufon.textOptions = { }); // export options to be used by cufon.js o.left = this.left; o.top = this.top; o.context = ctx; o.color = this.fill; var el = this._initDummyElementForCufon(); // set "cursor" to top/left corner this.transform(ctx); // draw text Cufon.replaceElement(el, { engine: 'canvas', separate: 'none', fontFamily: this.fontFamily, fontWeight: this.fontWeight, textDecoration: this.textDecoration, textShadow: this.textShadow, textAlign: this.textAlign, fontStyle: this.fontStyle, lineHeight: this.lineHeight, stroke: this.stroke, strokeWidth: this.strokeWidth, backgroundColor: this.backgroundColor, textBackgroundColor: this.textBackgroundColor }); // update width, height this.width = o.width; this.height = o.height; this._totalLineHeight = o.totalLineHeight; this._fontAscent = o.fontAscent; this._boundaries = o.boundaries; this._shadowOffsets = o.shadowOffsets; this._shadows = o.shadows || [ ]; el = null; // need to set coords _after_ the width/height was retreived from Cufon this.setCoords(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderViaNative: function(ctx) { this.transform(ctx, fabric.isLikelyNode); this._setTextStyles(ctx); var textLines = this.text.split(/\r?\n/); this.width = this._getTextWidth(ctx, textLines); this.height = this._getTextHeight(ctx, textLines); this._renderTextBackground(ctx, textLines); if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.save(); ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); } ctx.save(); this._setTextShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); this._renderTextFill(ctx, textLines); this._renderTextStroke(ctx, textLines); this.clipTo && ctx.restore(); this.textShadow && ctx.restore(); ctx.restore(); if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.restore(); } this._renderTextDecoration(ctx, textLines); this._setBoundaries(ctx, textLines); this._totalLineHeight = 0; this.setCoords(); }, /** * @private */ _setBoundaries: function(ctx, textLines) { this._boundaries = [ ]; for (var i = 0, len = textLines.length; i < len; i++) { var lineWidth = this._getLineWidth(ctx, textLines[i]); var lineLeftOffset = this._getLineLeftOffset(lineWidth); this._boundaries.push({ height: this.fontSize * this.lineHeight, width: lineWidth, left: lineLeftOffset }); } }, /** * @private */ _setTextStyles: function(ctx) { if (this.fill) { ctx.fillStyle = this.fill.toLive ? this.fill.toLive(ctx) : this.fill; } 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.stroke; } ctx.textBaseline = 'alphabetic'; ctx.textAlign = this.textAlign; ctx.font = this._getFontDeclaration(); }, /** * @private */ _getTextHeight: function(ctx, textLines) { return this.fontSize * textLines.length * this.lineHeight; }, /** * @private */ _getTextWidth: function(ctx, textLines) { var maxWidth = ctx.measureText(textLines[0]).width; for (var i = 1, len = textLines.length; i < len; i++) { var currentLineWidth = ctx.measureText(textLines[i]).width; if (currentLineWidth > maxWidth) { maxWidth = currentLineWidth; } } return maxWidth; }, /** * @private */ _setTextShadow: function(ctx) { if (this.textShadow) { // "rgba(0,0,0,0.2) 2px 2px 10px" // "rgb(0, 100, 0) 0 0 5px" // "red 2px 2px 1px" // "#f55 123 345 567" var reOffsetsAndBlur = /\s+(-?\d+)(?:px)?\s+(-?\d+)(?:px)?\s+(\d+)(?:px)?\s*/; var shadowDeclaration = this.textShadow; var offsetsAndBlur = reOffsetsAndBlur.exec(this.textShadow); var shadowColor = shadowDeclaration.replace(reOffsetsAndBlur, ''); ctx.save(); ctx.shadowColor = shadowColor; ctx.shadowOffsetX = parseInt(offsetsAndBlur[1], 10); ctx.shadowOffsetY = parseInt(offsetsAndBlur[2], 10); ctx.shadowBlur = parseInt(offsetsAndBlur[3], 10); this._shadows = [{ blur: ctx.shadowBlur, color: ctx.shadowColor, offX: ctx.shadowOffsetX, offY: ctx.shadowOffsetY }]; this._shadowOffsets = [[ parseInt(ctx.shadowOffsetX, 10), parseInt(ctx.shadowOffsetY, 10) ]]; } }, /** * @private * @param method * @param ctx * @param line * @param left * param top */ _drawTextLine: function(method, ctx, line, left, top) { // short-circuit if (this.textAlign !== 'justify') { ctx[method](line, left, top); return; } var lineWidth = ctx.measureText(line).width; var totalWidth = this.width; if (totalWidth > lineWidth) { // stretch the line var words = line.split(/\s+/); var wordsWidth = ctx.measureText(line.replace(/\s+/g, '')).width; var widthDiff = totalWidth - wordsWidth; var numSpaces = words.length - 1; var spaceWidth = widthDiff / numSpaces; var leftOffset = 0; for (var i = 0, len = words.length; i < len; i++) { ctx[method](words[i], left + leftOffset, top); leftOffset += ctx.measureText(words[i]).width + spaceWidth; } } else { ctx[method](line, left, top); } }, _getLeftOffset: function() { if (fabric.isLikelyNode && (this.originX === 'left' || this.originX === 'center')) { return 0; } return -this.width / 2; }, _getTopOffset: function() { if (fabric.isLikelyNode && (this.originY === 'top' || this.originY === 'center')) { return 0; } return -this.height / 2; }, /** * @private */ _renderTextFill: function(ctx, textLines) { if (this.fill) { this._boundaries = [ ]; for (var i = 0, len = textLines.length; i < len; i++) { this._drawTextLine( 'fillText', ctx, textLines[i], this._getLeftOffset(), this._getTopOffset() + (i * this.fontSize * this.lineHeight) + this.fontSize ); } } }, /** * @private */ _renderTextStroke: function(ctx, textLines) { if (this.stroke) { 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 = textLines.length; i < len; i++) { this._drawTextLine( 'strokeText', ctx, textLines[i], this._getLeftOffset(), this._getTopOffset() + (i * this.fontSize * this.lineHeight) + this.fontSize ); } ctx.closePath(); } }, /** * @private */ _renderTextBackground: function(ctx, textLines) { this._renderTextBoxBackground(ctx); this._renderTextLinesBackground(ctx, textLines); }, /** * @private */ _renderTextBoxBackground: function(ctx) { if (this.backgroundColor) { ctx.save(); ctx.fillStyle = this.backgroundColor; ctx.fillRect( this._getLeftOffset(), this._getTopOffset(), this.width, this.height ); ctx.restore(); } }, /** * @private */ _renderTextLinesBackground: function(ctx, textLines) { if (this.textBackgroundColor) { ctx.save(); ctx.fillStyle = this.textBackgroundColor; for (var i = 0, len = textLines.length; i < len; i++) { if (textLines[i] !== '') { var lineWidth = this._getLineWidth(ctx, textLines[i]); var lineLeftOffset = this._getLineLeftOffset(lineWidth); ctx.fillRect( this._getLeftOffset() + lineLeftOffset, this._getTopOffset() + (i * this.fontSize * this.lineHeight), lineWidth, this.fontSize * this.lineHeight ); } } ctx.restore(); } }, /** * @private */ _getLineLeftOffset: function(lineWidth) { if (this.textAlign === 'center') { return (this.width - lineWidth) / 2; } if (this.textAlign === 'right') { return this.width - lineWidth; } return 0; }, /** * @private * @param ctx * @param line */ _getLineWidth: function(ctx, line) { return this.textAlign === 'justify' ? this.width : ctx.measureText(line).width; }, /** * @private */ _renderTextDecoration: function(ctx, textLines) { var halfOfVerticalBox = this.originY === 'top' ? 0 : this._getTextHeight(ctx, textLines) / 2; var _this = this; /** @ignore */ function renderLinesAtOffset(offset) { for (var i = 0, len = textLines.length; i < len; i++) { var lineWidth = _this._getLineWidth(ctx, textLines[i]); var lineLeftOffset = _this._getLineLeftOffset(lineWidth); ctx.fillRect( _this._getLeftOffset() + lineLeftOffset, (offset + (i * _this.fontSize * _this.lineHeight)) - halfOfVerticalBox, lineWidth, 1); } } if (this.textDecoration.indexOf('underline') > -1) { renderLinesAtOffset(this.fontSize); } if (this.textDecoration.indexOf('line-through') > -1) { renderLinesAtOffset(this.fontSize / 2); } if (this.textDecoration.indexOf('overline') > -1) { renderLinesAtOffset(0); } }, /** * @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(' '); }, /** * @private */ _initDummyElementForCufon: function() { var el = fabric.document.createElement('pre'), container = fabric.document.createElement('div'); // Cufon doesn't play nice with textDecoration=underline if element doesn't have a parent container.appendChild(el); if (typeof G_vmlCanvasManager === 'undefined') { el.innerHTML = this.text; } else { // IE 7 & 8 drop newlines and white space on text nodes // see: http://web.student.tuwien.ac.at/~e0226430/innerHtmlQuirk.html // see: http://www.w3schools.com/dom/dom_mozilla_vs_ie.asp el.innerText = this.text.replace(/\r?\n/gi, '\r'); } el.style.fontSize = this.fontSize + 'px'; el.style.letterSpacing = 'normal'; return el; }, /** * Renders text instance on a specified context * @param ctx {CanvasRenderingContext2D} context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ render: function(ctx, noTransform) { // do not render if object is not visible if (!this.visible) return; ctx.save(); this._render(ctx); if (!noTransform && this.active) { this.drawBorders(ctx); this.drawControls(ctx); } ctx.restore(); }, /** * Returns object representation of an instance * @param {Array} propertiesToInclude * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return 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, textShadow: this.textShadow, textAlign: this.textAlign, path: this.path, stroke: this.stroke, strokeWidth: this.strokeWidth, backgroundColor: this.backgroundColor, textBackgroundColor: this.textBackgroundColor, useNative: this.useNative }); }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an instance * @return {String} svg representation of an instance */ toSVG: function() { var textLines = this.text.split(/\r?\n/), lineTopOffset = this.useNative ? this.fontSize * this.lineHeight : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), textLeftOffset = -(this.width/2), textTopOffset = this.useNative ? this.fontSize - 1 : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), shadowSpans = this._getSVGShadows(lineTopOffset, textLines); // move top offset by an ascent textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); return [ '', textAndBg.textBgRects.join(''), '', shadowSpans.join(''), textAndBg.textSpans.join(''), '', '' ].join(''); }, /** * @private */ _getSVGShadows: function(lineTopOffset, textLines) { var shadowSpans = [], j, i, jlen, ilen, lineTopOffsetMultiplier = 1; if (!this._shadows || !this._boundaries) { return shadowSpans; } for (j = 0, jlen = this._shadows.length; j < jlen; j++) { for (i = 0, ilen = textLines.length; i < ilen; i++) { if (textLines[i] !== '') { var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; shadowSpans.push( '', fabric.util.string.escapeXml(textLines[i]), ''); lineTopOffsetMultiplier = 1; } else { // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier // prevents empty tspans lineTopOffsetMultiplier++; } } } return shadowSpans; }, /** * @private */ _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; // bounding-box background if (this.backgroundColor && this._boundaries) { textBgRects.push( ''); } // text and text-background for (i = 0, len = textLines.length; i < len; i++) { if (textLines[i] !== '') { lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; textSpans.push( ' elements since setting opacity on containing one doesn't work in Illustrator this._getFillAttributes(this.fill), '>', fabric.util.string.escapeXml(textLines[i]), '' ); lineTopOffsetMultiplier = 1; } else { // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier // prevents empty tspans lineTopOffsetMultiplier++; } if (!this.textBackgroundColor || !this._boundaries) continue; textBgRects.push( ''); } return { textSpans: textSpans, textBgRects: textBgRects }; }, /** * 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 */ _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 "color" of an instance (alias of `set('fill', …)`) * @param {String} value * @return {fabric.Text} thisArg * @chainable */ setColor: function(value) { this.set('fill', value); return this; }, /** * Returns actual text value of an instance * @return {String} */ getText: function() { return this.text; }, /** * Sets specified property to a specified value * @param {String} name * @param {Any} value * @return {fabric.Text} thisArg * @chainable */ _set: function(name, value) { if (name === 'fontFamily' && this.path) { this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); } this.callSuper('_set', name, value); if (name in dimensionAffectingProps) { this._initDimensions(); this.setCoords(); } } }); /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) * @static */ fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( 'x y font-family font-style font-weight font-size text-decoration'.split(' ')); /** * Returns fabric.Text instance from an object representation * @static * @param {Object} object to create an instance from * @return {fabric.Text} an instance */ fabric.Text.fromObject = function(object) { return new fabric.Text(object.text, clone(object)); }; /** * Returns fabric.Text instance from an SVG element (not yet implemented) * @static * @param element * @param options * @return {fabric.Text} an instance */ 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); var text = new fabric.Text(element.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 */ text.set({ left: text.getLeft() + text.getWidth() / 2, top: text.getTop() - text.getHeight() / 2 }); return text; }; fabric.util.createAccessors(fabric.Text); })(typeof exports !== 'undefined' ? exports : this);