diff --git a/src/mixins/object.svg_export.js b/src/mixins/object.svg_export.js index 6ffe329304b..eb61fac8c87 100644 --- a/src/mixins/object.svg_export.js +++ b/src/mixins/object.svg_export.js @@ -58,7 +58,7 @@ /** * Returns styles-string for svg-export - * @param {Object} style style properties for the span a boolean to skip shadow filter output + * @param {Object} style the object from which to retrieve style properties * @param {Boolean} useWhiteSpace a boolean to include an additional attribute in the style. * @return {String} */ @@ -71,10 +71,12 @@ fontWeight = style.fontWeight ? 'font-weight: ' + style.fontWeight + term : '', fill = style.fill ? getSvgColorString('fill', style.fill) : '', stroke = style.stroke ? getSvgColorString('stroke', style.stroke) : '', - textDecoration = this.getSvgTextDecoration(style); + textDecoration = this.getSvgTextDecoration(style), + deltaY = style.deltaY ? 'baseline-shift: ' + (-style.deltaY) + '; ' : ''; if (textDecoration) { textDecoration = 'text-decoration: ' + textDecoration + term; } + return [ stroke, strokeWidth, @@ -84,10 +86,16 @@ fontWeight, textDecoration, fill, + deltaY, useWhiteSpace ? 'white-space: pre; ' : '' ].join(''); }, + /** + * Returns text-decoration property for svg-export + * @param {Object} style the object from which to retrieve style properties + * @return {String} + */ getSvgTextDecoration: function(style) { if ('overline' in style || 'underline' in style || 'linethrough' in style) { return (style.overline ? 'overline ' : '') + diff --git a/src/shapes/itext.class.js b/src/shapes/itext.class.js index 4f66ea29721..463722649d7 100644 --- a/src/shapes/itext.class.js +++ b/src/shapes/itext.class.js @@ -377,7 +377,8 @@ charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'), multiplier = this.scaleX * this.canvas.getZoom(), cursorWidth = this.cursorWidth / multiplier, - topOffset = boundaries.topOffset; + topOffset = boundaries.topOffset, + dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY'); topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight - charHeight * (1 - this._fontSizeFraction); @@ -390,7 +391,7 @@ ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; ctx.fillRect( boundaries.left + boundaries.leftOffset - cursorWidth / 2, - topOffset + boundaries.top, + topOffset + boundaries.top + dy, cursorWidth, charHeight); }, diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index 2b23be81961..2a38f95a682 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -24,7 +24,7 @@ /** * Properties which when set cause object to change dimensions - * @type Object + * @type Array * @private */ _dimensionAffectingProps: [ @@ -136,6 +136,26 @@ */ lineHeight: 1.16, + /** + * Superscript schema object (minimum overlap) + * @type {Object} + * @default + */ + superscript: { + size: 0.60, // fontSize factor + baseline: -0.35 // baseline-shift factor (upwards) + }, + + /** + * Subscript schema object (minimum overlap) + * @type {Object} + * @default + */ + subscript: { + size: 0.60, // fontSize factor + baseline: 0.11 // baseline-shift factor (downwards) + }, + /** * Background color of text lines * @type String @@ -227,8 +247,8 @@ charSpacing: 0, /** - * Object containing character styles - * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) + * Object containing character styles - top-level properties -> line numbers, + * 2nd-level properties - charater numbers * @type Object * @default */ @@ -245,7 +265,14 @@ _measuringContext: null, /** - * Array of properties that define a style unit. + * Baseline shift, stlyes only, keep at 0 for the main text object + * @type {Number} + * @default + */ + deltaY: 0, + + /** + * Array of properties that define a style unit (of 'styles'). * @type {Array} * @default */ @@ -260,6 +287,7 @@ 'underline', 'overline', 'linethrough', + 'deltaY', 'textBackgroundColor', ], @@ -633,13 +661,13 @@ }, /** - * return height of char in fontSize for a character at lineIndex, charIndex - * @param {Number} l line Index - * @param {Number} c char index - * @return {Number} fontSize of that character + * Computes height of character at given position + * @param {Number} line the line number + * @param {Number} char the character number + * @return {Number} fontSize of the character */ - getHeightOfChar: function(l, c) { - return this.getValueOfPropertyAt(l, c, 'fontSize'); + getHeightOfChar: function(line, char) { + return this.getValueOfPropertyAt(line, char, 'fontSize'); }, /** @@ -694,23 +722,26 @@ * @param {String} grapheme to be measured * @param {Number} lineIndex index of the line where the char is * @param {Number} charIndex position in the line - * @param {String} [previousChar] character preceding the one to be measured + * @param {String} [prevGrapheme] character preceding the one to be measured */ - _getGraphemeBox: function(grapheme, lineIndex, charIndex, previousGrapheme, skipLeft) { - var charStyle = this.getCompleteStyleDeclaration(lineIndex, charIndex), - prevCharStyle = previousGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { }, - info = this._measureChar(grapheme, charStyle, previousGrapheme, prevCharStyle), - kernedWidth = info.kernedWidth, width = info.width; + _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { + var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), + prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { }, + info = this._measureChar(grapheme, style, prevGrapheme, prevStyle), + kernedWidth = info.kernedWidth, + width = info.width; if (this.charSpacing !== 0) { width += this._getWidthOfCharSpacing(); kernedWidth += this._getWidthOfCharSpacing(); } + var box = { width: width, left: 0, - height: charStyle.fontSize, + height: style.fontSize, kernedWidth: kernedWidth, + deltaY: style.deltaY, }; if (charIndex > 0 && !skipLeft) { var previousBox = this.__charBounds[lineIndex][charIndex - 1]; @@ -720,32 +751,25 @@ }, /** - * Calculate height of chosen line - * height of line is based mainly on fontSize - * @private - * @param {Number} lineIndex index of the line to calculate + * Calculate height of line at 'lineIndex' + * @param {Number} lineIndex index of line to calculate + * @return {Number} */ getHeightOfLine: function(lineIndex) { if (this.__lineHeights[lineIndex]) { return this.__lineHeights[lineIndex]; } - var line = this._textLines[lineIndex], - maxHeight = this.getHeightOfChar(lineIndex, 0); - - for (var i = 1, len = line.length; i < len; i++) { - var currentCharHeight = this.getHeightOfChar(lineIndex, i); - if (currentCharHeight > maxHeight) { - maxHeight = currentCharHeight; - } + var line = this._textLines[lineIndex], maxHeight = 0; + for (var i = 0, len = line.length; i < len; i++) { + maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); } - this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; - return this.__lineHeights[lineIndex]; + + return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; }, /** - * calculate text box height - * @private + * Calculate text box height */ calcTextHeight: function() { var lineHeight, height = 0; @@ -921,11 +945,54 @@ if (decl && decl.textBackgroundColor) { this._removeShadow(ctx); } + if (decl && decl.deltaY) { + top += decl.deltaY; + } + shouldFill && ctx.fillText(_char, left, top); shouldStroke && ctx.strokeText(_char, left, top); decl && ctx.restore(); }, + /** + * Turns the character into a 'superior figure' (i.e. 'superscript') + * @param {Number} line the line number + * @param {Number} char the character number + * @returns {fabric.Text} thisArg + * @chainable + */ + setSuperscript: function(line, char) { + return this._setScript(line, char, this.superscript); + }, + + /** + * Turns the character into an 'inferior figure' (i.e. 'subscript') + * @param {Number} line the line number + * @param {Number} char the character number + * @returns {fabric.Text} thisArg + * @chainable + */ + setSubscript: function(line, char) { + return this._setScript(line, char, this.subscript); + }, + + /** + * Applies 'schema' at given position + * @private + * @param {Number} line the line number + * @param {Number} char the character number + * @param {Number} key one of {'this.superscript', 'this.subscript'} + * @returns {fabric.Text} thisArg + * @chainable + */ + _setScript: function(line, char, schema) { + var fontSize = this.getValueOfPropertyAt(line, char, 'fontSize'), + dy = this.getValueOfPropertyAt(line, char, 'deltaY'); + this.setPropertyAt(line, char, 'fontSize', fontSize * schema.size); + this.setPropertyAt(line, char, 'deltaY', dy + fontSize * schema.baseline); + return this; + }, + /** * @private * @param {Object} prevStyle @@ -938,7 +1005,8 @@ prevStyle.fontSize !== thisStyle.fontSize || prevStyle.fontFamily !== thisStyle.fontFamily || prevStyle.fontWeight !== thisStyle.fontWeight || - prevStyle.fontStyle !== thisStyle.fontStyle; + prevStyle.fontStyle !== thisStyle.fontStyle || + prevStyle.deltaY !== thisStyle.deltaY; }, /** @@ -1030,16 +1098,33 @@ }, /** - * @private - * @param {Number} LineIndex - * @param {Number} charIndex - * @param {String} property - + * Retrieves the value of property at given character position + * @param {Number} lineIndex the line number + * @param {Number} charIndex the charater number + * @param {String} property the property name + * @returns the value of 'property' */ getValueOfPropertyAt: function(lineIndex, charIndex, property) { - var charStyle = this._getStyleDeclaration(lineIndex, charIndex), - styleDecoration = charStyle && typeof charStyle[property] !== 'undefined'; - return styleDecoration ? charStyle[property] : this[property]; + var charStyle = this._getStyleDeclaration(lineIndex, charIndex); + if (charStyle && typeof charStyle[property] !== 'undefined') { + return charStyle[property]; + } + return this[property]; + }, + + /** + * Assigns 'value' to the property 'key' at given character position + * @param {Number} line the line number + * @param {Number} char the character number + * @param {String} key the property name + * @param {Any} value the value + * @returns {Object} this + */ + setPropertyAt: function(line, char, key, value) { + var decl = this._getStyleDeclaration(line, char) || {}; + decl[key] = value; + this._setStyleDeclaration(line, char, decl); + return this; }, /** @@ -1050,11 +1135,11 @@ if (!this[type] && !this.styleHas(type)) { return; } - var heightOfLine, - lineLeftOffset, + var heightOfLine, size, _size, + lineLeftOffset, dy, _dy, line, lastDecoration, leftOffset = this._getLeftOffset(), - topOffset = this._getTopOffset(), + topOffset = this._getTopOffset(), top, boxStart, boxWidth, charBox, currentDecoration, maxHeight, currentFill, lastFill; @@ -1071,21 +1156,30 @@ boxWidth = 0; lastDecoration = this.getValueOfPropertyAt(i, 0, type); lastFill = this.getValueOfPropertyAt(i, 0, 'fill'); + top = topOffset + maxHeight * (1 - this._fontSizeFraction); + size = this.getHeightOfChar(i, 0); + dy = this.getValueOfPropertyAt(i, 0, 'deltaY'); for (var j = 0, jlen = line.length; j < jlen; j++) { charBox = this.__charBounds[i][j]; currentDecoration = this.getValueOfPropertyAt(i, j, type); currentFill = this.getValueOfPropertyAt(i, j, 'fill'); - if ((currentDecoration !== lastDecoration || currentFill !== lastFill) && boxWidth > 0) { + _size = this.getHeightOfChar(i, j); + _dy = this.getValueOfPropertyAt(i, j, 'deltaY'); + if ((currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) && + boxWidth > 0) { ctx.fillStyle = lastFill; lastDecoration && lastFill && ctx.fillRect( leftOffset + lineLeftOffset + boxStart, - topOffset + maxHeight * (1 - this._fontSizeFraction) + this.offsets[type] * this.fontSize, + top + this.offsets[type] * size + dy, boxWidth, - this.fontSize / 15); + this.fontSize / 15 + ); boxStart = charBox.left; boxWidth = charBox.width; lastDecoration = currentDecoration; lastFill = currentFill; + size = _size; + dy = _dy; } else { boxWidth += charBox.kernedWidth; @@ -1094,7 +1188,7 @@ ctx.fillStyle = currentFill; currentDecoration && currentFill && ctx.fillRect( leftOffset + lineLeftOffset + boxStart, - topOffset + maxHeight * (1 - this._fontSizeFraction) + this.offsets[type] * this.fontSize, + top + this.offsets[type] * size + dy, boxWidth, this.fontSize / 15 ); diff --git a/test/unit/text.js b/test/unit/text.js index 7b54f1453e0..fc21eb6df43 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -551,8 +551,8 @@ var iText = new fabric.Text('test foo bar-baz\nqux', { styles: { 0: { - 0: { textDecoration: 'underline' }, - 2: { textDecoration: 'overline' }, + 0: { underline: true }, + 2: { overline: true }, 4: { textBackgroundColor: '#ffc' } }, 1: { @@ -563,9 +563,22 @@ } }); - assert.equal(typeof iText.getStyleAtPosition, 'function'); + var expectedStyle0 = { + stroke: null, + strokeWidth: 1, + fill: 'rgb(0,0,0)', + fontFamily: 'Times New Roman', + fontSize: 40, + fontWeight: 'normal', + fontStyle: 'normal', + underline: true, + overline: false, + linethrough: false, + textBackgroundColor: '', + deltaY: 0, + }; - assert.deepEqual(iText.getStyleAtPosition(2, true), { + var expectedStyle2 = { stroke: null, strokeWidth: 1, fill: 'rgb(0,0,0)', @@ -574,10 +587,17 @@ fontWeight: 'normal', fontStyle: 'normal', underline: false, - overline: false, + overline: true, linethrough: false, - textBackgroundColor: '' - }); + textBackgroundColor: '', + deltaY: 0, + }; + + assert.equal(typeof iText.getStyleAtPosition, 'function'); + + assert.deepEqual(iText.getStyleAtPosition(0, true), expectedStyle0, 'styles do match at 0'); + + assert.deepEqual(iText.getStyleAtPosition(2, true), expectedStyle2, 'styles do match at 2'); }); QUnit.test('toSVG with NUM_FRACTION_DIGITS', function(assert) { @@ -681,4 +701,48 @@ var expected = 'overline underline line-through '; assert.equal(styleString, expected, 'style is as expected with overline underline'); }); + + QUnit.test('text superscript', function(assert) { + var text = new fabric.Text('xxx', { styles: { + 0: { 0: { stroke: 'black', fill: 'blue' }, 1: { fill: 'blue' }, 2: { fontSize: 4, deltaY: 20 }} + } }); + assert.ok(typeof text.setSuperscript === 'function'); + + var size = text.fontSize; + var schema = text.superscript; + var styleFontSize = text.styles[0][2].fontSize; + var styleDeltaY = text.styles[0][2].deltaY; + text.setSuperscript(0, 1).setSuperscript(0, 2); + + assert.equal(text.styles[0][0].fontSize, undefined, 'character 0: fontSize is not set'); + assert.equal(text.styles[0][0].deltaY, undefined, 'character 0: deltaY is not set'); + + assert.equal(text.styles[0][1].fontSize, size * schema.size, 'character 1: fontSize has been set'); + assert.equal(text.styles[0][1].deltaY, size * schema.baseline, 'character 1: deltaY has been set'); + + assert.equal(text.styles[0][2].fontSize, styleFontSize * schema.size, 'character 2: fontSize has been decreased'); + assert.equal(text.styles[0][2].deltaY, styleDeltaY + styleFontSize * schema.baseline, 'character 2: deltaY has been decreased'); + }); + + QUnit.test('text subscript', function(assert) { + var text = new fabric.Text('xxx', { styles: { + 0: { 0: { stroke: 'black', fill: 'blue' }, 1: { fill: 'blue' }, 2: { fontSize: 4, deltaY: 20 }} + } }); + assert.ok(typeof text.setSubscript === 'function'); + + var size = text.fontSize; + var schema = text.subscript; + var styleFontSize = text.styles[0][2].fontSize; + var styleDeltaY = text.styles[0][2].deltaY; + text.setSubscript(0, 1).setSubscript(0, 2); + + assert.equal(text.styles[0][0].fontSize, undefined, 'character 0: fontSize is not set'); + assert.equal(text.styles[0][0].deltaY, undefined, 'character 0: deltaY is not set'); + + assert.equal(text.styles[0][1].fontSize, size * schema.size, 'character 1: fontSize has been set'); + assert.equal(text.styles[0][1].deltaY, size * schema.baseline, 'character 1: deltaY has been set'); + + assert.equal(text.styles[0][2].fontSize, styleFontSize * schema.size, 'character 2: fontSize has been decreased'); + assert.equal(text.styles[0][2].deltaY, styleDeltaY + styleFontSize * schema.baseline, 'character 2: deltaY has been increased'); + }); })();