diff --git a/src/mixins/itext_behavior.mixin.js b/src/mixins/itext_behavior.mixin.js index d6fd2334299..1ced81ce140 100644 --- a/src/mixins/itext_behavior.mixin.js +++ b/src/mixins/itext_behavior.mixin.js @@ -398,7 +398,9 @@ return; } - var newSelectionStart = this.getSelectionStartFromPointer(options.e); + var newSelectionStart = this.getSelectionStartFromPointer(options.e), + currentStart = this.selectionStart, + currentEnd = this.selectionEnd; if (newSelectionStart === this.__selectionStartOnMouseDown) { return; } @@ -410,9 +412,11 @@ this.selectionStart = newSelectionStart; this.selectionEnd = this.__selectionStartOnMouseDown; } - this._fireSelectionChanged(); - this._updateTextarea(); - this.renderCursorOrSelection(); + if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + } }, /** @@ -438,6 +442,7 @@ if (!this.hiddenTextarea || this.inCompositionMode) { return; } + this.cursorOffsetCache = { }; this.hiddenTextarea.value = this.text; this.hiddenTextarea.selectionStart = this.selectionStart; this.hiddenTextarea.selectionEnd = this.selectionEnd; diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 472ed44e763..7f15fc7f51f 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -315,7 +315,10 @@ this.oldHeight = this.height; this.callSuper('_render', ctx); this.ctx = ctx; - this.isEditing && this.renderCursorOrSelection(); + // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor + // the correct position but not at every cursor animation. + this.cursorOffsetCache = { }; + this.renderCursorOrSelection(); }, /** @@ -327,7 +330,6 @@ } var chars = this.text.split(''), boundaries, ctx; - if (this.canvas.contextTop) { ctx = this.canvas.contextTop; ctx.save(); @@ -454,13 +456,15 @@ * @private */ _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) { - + if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) { + return this.cursorOffsetCache; + } var lineLeftOffset = 0, - lineIndex = 0, charIndex = 0, topOffset = 0, - leftOffset = 0; + leftOffset = 0, + boundaries; for (var i = 0; i < this.selectionStart; i++) { if (chars[i] === '\n') { @@ -481,12 +485,16 @@ topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, lineIndex) / this.lineHeight - this.getCurrentCharFontSize(lineIndex, charIndex) * (1 - this._fontSizeFraction); } - - return { + if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) { + leftOffset -= this._getWidthOfCharSpacing(); + } + boundaries = { top: topOffset, left: leftOffset, lineLeft: lineLeftOffset }; + this.cursorOffsetCache = boundaries; + return this.cursorOffsetCache; }, /** @@ -544,6 +552,9 @@ lineOffset += this._getWidthOfChar(ctx, line[j], i, j); } } + if (j === line.length) { + boxWidth -= this._getWidthOfCharSpacing(); + } } else if (i > startLine && i < endLine) { boxWidth += this._getLineWidth(ctx, i) || 5; @@ -552,6 +563,9 @@ for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) { boxWidth += this._getWidthOfChar(ctx, line[j2], i, j2); } + if (end.charIndex === line.length) { + boxWidth -= this._getWidthOfCharSpacing(); + } } realLineHeight = lineHeight; if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) { @@ -579,24 +593,13 @@ } charOffset = charOffset || 0; - this.skipTextAlign = true; - - // set proper box offset - left -= this.textAlign === 'center' - ? (this.width / 2) - : (this.textAlign === 'right') - ? this.width - : 0; // set proper line offset var lineHeight = this._getHeightOfLine(ctx, lineIndex), - lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(ctx, lineIndex)), prevStyle, thisStyle, charsToRender = ''; - left += lineLeftOffset || 0; - ctx.save(); top -= lineHeight / this.lineHeight * this._fontSizeFraction; for (var i = charOffset, len = line.length + charOffset; i <= len; i++) { @@ -622,7 +625,6 @@ * @param {Number} top Top coordinate */ _renderCharsFast: function(method, ctx, line, left, top) { - this.skipTextAlign = false; if (method === 'fillText' && this.fill) { this.callSuper('_renderChars', method, ctx, line, left, top); @@ -646,7 +648,7 @@ _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { var charWidth, charHeight, shouldFill, shouldStroke, decl = this._getStyleDeclaration(lineIndex, i), - offset, textDecoration; + offset, textDecoration, chars; if (decl) { charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i); @@ -663,14 +665,26 @@ decl && ctx.save(); - charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl || {}); + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl || null); textDecoration = textDecoration || this.textDecoration; if (decl && decl.textBackgroundColor) { this._removeShadow(ctx); } - shouldFill && ctx.fillText(_char, left, top); - shouldStroke && ctx.strokeText(_char, left, top); + if (this.charSpacing !== 0) { + chars = _char.split(''); + charWidth = 0; + for (var j = 0, len = chars.length, char; j < len; j++) { + char = chars[j]; + shouldFill && ctx.fillText(char, left + charWidth, top); + shouldStroke && ctx.strokeText(char, left + charWidth, top); + charWidth += ctx.measureText(char).width + this._getWidthOfCharSpacing(); + } + } + else { + shouldFill && ctx.fillText(_char, left, top); + shouldStroke && ctx.strokeText(_char, left, top); + } if (textDecoration || textDecoration !== '') { offset = this._fontSizeFraction * lineHeight / this.lineHeight; @@ -826,8 +840,8 @@ * @param {Object} [decl] */ _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { - var charDecl = this._getStyleDeclaration(lineIndex, charIndex), - styleDeclaration = (decl && clone(decl)) || clone(charDecl), + var charDecl = decl || this._getStyleDeclaration(lineIndex, charIndex), + styleDeclaration = clone(charDecl), width, cacheProp, charWidthsCache; this._applyFontStyles(styleDeclaration); @@ -964,21 +978,13 @@ if (!this._isMeasuring && this.textAlign === 'justify' && this._reSpacesAndTabs.test(_char)) { return this._getWidthOfSpace(ctx, lineIndex); } - var charWidthsCache, cacheProp, - styleDeclaration = this._getStyleDeclaration(lineIndex, charIndex, true); - this._applyFontStyles(styleDeclaration); - charWidthsCache = this._getFontCache(styleDeclaration.fontFamily); - cacheProp = this._getCacheProp(_char, styleDeclaration); - - if (charWidthsCache[cacheProp] && this.caching) { - return charWidthsCache[cacheProp]; - } - else if (ctx) { - ctx.save(); - var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); - ctx.restore(); - return width; + ctx.save(); + var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); + if (this.charSpacing !== 0) { + width += this._getWidthOfCharSpacing(); } + ctx.restore(); + return width; }, /** @@ -1014,6 +1020,9 @@ _measureLine: function(ctx, lineIndex) { this._isMeasuring = true; var width = this._getWidthOfCharsAt(ctx, lineIndex, this._textLines[lineIndex].length); + if (this.charSpacing !== 0) { + width -= this._getWidthOfCharSpacing(); + } this._isMeasuring = false; return width; }, @@ -1039,8 +1048,9 @@ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} line + * @param {String} line * @param {Number} lineIndex + * @param {Number} charOffset */ _getWidthOfWords: function (ctx, line, lineIndex, charOffset) { var width = 0; diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 514244f0700..cc6a2f0fcda 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -48,10 +48,11 @@ fontFamily: true, fontStyle: true, lineHeight: true, - stroke: true, - strokeWidth: true, text: true, - textAlign: true + charSpacing: true, + textAlign: true, + stroke: false, + strokeWidth: false, }, /** @@ -319,6 +320,14 @@ */ _fontSizeMult: 1.13, + /** + * additional space between characters + * expressed in thousands of em unit + * @type Number + * @default + */ + charSpacing: 0, + /** * Constructor * @param {String} text Text string @@ -387,23 +396,8 @@ * @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); - } }, /** @@ -412,9 +406,6 @@ */ _setTextStyles: function(ctx) { ctx.textBaseline = 'alphabetic'; - if (!this.skipTextAlign) { - ctx.textAlign = this.textAlign; - } ctx.font = this._getFontDeclaration(); }, @@ -463,7 +454,7 @@ */ _renderChars: function(method, ctx, chars, left, top) { // remove Text word from method var - var shortM = method.slice(0, -4); + var shortM = method.slice(0, -4), char, width; if (this[shortM].toLive) { var offsetX = -this.width / 2 + this[shortM].offsetX || 0, offsetY = -this.height / 2 + this[shortM].offsetY || 0; @@ -472,7 +463,19 @@ left -= offsetX; top -= offsetY; } - ctx[method](chars, left, top); + if (this.charSpacing !== 0) { + var additionalSpace = this._getWidthOfCharSpacing(); + chars = chars.split(''); + for (var i = 0, len = chars.length; i < len; i++) { + char = chars[i]; + width = ctx.measureText(char).width + additionalSpace; + ctx[method](char, left, top); + left += width; + } + } + else { + ctx[method](chars, left, top); + } this[shortM].toLive && ctx.restore(); }, @@ -499,7 +502,7 @@ // stretch the line var words = line.split(/\s+/), charOffset = 0, - wordsWidth = this._getWidthOfWords(ctx, line, lineIndex, 0), + wordsWidth = this._getWidthOfWords(ctx, words.join(''), lineIndex, 0), widthDiff = this.width - wordsWidth, numSpaces = words.length - 1, spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, @@ -519,10 +522,16 @@ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} line - */ - _getWidthOfWords: function (ctx, line) { - return ctx.measureText(line.replace(/\s+/g, '')).width; + * @param {String} word + */ + _getWidthOfWords: function (ctx, word) { + var width = ctx.measureText(word).width, charCount, additionalSpace; + if (this.charSpacing !== 0) { + charCount = word.split('').length; + additionalSpace = charCount * this._getWidthOfCharSpacing(); + width += additionalSpace; + } + return width; }, /** @@ -552,29 +561,39 @@ * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _renderTextFill: function(ctx) { - if (!this.fill && this.isEmptyStyles()) { - return; - } + _renderTextCommon: function(ctx, method) { - var lineHeights = 0; + var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(); for (var i = 0, len = this._textLines.length; i < len; i++) { var heightOfLine = this._getHeightOfLine(ctx, i), - maxHeight = heightOfLine / this.lineHeight; - + maxHeight = heightOfLine / this.lineHeight, + lineWidth = this._getLineWidth(ctx, i), + leftOffset = this._getLineLeftOffset(lineWidth); this._renderTextLine( - 'fillText', + method, ctx, this._textLines[i], - this._getLeftOffset(), - this._getTopOffset() + lineHeights + maxHeight, + left + leftOffset, + top + lineHeights + maxHeight, i ); lineHeights += heightOfLine; } }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextFill: function(ctx) { + if (!this.fill && this.isEmptyStyles()) { + return; + } + + this._renderTextCommon(ctx, 'fillText'); + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -584,8 +603,6 @@ return; } - var lineHeights = 0; - if (this.shadow && !this.shadow.affectStroke) { this._removeShadow(ctx); } @@ -601,20 +618,7 @@ } 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; - } + this._renderTextCommon(ctx, 'strokeText'); ctx.closePath(); ctx.restore(); }, @@ -769,6 +773,13 @@ return width; }, + _getWidthOfCharSpacing: function() { + if (this.charSpacing !== 0) { + return this.fontSize * this.charSpacing / 1000; + } + return 0; + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -776,7 +787,14 @@ * @return {Number} Line width */ _measureLine: function(ctx, lineIndex) { - return ctx.measureText(this._textLines[lineIndex]).width; + var line = this._textLines[lineIndex], + width = ctx.measureText(line).width, + additionalSpace = 0, charCount; + if (this.charSpacing !== 0) { + charCount = line.split('').length; + additionalSpace = (charCount - 1) * this._getWidthOfCharSpacing(); + } + return width + additionalSpace; }, /** @@ -787,7 +805,6 @@ if (!this.textDecoration) { return; } - var halfOfVerticalBox = this.height / 2, _this = this, offsets = []; @@ -1012,7 +1029,7 @@ var line = this._textLines[i], words = line.split(/\s+/), - wordsWidth = this._getWidthOfWords(ctx, line), + wordsWidth = this._getWidthOfWords(ctx, words.join('')), widthDiff = this.width - wordsWidth, numSpaces = words.length - 1, spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, diff --git a/src/shapes/textbox.class.js b/src/shapes/textbox.class.js index c4316d3c5bb..fb7de902baa 100644 --- a/src/shapes/textbox.class.js +++ b/src/shapes/textbox.class.js @@ -94,7 +94,6 @@ // wrap lines this._textLines = this._splitTextIntoLines(); - // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap if (this.dynamicMinWidth > this.width) { this._set('width', this.dynamicMinWidth); @@ -244,11 +243,9 @@ _measureText: function(ctx, text, lineIndex, charOffset) { var width = 0; charOffset = charOffset || 0; - for (var i = 0, len = text.length; i < len; i++) { width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset); } - return width; }, @@ -271,7 +268,8 @@ wordWidth = 0, infixWidth = 0, largestWordWidth = 0, - lineJustStarted = true; + lineJustStarted = true, + additionalSpace = this._getWidthOfCharSpacing(); for (var i = 0; i < words.length; i++) { word = words[i]; @@ -279,7 +277,7 @@ offset += word.length; - lineWidth += infixWidth + wordWidth; + lineWidth += infixWidth + wordWidth - additionalSpace; if (lineWidth >= this.width && !lineJustStarted) { lines.push(line); @@ -287,13 +285,16 @@ lineWidth = wordWidth; lineJustStarted = true; } + else { + lineWidth += additionalSpace; + } if (!lineJustStarted) { line += infix; } line += word; - infixWidth = this._measureText(ctx, infix, lineIndex, offset); + infixWidth = this._measureText(ctx, infix, lineIndex, offset) + additionalSpace; offset++; lineJustStarted = false; // keep track of largest word @@ -305,7 +306,7 @@ i && lines.push(line); if (largestWordWidth > this.dynamicMinWidth) { - this.dynamicMinWidth = largestWordWidth; + this.dynamicMinWidth = largestWordWidth - additionalSpace; } return lines; diff --git a/test/unit/itext_key_behaviour.js b/test/unit/itext_key_behaviour.js index 9b7cbb61664..bd1cf1441b9 100644 --- a/test/unit/itext_key_behaviour.js +++ b/test/unit/itext_key_behaviour.js @@ -1,8 +1,11 @@ (function(){ + var canvas = fabric.document.createElement('canvas'), + ctx = canvas.getContext('2d'); + test('event selection:changed firing', function() { var iText = new fabric.IText('test need some word\nsecond line'), selection = 0; - + iText.ctx = ctx; function countSelectionChange() { selection++; } @@ -133,7 +136,7 @@ test('moving cursor with shift', function() { var iText = new fabric.IText('test need some word\nsecond line'), selection = 0; - + iText.ctx = ctx; function countSelectionChange() { selection++; }