From 0e09961c64763cddc4184e955f6e9084e67e32f9 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 30 Nov 2014 19:04:25 +0100 Subject: [PATCH] Update to text, rendering and optimization --- src/gradient.class.js | 15 +- src/mixins/itext.svg_export.js | 48 ++- src/mixins/itext_behavior.mixin.js | 41 +-- src/mixins/itext_click_behavior.mixin.js | 16 +- src/mixins/itext_key_behavior.mixin.js | 139 +++----- src/mixins/object.svg_export.js | 2 +- src/shapes/itext.class.js | 291 ++++++--------- src/shapes/text.class.js | 427 +++++++++-------------- src/shapes/text.cufon.js | 79 ----- test/unit/itext.js | 4 +- test/unit/text.js | 16 +- 11 files changed, 393 insertions(+), 685 deletions(-) delete mode 100644 src/shapes/text.cufon.js diff --git a/src/gradient.class.js b/src/gradient.class.js index bc6cb3e8..3f3b9cfb 100644 --- a/src/gradient.class.js +++ b/src/gradient.class.js @@ -241,14 +241,14 @@ * @return {CanvasGradient} */ toLive: function(ctx, object) { - var gradient, coords = fabric.util.object.clone(this.coords); + var gradient, prop, coords = fabric.util.object.clone(this.coords); if (!this.type) { return; } if (object.group && object.group.type === 'path-group') { - for (var prop in coords) { + for (prop in coords) { if (prop === 'x1' || prop === 'x2') { coords[prop] += -this.offsetX + object.width / 2; } @@ -258,6 +258,17 @@ } } + if (object.type === 'text') { + for (prop in coords) { + if (prop === 'x1' || prop === 'x2') { + coords[prop] -= object.width / 2; + } + else if (prop === 'y1' || prop === 'y2') { + coords[prop] -= object.height / 2; + } + } + } + if (this.type === 'linear') { gradient = ctx.createLinearGradient( coords.x1, coords.y1, coords.x2, coords.y2); diff --git a/src/mixins/itext.svg_export.js b/src/mixins/itext.svg_export.js index 7e234d11..b95d3be6 100644 --- a/src/mixins/itext.svg_export.js +++ b/src/mixins/itext.svg_export.js @@ -4,27 +4,26 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private */ - _setSVGTextLineText: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + _setSVGTextLineText: function(lineIndex, textSpans, height, textLeftOffset, textTopOffset, textBgRects) { if (!this.styles[lineIndex]) { this.callSuper('_setSVGTextLineText', - textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier); + lineIndex, textSpans, height, textLeftOffset, textTopOffset); } else { this._setSVGTextLineChars( - textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); + lineIndex, textSpans, height, textLeftOffset, textBgRects); } }, /** * @private */ - _setSVGTextLineChars: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { + _setSVGTextLineChars: function(lineIndex, textSpans, height, textLeftOffset, textBgRects) { - var yProp = lineIndex === 0 || this.useNative ? 'y' : 'dy', - chars = textLine.split(''), + var chars = this._textLines[lineIndex].split(''), charOffset = 0, - lineLeftOffset = this._getSVGLineLeftOffset(lineIndex), - lineTopOffset = this._getSVGLineTopOffset(lineIndex), + lineLeftOffset = this._getSVGLineLeftOffset(lineIndex) - this.width / 2, + lineOffset = this._getSVGLineTopOffset(lineIndex), heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); for (var i = 0, len = chars.length; i < len; i++) { @@ -32,14 +31,14 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot textSpans.push( this._createTextCharSpan( - chars[i], styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset)); + chars[i], styleDecl, lineLeftOffset, lineOffset.lineTop + lineOffset.offset, charOffset)); var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); if (styleDecl.textBackgroundColor) { textBgRects.push( this._createTextCharBg( - styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset)); + styleDecl, lineLeftOffset, lineOffset.lineTop, heightOfLine, charWidth, charOffset)); } charOffset += charWidth; @@ -50,20 +49,22 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @private */ _getSVGLineLeftOffset: function(lineIndex) { - return (this._boundaries && this._boundaries[lineIndex]) - ? fabric.util.toFixed(this._boundaries[lineIndex].left, 2) - : 0; + return fabric.util.toFixed(this._getLineLeftOffset(this.__lineWidths[lineIndex]), 2); }, /** * @private */ _getSVGLineTopOffset: function(lineIndex) { - var lineTopOffset = 0; - for (var j = 0; j <= lineIndex; j++) { + var lineTopOffset = 0, lastHeight = 0; + for (var j = 0; j < lineIndex; j++) { lineTopOffset += this._getHeightOfLine(this.ctx, j); } - return lineTopOffset - this.height / 2; + lastHeight = this._getHeightOfLine(this.ctx, j); + return { + lineTop: lineTopOffset, + offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) + }; }, /** @@ -73,13 +74,10 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot return [ //jscs:disable validateIndentation '' //jscs:enable validateIndentation ].join(''); @@ -88,7 +86,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private */ - _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset) { + _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, charOffset) { var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ visible: true, @@ -99,16 +97,14 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot return [ //jscs:disable validateIndentation - '', - fabric.util.string.escapeXml(_char), '' //jscs:enable validateIndentation diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index 28114007..0f492676 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -157,10 +157,8 @@ * Selects entire text */ selectAll: function() { - this.selectionStart = 0; - this.selectionEnd = this.text.length; - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + this.setSelectionStart(0); + this.setSelectionEnd(this.text.length); }, /** @@ -375,6 +373,7 @@ this.hiddenTextarea.value = this.text; this.hiddenTextarea.selectionStart = this.selectionStart; + this.hiddenTextarea.selectionEnd = this.selectionEnd; }, /** @@ -441,9 +440,8 @@ * @private */ _removeExtraneousStyles: function() { - var textLines = this.text.split(this._reNewline); for (var prop in this.styles) { - if (!textLines[prop]) { + if (!this._textLines[prop]) { delete this.styles[prop]; } } @@ -474,13 +472,14 @@ this.text = this.text.slice(0, start) + this.text.slice(end); + this._clearCache(); }, /** * Inserts a character where cursor is (replacing selection if one exists) * @param {String} _chars Characters to insert */ - insertChars: function(_chars) { + insertChars: function(_chars, useCopiedStyle) { var isEndOfLine = this.text.slice(this.selectionStart, this.selectionStart + 1) === '\n'; this.text = this.text.slice(0, this.selectionStart) + @@ -488,21 +487,16 @@ this.text.slice(this.selectionEnd); if (this.selectionStart === this.selectionEnd) { - this.insertStyleObjects(_chars, isEndOfLine, this.copiedStyles); + this.insertStyleObjects(_chars, isEndOfLine, useCopiedStyle); } // else if (this.selectionEnd - this.selectionStart > 1) { // TODO: replace styles properly // console.log('replacing MORE than 1 char'); // } - - this.selectionStart += _chars.length; - this.selectionEnd = this.selectionStart; - - if (this.canvas) { - // TODO: double renderAll gets rid of text box shift happenning sometimes - // need to find out what exactly causes it and fix it - this.canvas.renderAll().renderAll(); - } + this.setSelectionStart(this.selectionStart + _chars.length); + this.setSelectionEnd(this.selectionStart); + this._clearCache(); + this.canvas && this.canvas.renderAll(); this.setCoords(); this.fire('changed'); @@ -544,6 +538,7 @@ } this.styles[lineIndex + 1] = newLineStyles; } + this._clearCache(); }, /** @@ -573,15 +568,16 @@ this.styles[lineIndex][charIndex] = style || clone(currentLineStyles[charIndex - 1]); + this._clearCache(); }, /** * Inserts style object(s) * @param {String} _chars Characters at the location where style is inserted * @param {Boolean} isEndOfLine True if it's end of line - * @param {Array} [styles] Styles to insert + * @param {Boolean} [useCopiedStyle] Style to insert */ - insertStyleObjects: function(_chars, isEndOfLine, styles) { + insertStyleObjects: function(_chars, isEndOfLine, useCopiedStyle) { // removed shortcircuit over isEmptyStyles var cursorLocation = this.get2DCursorLocation(), @@ -596,8 +592,8 @@ this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); } else { - if (styles) { - this._insertStyles(styles); + if (useCopiedStyle) { + this._insertStyles(this.copiedStyles); } else { // TODO: support multiple style insertion if _chars.length > 1 @@ -649,8 +645,7 @@ if (isBeginningOfLine) { - var textLines = this.text.split(this._reNewline), - textOnPreviousLine = textLines[lineIndex - 1], + var textOnPreviousLine = this._textLines[lineIndex - 1], newCharIndexOnPrevLine = textOnPreviousLine ? textOnPreviousLine.length : 0; diff --git a/src/mixins/itext_click_behavior.mixin.js b/src/mixins/itext_click_behavior.mixin.js index 6a1e4a96..ad76a14d 100644 --- a/src/mixins/itext_click_behavior.mixin.js +++ b/src/mixins/itext_click_behavior.mixin.js @@ -203,30 +203,30 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ getSelectionStartFromPointer: function(e) { var mouseOffset = this._getLocalRotatedPointer(e), - textLines = this.text.split(this._reNewline), prevWidth = 0, width = 0, height = 0, charIndex = 0, - newSelectionStart; - - for (var i = 0, len = textLines.length; i < len; i++) { + newSelectionStart, + line; + for (var i = 0, len = this._textLines.length; i < len; i++) { + line = this._textLines[i].split(''); height += this._getHeightOfLine(this.ctx, i) * this.scaleY; - var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines), + var widthOfLine = this._getLineWidth(this.ctx, i), lineLeftOffset = this._getLineLeftOffset(widthOfLine); width = lineLeftOffset * this.scaleX; if (this.flipX) { // when oject is horizontally flipped we reverse chars - textLines[i] = textLines[i].split('').reverse().join(''); + this._textLines[i] = line.split('').reverse().join(''); } - for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + for (var j = 0, jlen = line.length; j < jlen; j++) { - var _char = textLines[i][j]; + var _char = line[j]; prevWidth = width; width += this._getWidthOfChar(this.ctx, _char, i, this.flipX ? jlen - j : j) * diff --git a/src/mixins/itext_key_behavior.mixin.js b/src/mixins/itext_key_behavior.mixin.js index e700ed73..762a3b9f 100644 --- a/src/mixins/itext_key_behavior.mixin.js +++ b/src/mixins/itext_key_behavior.mixin.js @@ -62,7 +62,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (!this.isEditing) { return; } - if (e.keyCode in this._keysMap) { this[this._keysMap[e.keyCode]](e); } @@ -72,10 +71,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot else { return; } - e.stopImmediatePropagation(); e.preventDefault(); - this.canvas && this.canvas.renderAll(); }, @@ -125,7 +122,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } if (copiedText) { - this.insertChars(copiedText); + this.insertChars(copiedText, true); } }, @@ -173,10 +170,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ getDownCursorOffset: function(e, isRight) { var selectionProp = isRight ? this.selectionEnd : this.selectionStart, - textLines = this.text.split(this._reNewline), - _char, - lineLeftOffset, - + _char, lineLeftOffset, textBeforeCursor = this.text.slice(0, selectionProp), textAfterCursor = this.text.slice(selectionProp), @@ -187,13 +181,13 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot cursorLocation = this.get2DCursorLocation(selectionProp); // if on last line, down cursor goes to end of line - if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey || e.keyCode === 34) { + if (cursorLocation.lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) { // move to the end of a text return this.text.length - selectionProp; } - var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); + var widthOfSameLineBeforeCursor = this._getLineWidth(this.ctx, cursorLocation.lineIndex); lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset, @@ -205,7 +199,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } var indexOnNextLine = this._getIndexOnNextLine( - cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines); + cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor); return textOnSameLineAfterCursor.length + 1 + indexOnNextLine; }, @@ -213,9 +207,9 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private */ - _getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + _getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor) { var lineIndex = cursorLocation.lineIndex + 1, - widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines), + widthOfNextLine = this._getLineWidth(this.ctx, lineIndex), lineLeftOffset = this._getLineLeftOffset(widthOfNextLine), widthOfCharsOnNextLine = lineLeftOffset, indexOnNextLine = 0, @@ -277,15 +271,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ moveCursorDownWithoutShift: function(offset) { this._selectionDirection = 'right'; - this.selectionStart += offset; - - if (this.selectionStart > this.text.length) { - this.selectionStart = this.text.length; - } - this.selectionEnd = this.selectionStart; - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + this.setSelectionStart(this.selectionStart + offset); + this.setSelectionEnd(this.selectionStart); }, /** @@ -293,8 +280,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ swapSelectionPoints: function() { var swapSel = this.selectionEnd; - this.selectionEnd = this.selectionStart; - this.selectionStart = swapSel; + this.setSelectionEnd(this.selectionStart); + this.setSelectionStart(swapSel); }, /** @@ -305,18 +292,19 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionEnd === this.selectionStart) { this._selectionDirection = 'right'; } - var prop = this._selectionDirection === 'right' ? 'selectionEnd' : 'selectionStart'; - this[prop] += offset; + if (this._selectionDirection === 'right') { + this.setSelectionEnd(this.selectionEnd + offset); + } + else { + this.setSelectionStart(this.selectionStart + offset); + } if (this.selectionEnd < this.selectionStart && this._selectionDirection === 'left') { this.swapSelectionPoints(); this._selectionDirection = 'right'; } if (this.selectionEnd > this.text.length) { - this.selectionEnd = this.text.length; + this.setSelectionEnd(this.text.length); } - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** @@ -335,9 +323,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot var textBeforeCursor = this.text.slice(0, selectionProp), textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '', - textLines = this.text.split(this._reNewline), _char, - widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines), + widthOfSameLineBeforeCursor = this._getLineWidth(this.ctx, cursorLocation.lineIndex), lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor), widthOfCharsOnSameLineBeforeCursor = lineLeftOffset, lineIndex = cursorLocation.lineIndex; @@ -348,7 +335,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } var indexOnPrevLine = this._getIndexOnPrevLine( - cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines); + cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor); return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length; }, @@ -356,10 +343,10 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot /** * @private */ - _getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) { + _getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor) { var lineIndex = cursorLocation.lineIndex - 1, - widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines), + widthOfPreviousLine = this._getLineWidth(this.ctx, lineIndex), lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine), widthOfCharsOnPreviousLine = lineLeftOffset, indexOnPrevLine = 0, @@ -423,18 +410,16 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionEnd === this.selectionStart) { this._selectionDirection = 'left'; } - var prop = this._selectionDirection === 'right' ? 'selectionEnd' : 'selectionStart'; - this[prop] -= offset; + if (this._selectionDirection === 'right') { + this.setSelectionEnd(this.selectionEnd - offset); + } + else { + this.setSelectionStart(this.selectionStart - offset); + } if (this.selectionEnd < this.selectionStart && this._selectionDirection === 'right') { this.swapSelectionPoints(); this._selectionDirection = 'left'; } - if (this.selectionStart < 0) { - this.selectionStart = 0; - } - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** @@ -443,17 +428,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ moveCursorUpWithoutShift: function(offset) { if (this.selectionStart === this.selectionEnd) { - this.selectionStart -= offset; + this.setSelectionStart(this.selectionStart - offset); } - if (this.selectionStart < 0) { - this.selectionStart = 0; - } - this.selectionEnd = this.selectionStart; + this.setSelectionEnd(this.selectionStart); this._selectionDirection = 'left'; - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** @@ -482,14 +461,15 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @private */ _move: function(e, prop, direction) { + var propMethod = (prop === 'selectionStart' ? 'setSelectionStart' : 'setSelectionEnd'); if (e.altKey) { - this[prop] = this['findWordBoundary' + direction](this[prop]); + this[propMethod](this['findWordBoundary' + direction](this[prop])); } else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { - this[prop] = this['findLineBoundary' + direction](this[prop]); + this[propMethod](this['findLineBoundary' + direction](this[prop])); } else { - this[prop] += (direction === 'Left' ? -1 : 1); + this[propMethod](this[prop] + (direction === 'Left' ? -1 : 1)); } }, @@ -519,10 +499,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionEnd === this.selectionStart) { this._moveLeft(e, 'selectionStart'); } - this.selectionEnd = this.selectionStart; - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + this.setSelectionEnd(this.selectionStart); }, /** @@ -539,15 +516,9 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // increase selection by one if it's a newline if (this.text.charAt(this.selectionStart) === '\n') { - this.selectionStart--; - } - if (this.selectionStart < 0) { - this.selectionStart = 0; + this.setSelectionStart(this.selectionStart - 1); } } - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** @@ -586,15 +557,9 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // increase selection by one if it's a newline if (this.text.charAt(this.selectionEnd - 1) === '\n') { - this.selectionEnd++; - } - if (this.selectionEnd > this.text.length) { - this.selectionEnd = this.text.length; + this.setSelectionEnd(this.selectionEnd + 1); } } - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** @@ -606,22 +571,16 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this.selectionStart === this.selectionEnd) { this._moveRight(e, 'selectionStart'); - this.selectionEnd = this.selectionStart; + this.setSelectionEnd(this.selectionStart); } else { - this.selectionEnd += this.getNumNewLinesInSelectedText(); - if (this.selectionEnd > this.text.length) { - this.selectionEnd = this.text.length; - } - this.selectionStart = this.selectionEnd; + this.setSelectionEnd(this.selectionEnd + this.getNumNewLinesInSelectedText()); + this.setSelectionStart(this.selectionEnd); } - - this.fire('selection:changed'); - this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** - * Inserts a character where cursor is (replacing selection if one exists) + * Removes characters selected by selection * @param {Event} e Event object */ removeChars: function(e) { @@ -632,15 +591,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this._removeCharsFromTo(this.selectionStart, this.selectionEnd); } - this.selectionEnd = this.selectionStart; + this.setSelectionEnd(this.selectionStart); this._removeExtraneousStyles(); - if (this.canvas) { - // TODO: double renderAll gets rid of text box shift happenning sometimes - // need to find out what exactly causes it and fix it - this.canvas.renderAll().renderAll(); - } + this._clearCache(); + this.canvas && this.canvas.renderAll(); this.setCoords(); this.fire('changed'); @@ -659,20 +615,19 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); this._removeCharsFromTo(leftLineBoundary, this.selectionStart); - this.selectionStart = leftLineBoundary; + this.setSelectionStart(leftLineBoundary); } else if (e.altKey) { // remove all till the start of current word var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart); this._removeCharsFromTo(leftWordBoundary, this.selectionStart); - this.selectionStart = leftWordBoundary; + this.setSelectionStart(leftWordBoundary); } else { var isBeginningOfLine = this.text.slice(this.selectionStart - 1, this.selectionStart) === '\n'; this.removeStyleObject(isBeginningOfLine); - - this.selectionStart--; + this.setSelectionStart(this.selectionStart - 1); this.text = this.text.slice(0, this.selectionStart) + this.text.slice(this.selectionStart + 1); } diff --git a/src/mixins/object.svg_export.js b/src/mixins/object.svg_export.js index ce6ccff1..c7424041 100644 --- a/src/mixins/object.svg_export.js +++ b/src/mixins/object.svg_export.js @@ -23,7 +23,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', visibility = this.visible ? '' : ' visibility: hidden;', - filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + filter = this.shadow ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; return [ 'stroke: ', stroke, '; ', diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 2700e227..90a865fb 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -24,10 +24,10 @@ * Select text vertically: shift + up, shift + down * Move cursor by word: alt + left, alt + right * Select words: shift + alt + left, shift + alt + right - * Move cursor to line start/end: cmd + left, cmd + right - * Select till start/end of line: cmd + shift + left, cmd + shift + right + * Move cursor to line start/end: cmd + left, cmd + right or home, end + * Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end * Jump to start/end of text: cmd + up, cmd + down - * Select till start/end of text: cmd + shift + up, cmd + shift + down + * Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown * Delete character: backspace * Delete word: alt + backspace * Delete line: cmd + backspace @@ -36,6 +36,7 @@ * Paste text: ctrl/cmd + v * Cut text: ctrl/cmd + x * Select entire text: ctrl/cmd + a + * Quit editing tab or esc * * *

Supported mouse/touch combination

@@ -146,18 +147,13 @@ * @type Boolean * @default */ - _skipFillStrokeCheck: true, + _skipFillStrokeCheck: false, /** * @private */ _reSpace: /\s|\n/, - /** - * @private - */ - _fontSizeFraction: 4, - /** * @private */ @@ -191,10 +187,14 @@ fabric.IText.instances.push(this); - // caching - this.__lineWidths = { }; - this.__lineHeights = { }; - this.__lineOffsets = { }; + }, + + /** + * @private + */ + _clearCache: function() { + this.callSuper('_clearCache'); + this.__maxFontHeights = [ ]; }, /** @@ -222,12 +222,13 @@ * @param {Number} index Index to set selection start to */ setSelectionStart: function(index) { + index = Math.max(index, 0); if (this.selectionStart !== index) { this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + this.selectionStart = index; } - this.selectionStart = index; - this.hiddenTextarea && (this.hiddenTextarea.selectionStart = index); + this._updateTextarea(); }, /** @@ -235,12 +236,13 @@ * @param {Number} index Index to set selection end to */ setSelectionEnd: function(index) { + index = Math.min(index, this.text.length); if (this.selectionEnd !== index) { this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + this.selectionEnd = index; } - this.selectionEnd = index; - this.hiddenTextarea && (this.hiddenTextarea.selectionEnd = index); + this._updateTextarea(); }, /** @@ -405,18 +407,14 @@ */ _getCursorBoundaries: function(chars, typeOfBoundaries) { - var cursorLocation = this.get2DCursorLocation(), - - textLines = this.text.split(this._reNewline), - // left/top are left/top of entire text box // leftOffset/topOffset are offset from that left/top point of a text box - left = Math.round(this._getLeftOffset()), + var left = Math.round(this._getLeftOffset()), top = this._getTopOffset(), offsets = this._getCursorBoundariesOffsets( - chars, typeOfBoundaries, cursorLocation, textLines); + chars, typeOfBoundaries); return { left: left, @@ -429,26 +427,19 @@ /** * @private */ - _getCursorBoundariesOffsets: function(chars, typeOfBoundaries, cursorLocation, textLines) { + _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) { var lineLeftOffset = 0, lineIndex = 0, charIndex = 0, - - leftOffset = 0, - topOffset = typeOfBoundaries === 'cursor' - // selection starts at the very top of the line, - // whereas cursor starts at the padding created by line height - ? (this._getHeightOfLine(this.ctx, 0) - - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)) - : 0; + topOffset = 0, + leftOffset = 0; for (var i = 0; i < this.selectionStart; i++) { if (chars[i] === '\n') { leftOffset = 0; - var index = lineIndex + (typeOfBoundaries === 'cursor' ? 1 : 0); - topOffset += this._getCachedLineHeight(index); + topOffset += this._getHeightOfLine(this.ctx, lineIndex); lineIndex++; charIndex = 0; @@ -458,10 +449,12 @@ charIndex++; } - lineLeftOffset = this._getCachedLineOffset(lineIndex, textLines); + lineLeftOffset = this._getCachedLineOffset(lineIndex); + } + if (typeOfBoundaries === 'cursor') { + topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, lineIndex) / this.lineHeight + - this.getCurrentCharFontSize(lineIndex, charIndex) * (1 - this._fontSizeFraction); } - - this._clearCache(); return { top: topOffset, @@ -473,33 +466,8 @@ /** * @private */ - _clearCache: function() { - this.__lineWidths = { }; - this.__lineHeights = { }; - this.__lineOffsets = { }; - }, - - /** - * @private - */ - _getCachedLineHeight: function(index) { - return this.__lineHeights[index] || - (this.__lineHeights[index] = this._getHeightOfLine(this.ctx, index)); - }, - - /** - * @private - */ - _getCachedLineWidth: function(lineIndex, textLines) { - return this.__lineWidths[lineIndex] || - (this.__lineWidths[lineIndex] = this._getWidthOfLine(this.ctx, lineIndex, textLines)); - }, - - /** - * @private - */ - _getCachedLineOffset: function(lineIndex, textLines) { - var widthOfLine = this._getCachedLineWidth(lineIndex, textLines); + _getCachedLineOffset: function(lineIndex) { + var widthOfLine = this._getLineWidth(this.ctx, lineIndex); return this.__lineOffsets[lineIndex] || (this.__lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine)); @@ -549,30 +517,29 @@ var start = this.get2DCursorLocation(this.selectionStart), end = this.get2DCursorLocation(this.selectionEnd), startLine = start.lineIndex, - endLine = end.lineIndex, - textLines = this.text.split(this._reNewline); + endLine = end.lineIndex; for (var i = startLine; i <= endLine; i++) { - var lineOffset = this._getCachedLineOffset(i, textLines) || 0, - lineHeight = this._getCachedLineHeight(i), - boxWidth = 0; + var lineOffset = this._getCachedLineOffset(i) || 0, + lineHeight = this._getHeightOfLine(this.ctx, i), + boxWidth = 0, line = this._textLines[i]; if (i === startLine) { - for (var j = 0, len = textLines[i].length; j < len; j++) { + for (var j = 0, len = line.length; j < len; j++) { if (j >= start.charIndex && (i !== endLine || j < end.charIndex)) { - boxWidth += this._getWidthOfChar(ctx, textLines[i][j], i, j); + boxWidth += this._getWidthOfChar(ctx, line[j], i, j); } if (j < start.charIndex) { - lineOffset += this._getWidthOfChar(ctx, textLines[i][j], i, j); + lineOffset += this._getWidthOfChar(ctx, line[j], i, j); } } } else if (i > startLine && i < endLine) { - boxWidth += this._getCachedLineWidth(i, textLines) || 5; + boxWidth += this._getLineWidth(ctx, i) || 5; } else if (i === endLine) { for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) { - boxWidth += this._getWidthOfChar(ctx, textLines[i][j2], i, j2); + boxWidth += this._getWidthOfChar(ctx, line[j2], i, j2); } } @@ -608,10 +575,8 @@ : 0; // set proper line offset - var textLines = this.text.split(this._reNewline), - lineWidth = this._getWidthOfLine(ctx, lineIndex, textLines), - lineHeight = this._getHeightOfLine(ctx, lineIndex, textLines), - lineLeftOffset = this._getLineLeftOffset(lineWidth), + var lineHeight = this._getHeightOfLine(ctx, lineIndex), + lineLeftOffset = this._getCachedLineOffset(lineIndex), chars = line.split(''), prevStyle, charsToRender = ''; @@ -619,7 +584,7 @@ left += lineLeftOffset || 0; ctx.save(); - + top -= lineHeight / this.lineHeight * this._fontSizeFraction; for (var i = 0, len = chars.length; i <= len; i++) { prevStyle = prevStyle || this.getCurrentCharStyle(lineIndex, i); var thisStyle = this.getCurrentCharStyle(lineIndex, i + 1); @@ -649,7 +614,7 @@ if (method === 'fillText' && this.fill) { this.callSuper('_renderChars', method, ctx, line, left, top); } - if (method === 'strokeText' && this.stroke) { + if (method === 'strokeText' && ((this.stroke && this.strokeWidth > 0) || this.skipFillStrokeCheck)) { this.callSuper('_renderChars', method, ctx, line, left, top); } }, @@ -666,7 +631,8 @@ * @param {Number} lineHeight Height of the line */ _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { - var decl, charWidth, charHeight; + var decl, charWidth, charHeight, + offset = this._fontSizeFraction * lineHeight / this.lineHeight; if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { @@ -684,7 +650,7 @@ ctx.strokeText(_char, left, top); } - this._renderCharDecoration(ctx, decl, left, top, charWidth, lineHeight, charHeight); + this._renderCharDecoration(ctx, decl, left, top, offset, charWidth, charHeight); ctx.restore(); ctx.translate(charWidth, 0); @@ -697,7 +663,7 @@ ctx[method](_char, left, top); } charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i); - this._renderCharDecoration(ctx, null, left, top, charWidth, lineHeight); + this._renderCharDecoration(ctx, null, left, top, offset, charWidth, this.fontSize); ctx.translate(ctx.measureText(_char).width, 0); } @@ -725,58 +691,42 @@ * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _renderCharDecoration: function(ctx, styleDeclaration, left, top, charWidth, lineHeight, charHeight) { + _renderCharDecoration: function(ctx, styleDeclaration, left, top, offset, charWidth, charHeight) { var textDecoration = styleDeclaration ? (styleDeclaration.textDecoration || this.textDecoration) - : this.textDecoration, - - fontSize = (styleDeclaration ? styleDeclaration.fontSize : null) || this.fontSize; + : this.textDecoration; if (!textDecoration) { return; } if (textDecoration.indexOf('underline') > -1) { - this._renderCharDecorationAtOffset( - ctx, + ctx.fillRect( left, - top + (this.fontSize / this._fontSizeFraction), - charWidth, - 0, - this.fontSize / 20 + top + charHeight / 10, + charWidth , + charHeight / 15 ); } if (textDecoration.indexOf('line-through') > -1) { - this._renderCharDecorationAtOffset( - ctx, + ctx.fillRect( left, - top + (this.fontSize / this._fontSizeFraction), + top - charHeight * (this._fontSizeFraction + this._fontSizeMult - 1) + charHeight / 15, charWidth, - charHeight / 2, - fontSize / 20 + charHeight / 15 ); } if (textDecoration.indexOf('overline') > -1) { - this._renderCharDecorationAtOffset( - ctx, + ctx.fillRect( left, - top, + top - (this._fontSizeMult - this._fontSizeFraction) * charHeight, charWidth, - lineHeight - (this.fontSize / this._fontSizeFraction), - this.fontSize / 20 + charHeight / 15 ); } }, - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderCharDecorationAtOffset: function(ctx, left, top, charWidth, offset, thickness) { - ctx.fillRect(left, top - offset, charWidth, thickness); - }, - /** * @private * @param {String} method @@ -785,27 +735,26 @@ */ _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine - top += this.fontSize / 4; + // the adding 0.03 is just to align text with itext by overlap test + top += this.fontSize * (this._fontSizeFraction + 0.03); this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines */ - _renderTextDecoration: function(ctx, textLines) { + _renderTextDecoration: function(ctx) { if (this.isEmptyStyles()) { - return this.callSuper('_renderTextDecoration', ctx, textLines); + return this.callSuper('_renderTextDecoration', ctx); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines */ - _renderTextLinesBackground: function(ctx, textLines) { + _renderTextLinesBackground: function(ctx) { if (!this.textBackgroundColor && !this.styles) { return; } @@ -816,43 +765,42 @@ ctx.fillStyle = this.textBackgroundColor; } - var lineHeights = 0, - fractionOfFontSize = this.fontSize / this._fontSizeFraction; + var lineHeights = 0; - for (var i = 0, len = textLines.length; i < len; i++) { + for (var i = 0, len = this._textLines.length; i < len; i++) { - var heightOfLine = this._getHeightOfLine(ctx, i, textLines); - if (textLines[i] === '') { + var heightOfLine = this._getHeightOfLine(ctx, i); + if (this._textLines[i] === '') { lineHeights += heightOfLine; continue; } - var lineWidth = this._getWidthOfLine(ctx, i, textLines), - lineLeftOffset = this._getLineLeftOffset(lineWidth); + var lineWidth = this._getLineWidth(ctx, i), + lineLeftOffset = this._getCachedLineOffset(i); if (this.textBackgroundColor) { ctx.fillStyle = this.textBackgroundColor; ctx.fillRect( this._getLeftOffset() + lineLeftOffset, - this._getTopOffset() + lineHeights + fractionOfFontSize, + this._getTopOffset() + lineHeights, lineWidth, - heightOfLine + heightOfLine / this.lineHeight ); } if (this.styles[i]) { - for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { + for (var j = 0, jlen = this._textLines[i].length; j < jlen; j++) { if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) { - var _char = textLines[i][j]; + var _char = this._textLines[i][j]; ctx.fillStyle = this.styles[i][j].textBackgroundColor; ctx.fillRect( - this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j, textLines), - this._getTopOffset() + lineHeights + fractionOfFontSize, - this._getWidthOfChar(ctx, _char, i, j, textLines) + 1, - heightOfLine + this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j), + this._getTopOffset() + lineHeights, + this._getWidthOfChar(ctx, _char, i, j) + 1, + heightOfLine / this.lineHeight ); } } @@ -867,12 +815,10 @@ */ _getCacheProp: function(_char, styleDeclaration) { return _char + - styleDeclaration.fontFamily + styleDeclaration.fontSize + styleDeclaration.fontWeight + styleDeclaration.fontStyle + - styleDeclaration.shadow; }, @@ -1005,9 +951,8 @@ * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _getWidthOfCharAt: function(ctx, lineIndex, charIndex, lines) { - lines = lines || this.text.split(this._reNewline); - var _char = lines[lineIndex].split('')[charIndex]; + _getWidthOfCharAt: function(ctx, lineIndex, charIndex) { + var _char = this._textLines[lineIndex].split('')[charIndex]; return this._getWidthOfChar(ctx, _char, lineIndex, charIndex); }, @@ -1015,9 +960,8 @@ * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _getHeightOfCharAt: function(ctx, lineIndex, charIndex, lines) { - lines = lines || this.text.split(this._reNewline); - var _char = lines[lineIndex].split('')[charIndex]; + _getHeightOfCharAt: function(ctx, lineIndex, charIndex) { + var _char = this._textLines[lineIndex].split('')[charIndex]; return this._getHeightOfChar(ctx, _char, lineIndex, charIndex); }, @@ -1025,10 +969,10 @@ * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _getWidthOfCharsAt: function(ctx, lineIndex, charIndex, lines) { + _getWidthOfCharsAt: function(ctx, lineIndex, charIndex) { var width = 0; for (var i = 0; i < charIndex; i++) { - width += this._getWidthOfCharAt(ctx, lineIndex, i, lines); + width += this._getWidthOfCharAt(ctx, lineIndex, i); } return width; }, @@ -1037,11 +981,12 @@ * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _getWidthOfLine: function(ctx, lineIndex, textLines) { - // if (!this.styles[lineIndex]) { - // return this.callSuper('_getLineWidth', ctx, textLines[lineIndex]); - // } - return this._getWidthOfCharsAt(ctx, lineIndex, textLines[lineIndex].length, textLines); + _getLineWidth: function(ctx, lineIndex) { + if (this.__lineWidths[lineIndex]) { + return this.__lineWidths[lineIndex]; + } + this.__lineWidths[lineIndex] = this._getWidthOfCharsAt(ctx, lineIndex, this._textLines[lineIndex].length); + return this.__lineWidths[lineIndex]; }, /** @@ -1085,33 +1030,13 @@ * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _getTextWidth: function(ctx, textLines) { - - if (this.isEmptyStyles()) { - return this.callSuper('_getTextWidth', ctx, textLines); + _getHeightOfLine: function(ctx, lineIndex) { + if (this.__lineHeights[lineIndex]) { + return this.__lineHeights[lineIndex]; } - var maxWidth = this._getWidthOfLine(ctx, 0, textLines); - - for (var i = 1, len = textLines.length; i < len; i++) { - var currentLineWidth = this._getWidthOfLine(ctx, i, textLines); - if (currentLineWidth > maxWidth) { - maxWidth = currentLineWidth; - } - } - return maxWidth; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _getHeightOfLine: function(ctx, lineIndex, textLines) { - - textLines = textLines || this.text.split(this._reNewline); - - var maxHeight = this._getHeightOfChar(ctx, textLines[lineIndex][0], lineIndex, 0), - line = textLines[lineIndex], + var line = this._textLines[lineIndex], + maxHeight = this._getHeightOfChar(ctx, line[0], lineIndex, 0), chars = line.split(''); for (var i = 1, len = chars.length; i < len; i++) { @@ -1120,31 +1045,23 @@ maxHeight = currentCharHeight; } } - - return maxHeight * this.lineHeight; + this.__maxFontHeights[lineIndex] = maxHeight; + this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; + return this.__lineHeights[lineIndex]; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines */ - _getTextHeight: function(ctx, textLines) { + _getTextHeight: function(ctx) { var height = 0; - for (var i = 0, len = textLines.length; i < len; i++) { - height += this._getHeightOfLine(ctx, i, textLines); + for (var i = 0, len = this._textLines.length; i < len; i++) { + height += this._getHeightOfLine(ctx, i); } return height; }, - /** - * @private - */ - _getTopOffset: function() { - var topOffset = fabric.Text.prototype._getTopOffset.call(this); - return topOffset - (this.fontSize / this._fontSizeFraction); - }, - /** * This method is overwritten to account for different top offset * @private @@ -1159,7 +1076,7 @@ ctx.fillRect( this._getLeftOffset(), - this._getTopOffset() + (this.fontSize / this._fontSizeFraction), + this._getTopOffset(), this.width, this.height ); diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 05f6a94a..9e61fddb 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -23,9 +23,7 @@ 'textAlign', 'fontStyle', 'lineHeight', - 'textBackgroundColor', - 'useNative', - 'path' + 'textBackgroundColor' ); /** @@ -258,7 +256,7 @@ * @type Number * @default */ - lineHeight: 1.3, + lineHeight: 1.16, /** * Background color of text lines @@ -267,20 +265,6 @@ */ textBackgroundColor: '', - /** - * URL of a font file, when using Cufon - * @type String | null - * @default - */ - path: null, - - /** - * 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}) @@ -305,6 +289,18 @@ */ 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 @@ -313,7 +309,6 @@ */ initialize: function(text, options) { options = options || { }; - this.text = text; this.__skipDimension = true; this.setOptions(options); @@ -329,8 +324,13 @@ if (this.__skipDimension) { return; } - var canvasEl = fabric.util.createCanvasElement(); - this._render(canvasEl.getContext('2d')); + this._clearCache(); + + var ctx = fabric.util.createCanvasElement().getContext('2d'); + this._textLines = this.text.split(this._reNewline); + this._setTextStyles(ctx); + this.width = this._getTextWidth(ctx); + this.height = this._getTextHeight(ctx); }, /** @@ -348,54 +348,31 @@ */ _render: function(ctx) { - if (typeof Cufon === 'undefined' || this.useNative === true) { - this._renderViaNative(ctx); - } - else { - this._renderViaCufon(ctx); - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderViaNative: function(ctx) { - var textLines = this.text.split(this._reNewline); - - this._setTextStyles(ctx); - - this.width = this._getTextWidth(ctx, textLines); - this.height = this._getTextHeight(ctx, textLines); - this.clipTo && fabric.util.clipContext(this, ctx); - this._renderTextBackground(ctx, textLines); + this._renderTextBackground(ctx); this._translateForTextAlign(ctx); - this._renderText(ctx, textLines); + this._renderText(ctx); if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.restore(); } - this._renderTextDecoration(ctx, textLines); + this._renderTextDecoration(ctx); this.clipTo && ctx.restore(); - - this._setBoundaries(ctx, textLines); - this._totalLineHeight = 0; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _renderText: function(ctx, textLines) { + _renderText: function(ctx) { ctx.save(); this._setOpacity(ctx); this._setShadow(ctx); this._setupCompositeOperation(ctx); - this._renderTextFill(ctx, textLines); - this._renderTextStroke(ctx, textLines); + this._renderTextFill(ctx); + this._renderTextStroke(ctx); this._restoreCompositeOperation(ctx); this._removeShadow(ctx); ctx.restore(); @@ -412,34 +389,11 @@ } }, - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines - */ - _setBoundaries: function(ctx, textLines) { - this._boundaries = [ ]; - - for (var i = 0, len = textLines.length; i < len; i++) { - - var lineWidth = this._getLineWidth(ctx, textLines[i]), - lineLeftOffset = this._getLineLeftOffset(lineWidth); - - this._boundaries.push({ - height: this.fontSize * this.lineHeight, - width: lineWidth, - left: lineLeftOffset - }); - } - }, - /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _setTextStyles: function(ctx) { - this._setFillStyles(ctx); - this._setStrokeStyles(ctx); ctx.textBaseline = 'alphabetic'; if (!this.skipTextAlign) { ctx.textAlign = this.textAlign; @@ -450,24 +404,22 @@ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines * @return {Number} Height of fabric.Text object */ - _getTextHeight: function(ctx, textLines) { - return this.fontSize * textLines.length * this.lineHeight; + _getTextHeight: function() { + return this._textLines.length * this._getHeightOfLine(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines * @return {Number} Maximum width of fabric.Text object */ - _getTextWidth: function(ctx, textLines) { - var maxWidth = ctx.measureText(textLines[0] || '|').width; + _getTextWidth: function(ctx) { + var maxWidth = this._getLineWidth(ctx, 0); - for (var i = 1, len = textLines.length; i < len; i++) { - var currentLineWidth = ctx.measureText(textLines[i]).width; + for (var i = 1, len = this._textLines.length; i < len; i++) { + var currentLineWidth = this._getLineWidth(ctx, i); if (currentLineWidth > maxWidth) { maxWidth = currentLineWidth; } @@ -498,7 +450,7 @@ */ _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // lift the line by quarter of fontSize - top -= this.fontSize / 4; + top -= this.fontSize * this._fontSizeFraction; // short-circuit if (this.textAlign !== 'justify') { @@ -506,7 +458,7 @@ return; } - var lineWidth = ctx.measureText(line).width, + var lineWidth = this._getLineWidth(ctx, i), totalWidth = this.width; if (totalWidth > lineWidth) { @@ -547,28 +499,27 @@ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines */ - _renderTextFill: function(ctx, textLines) { + _renderTextFill: function(ctx) { if (!this.fill && !this._skipFillStrokeCheck) { return; } - this._boundaries = [ ]; var lineHeights = 0; - for (var i = 0, len = textLines.length; i < len; i++) { - var heightOfLine = this._getHeightOfLine(ctx, i, textLines); - lineHeights += heightOfLine; + 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, - textLines[i], + this._textLines[i], this._getLeftOffset(), - this._getTopOffset() + lineHeights, + this._getTopOffset() + lineHeights + maxHeight, i ); + lineHeights += heightOfLine; } if (this.shadow && !this.shadow.affectStroke) { this._removeShadow(ctx); @@ -578,9 +529,8 @@ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines */ - _renderTextStroke: function(ctx, textLines) { + _renderTextStroke: function(ctx) { if ((!this.stroke || this.strokeWidth === 0) && !this._skipFillStrokeCheck) { return; } @@ -588,6 +538,7 @@ var lineHeights = 0; 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) { @@ -597,25 +548,26 @@ } ctx.beginPath(); - for (var i = 0, len = textLines.length; i < len; i++) { - var heightOfLine = this._getHeightOfLine(ctx, i, textLines); - lineHeights += heightOfLine; + 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, - textLines[i], + this._textLines[i], this._getLeftOffset(), - this._getTopOffset() + lineHeights, + this._getTopOffset() + lineHeights + maxHeight, i ); + lineHeights += heightOfLine; } ctx.closePath(); ctx.restore(); }, _getHeightOfLine: function() { - return this.fontSize * this.lineHeight; + return this.fontSize * this._fontSizeMult * this.lineHeight; }, /** @@ -623,9 +575,9 @@ * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ - _renderTextBackground: function(ctx, textLines) { + _renderTextBackground: function(ctx) { this._renderTextBoxBackground(ctx); - this._renderTextLinesBackground(ctx, textLines); + this._renderTextLinesBackground(ctx); }, /** @@ -653,9 +605,9 @@ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} textLines Array of all text lines */ - _renderTextLinesBackground: function(ctx, textLines) { + _renderTextLinesBackground: function(ctx) { + var lineTopOffset = 0, heightOfLine = this._getHeightOfLine(); if (!this.textBackgroundColor) { return; } @@ -663,20 +615,21 @@ ctx.save(); ctx.fillStyle = this.textBackgroundColor; - for (var i = 0, len = textLines.length; i < len; i++) { + for (var i = 0, len = this._textLines.length; i < len; i++) { - if (textLines[i] !== '') { + if (this._textLines[i] !== '') { - var lineWidth = this._getLineWidth(ctx, textLines[i]), + var lineWidth = this._getLineWidth(ctx, i), lineLeftOffset = this._getLineLeftOffset(lineWidth); ctx.fillRect( this._getLeftOffset() + lineLeftOffset, - this._getTopOffset() + (i * this.fontSize * this.lineHeight), + this._getTopOffset() + lineTopOffset, lineWidth, - this.fontSize * this.lineHeight + this.fontSize * this._fontSizeMult ); } + lineTopOffset += heightOfLine; } ctx.restore(); }, @@ -698,53 +651,85 @@ /** * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} line Text line - * @return {Number} Line width */ - _getLineWidth: function(ctx, line) { - return this.textAlign === 'justify' - ? this.width - : ctx.measureText(line).width; + _clearCache: function() { + this.__lineWidths = [ ]; + this.__lineHeights = [ ]; + this.__lineOffsets = [ ]; + }, + + /** + * @private + */ + _shouldClearCache: function() { + var shouldClear = false; + 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 {Array} textLines Array of all text lines + * @return {Number} Line width */ - _renderTextDecoration: function(ctx, textLines) { + _getLineWidth: function(ctx, lineIndex) { + if (this.__lineWidths[lineIndex]) { + return this.__lineWidths[lineIndex]; + } + this.__lineWidths[lineIndex] = this.textAlign === 'justify' ? + this.width : ctx.measureText(this._textLines[lineIndex]).width; + return this.__lineWidths[lineIndex]; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextDecoration: function(ctx) { if (!this.textDecoration) { return; } - // var halfOfVerticalBox = this.originY === 'top' ? 0 : this._getTextHeight(ctx, textLines) / 2; - var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2, - _this = this; + var halfOfVerticalBox = this.height / 2, + _this = this, offsets = []; /** @ignore */ - function renderLinesAtOffset(offset) { - for (var i = 0, len = textLines.length; i < len; i++) { + function renderLinesAtOffset(offsets) { + var i, lineHeight = 0, len, j, oLen; + for (i = 0, len = _this._textLines.length; i < len; i++) { - var lineWidth = _this._getLineWidth(ctx, textLines[i]), - lineLeftOffset = _this._getLineLeftOffset(lineWidth); + var lineWidth = _this._getLineWidth(ctx, i), + lineLeftOffset = _this._getLineLeftOffset(lineWidth), + heightOfLine = _this._getHeightOfLine(ctx, i); - ctx.fillRect( - _this._getLeftOffset() + lineLeftOffset, - ~~((offset + (i * _this._getHeightOfLine(ctx, i, textLines))) - halfOfVerticalBox), - lineWidth, - 1); + 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) { - renderLinesAtOffset(this.fontSize * this.lineHeight); + offsets.push(0.85); // 1 - 3/16 } if (this.textDecoration.indexOf('line-through') > -1) { - renderLinesAtOffset(this.fontSize * this.lineHeight - this.fontSize / 2); + offsets.push(0.43); } if (this.textDecoration.indexOf('overline') > -1) { - renderLinesAtOffset(this.fontSize * this.lineHeight - this.fontSize); + offsets.push(-0.12); + } + + if (offsets.length > 0) { + renderLinesAtOffset(offsets); } }, @@ -772,10 +757,19 @@ } ctx.save(); + this._setTextStyles(ctx); + + if (this._shouldClearCache()) { + this._clearCache(); + this._textLines = this.text.split(this._reNewline); + this.width = this._getTextWidth(ctx); + this.height = this._getTextHeight(ctx); + } if (!noTransform) { this.transform(ctx); } - + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup) { @@ -806,9 +800,7 @@ lineHeight: this.lineHeight, textDecoration: this.textDecoration, textAlign: this.textAlign, - path: this.path, - textBackgroundColor: this.textBackgroundColor, - useNative: this.useNative + textBackgroundColor: this.textBackgroundColor }); if (!this.includeDefaultValues) { this._removeDefaultValues(object); @@ -823,16 +815,10 @@ * @return {String} svg representation of an instance */ toSVG: function(reviver) { - var markup = [ ], - textLines = this.text.split(this._reNewline), - offsets = this._getSVGLeftTopOffsets(textLines), - textAndBg = this._getSVGTextAndBg(offsets.lineTop, offsets.textLeft, textLines), - shadowSpans = this._getSVGShadows(offsets.lineTop, textLines); - - // move top offset by an ascent - offsets.textTop += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); - - this._wrapSVGTextAndBg(markup, textAndBg, shadowSpans, offsets); + 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(''); }, @@ -840,19 +826,14 @@ /** * @private */ - _getSVGLeftTopOffsets: function(textLines) { - var lineTop = this.useNative - ? this.fontSize * this.lineHeight - : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), - - textLeft = -(this.width/2), - textTop = this.useNative - ? (this.fontSize * this.lineHeight - 0.25 * this.fontSize) // to lift by 1 / 4 of font height. - : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight; + _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), + textTop: textTop + (this.group && this.group.type === 'path-group' ? -this.top : 0), lineTop: lineTop }; }, @@ -860,99 +841,43 @@ /** * @private */ - _wrapSVGTextAndBg: function(markup, textAndBg, shadowSpans, offsets) { + _wrapSVGTextAndBg: function(markup, textAndBg) { markup.push( - '\n', + '\t\n', textAndBg.textBgRects.join(''), - '', - shadowSpans.join(''), + 'style="', this.getSvgStyles(), '" >', textAndBg.textSpans.join(''), '\n', - '\n' + '\t\n' ); }, /** * @private - * @param {Number} lineHeight - * @param {Array} textLines Array of all text lines - * @return {Array} - */ - _getSVGShadows: function(lineHeight, textLines) { - var shadowSpans = [], - i, len, - lineTopOffsetMultiplier = 1; - - if (!this.shadow || !this._boundaries) { - return shadowSpans; - } - - for (i = 0, len = textLines.length; i < len; 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 - * @param {Number} lineHeight + * @param {Number} textTopOffset Text top offset * @param {Number} textLeftOffset Text left offset - * @param {Array} textLines Array of all text lines * @return {Object} */ - _getSVGTextAndBg: function(lineHeight, textLeftOffset, textLines) { + _getSVGTextAndBg: function(textTopOffset, textLeftOffset) { var textSpans = [ ], textBgRects = [ ], - lineTopOffsetMultiplier = 1; - + height = 0; // bounding-box background this._setSVGBg(textBgRects); // text and text-background - for (var i = 0, len = textLines.length; i < len; i++) { - if (textLines[i] !== '') { - this._setSVGTextLineText(textLines[i], i, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); - lineTopOffsetMultiplier = 1; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (this.textBackgroundColor) { + this._setSVGTextLineBg(textBgRects, i, textLeftOffset, textTopOffset, height); } - 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; - } - - this._setSVGTextLineBg(textBgRects, i, textLeftOffset, lineHeight); + this._setSVGTextLineText(i, textSpans, height, textLeftOffset, textTopOffset, textBgRects); + height += this._getHeightOfLine(this.ctx, i); } return { @@ -961,56 +886,52 @@ }; }, - _setSVGTextLineText: function(textLine, i, textSpans, lineHeight, lineTopOffsetMultiplier) { - var lineLeftOffset = (this._boundaries && this._boundaries[i]) - ? toFixed(this._boundaries[i].left, 2) - : 0; - + _setSVGTextLineText: function(i, textSpans, height, textLeftOffset, textTopOffset) { + var yPos = this.fontSize * (this._fontSizeMult - this._fontSizeFraction) + - textTopOffset + height - this.height / 2; textSpans.push( ' elements since setting opacity // on containing one doesn't work in Illustrator this._getFillAttributes(this.fill), '>', - fabric.util.string.escapeXml(textLine), + fabric.util.string.escapeXml(this._textLines[i]), '' ); }, - _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, lineHeight) { + _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, textTopOffset, height) { textBgRects.push( - '\n'); }, _setSVGBg: function(textBgRects) { - if (this.backgroundColor && this._boundaries) { + if (this.backgroundColor) { textBgRects.push( - ''); + toFixed(this.height, 4), + '">\n'); } }, @@ -1039,9 +960,6 @@ * @chainable */ _set: function(key, value) { - if (key === 'fontFamily' && this.path) { - this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); - } this.callSuper('_set', key, value); if (key in this._dimensionAffectingProps) { @@ -1107,7 +1025,6 @@ if (!options.originX) { options.originX = 'left'; } - options.top += options.fontSize / 4; var text = new fabric.Text(element.textContent, options), /* Adjust positioning: @@ -1124,7 +1041,7 @@ } text.set({ left: text.getLeft() + offX, - top: text.getTop() - text.getHeight() / 2 + top: text.getTop() - text.getHeight() / 2 + text.fontSize * (0.18 + text._fontSizeFraction) /* 0.3 is the old lineHeight */ }); return text; diff --git a/src/shapes/text.cufon.js b/src/shapes/text.cufon.js deleted file mode 100644 index 63aa47bd..00000000 --- a/src/shapes/text.cufon.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ -fabric.util.object.extend(fabric.Text.prototype, { - _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.shadow && this.shadow.toString(), - 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; - - el = null; - - // need to set coords _after_ the width/height was retreived from Cufon - this.setCoords(); - }, - - /** - * @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); - - //jscs:disable requireCamelCaseOrUpperCaseIdentifiers - if (typeof G_vmlCanvasManager === 'undefined') { - el.innerHTML = this.text; - } - //jscs:enable requireCamelCaseOrUpperCaseIdentifiers - 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; - } -}); diff --git a/test/unit/itext.js b/test/unit/itext.js index 114566ae..77989f54 100644 --- a/test/unit/itext.js +++ b/test/unit/itext.js @@ -9,7 +9,7 @@ 'left': 0, 'top': 0, 'width': 20, - 'height': 52, + 'height': 58.76, 'fill': 'rgb(0,0,0)', 'stroke': null, 'strokeWidth': 1, @@ -34,10 +34,8 @@ 'lineHeight': 1.3, 'textDecoration': '', 'textAlign': 'left', - 'path': null, 'backgroundColor': '', 'textBackgroundColor': '', - 'useNative': true, 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', styles: { } diff --git a/test/unit/text.js b/test/unit/text.js index 57dac0f2..0c0131ef 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -15,7 +15,7 @@ 'left': 0, 'top': 0, 'width': CHAR_WIDTH, - 'height': 52, + 'height': 52.43, 'fill': 'rgb(0,0,0)', 'stroke': null, 'strokeWidth': 1, @@ -38,17 +38,15 @@ 'fontWeight': 'normal', 'fontFamily': 'Times New Roman', 'fontStyle': '', - 'lineHeight': 1.3, + 'lineHeight': 1.16, 'textDecoration': '', 'textAlign': 'left', - 'path': null, 'textBackgroundColor': '', - 'useNative': true, 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over' }; - var TEXT_SVG = '\nx\n\n'; + var TEXT_SVG = '\t\n\t\tx\n\t\n'; test('constructor', function() { ok(fabric.Text); @@ -156,9 +154,9 @@ var expectedObject = fabric.util.object.extend(fabric.util.object.clone(REFERENCE_TEXT_OBJECT), { left: 4, - top: -6.4, + top: -3.61, width: 8, - height: 20.8, + height: 20.97, fontSize: 16, originX: 'left' }); @@ -197,9 +195,9 @@ var expectedObject = fabric.util.object.extend(fabric.util.object.clone(REFERENCE_TEXT_OBJECT), { /* left varies slightly due to node-canvas rendering */ left: fabric.util.toFixed(textWithAttrs.left + '', 2), - top: -29.2, + top: -7.72, width: CHAR_WIDTH, - height: 159.9, + height: 161.23, fill: 'rgb(255,255,255)', opacity: 0.45, stroke: 'blue',