Textspacing (#3097)

Introduce charspacing.
This commit is contained in:
Andrea Bogazzi 2016-08-14 23:19:42 +02:00 committed by GitHub
parent cf5f327f55
commit 46624d3f50
5 changed files with 145 additions and 109 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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
* @param {String} word
*/
_getWidthOfWords: function (ctx, line) {
return ctx.measureText(line.replace(/\s+/g, '')).width;
_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,

View file

@ -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;

View file

@ -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++;
}