fabric-pure-browser
Version:
Fabric.js package with no node-specific dependencies (node-canvas, jsdom). The project is published once a day (in case if a new version appears) from 'master' branch of https://github.com/fabricjs/fabric.js repository. You can keep original imports in
1,501 lines (1,389 loc) • 57.8 kB
JavaScript
(function(global) {
'use strict';
var fabric = global.fabric || (global.fabric = { }),
clone = fabric.util.object.clone;
if (fabric.Text) {
fabric.warn('fabric.Text is already defined');
return;
}
var additionalProps =
('fontFamily fontWeight fontSize text underline overline linethrough' +
' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles' +
' direction path pathStartOffset pathSide pathAlign').split(' ');
/**
* Text class
* @class fabric.Text
* @extends fabric.Object
* @return {fabric.Text} thisArg
* @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text}
* @see {@link fabric.Text#initialize} for constructor definition
*/
fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ {
/**
* Properties which when set cause object to change dimensions
* @type Array
* @private
*/
_dimensionAffectingProps: [
'fontSize',
'fontWeight',
'fontFamily',
'fontStyle',
'lineHeight',
'text',
'charSpacing',
'textAlign',
'styles',
'path',
'pathStartOffset',
'pathSide',
'pathAlign'
],
/**
* @private
*/
_reNewline: /\r?\n/,
/**
* Use this regular expression to filter for whitespaces that is not a new line.
* Mostly used when text is 'justify' aligned.
* @private
*/
_reSpacesAndTabs: /[ \t\r]/g,
/**
* Use this regular expression to filter for whitespace that is not a new line.
* Mostly used when text is 'justify' aligned.
* @private
*/
_reSpaceAndTab: /[ \t\r]/,
/**
* Use this regular expression to filter consecutive groups of non spaces.
* Mostly used when text is 'justify' aligned.
* @private
*/
_reWords: /\S+/g,
/**
* Type of an object
* @type String
* @default
*/
type: 'text',
/**
* Font size (in pixels)
* @type Number
* @default
*/
fontSize: 40,
/**
* Font weight (e.g. bold, normal, 400, 600, 800)
* @type {(Number|String)}
* @default
*/
fontWeight: 'normal',
/**
* Font family
* @type String
* @default
*/
fontFamily: 'Times New Roman',
/**
* Text decoration underline.
* @type Boolean
* @default
*/
underline: false,
/**
* Text decoration overline.
* @type Boolean
* @default
*/
overline: false,
/**
* Text decoration linethrough.
* @type Boolean
* @default
*/
linethrough: false,
/**
* Text alignment. Possible values: "left", "center", "right", "justify",
* "justify-left", "justify-center" or "justify-right".
* @type String
* @default
*/
textAlign: 'left',
/**
* Font style . Possible values: "", "normal", "italic" or "oblique".
* @type String
* @default
*/
fontStyle: 'normal',
/**
* Line height
* @type Number
* @default
*/
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
* @default
*/
textBackgroundColor: '',
/**
* List of properties to consider when checking if
* state of an object is changed ({@link fabric.Object#hasStateChanged})
* as well as for history (undo/redo) purposes
* @type Array
*/
stateProperties: fabric.Object.prototype.stateProperties.concat(additionalProps),
/**
* List of properties to consider when checking if cache needs refresh
* @type Array
*/
cacheProperties: fabric.Object.prototype.cacheProperties.concat(additionalProps),
/**
* When defined, an object is rendered via stroke and this property specifies its color.
* <b>Backwards incompatibility note:</b> This property was named "strokeStyle" until v1.1.6
* @type String
* @default
*/
stroke: null,
/**
* Shadow object representing shadow of this shape.
* <b>Backwards incompatibility note:</b> This property was named "textShadow" (String) until v1.2.11
* @type fabric.Shadow
* @default
*/
shadow: null,
/**
* fabric.Path that the text should follow.
* since 4.6.0 the path will be drawn automatically.
* if you want to make the path visible, give it a stroke and strokeWidth or fill value
* if you want it to be hidden, assign visible = false to the path.
* This feature is in BETA, and SVG import/export is not yet supported.
* @type fabric.Path
* @example
* var textPath = new fabric.Text('Text on a path', {
* top: 150,
* left: 150,
* textAlign: 'center',
* charSpacing: -50,
* path: new fabric.Path('M 0 0 C 50 -100 150 -100 200 0', {
* strokeWidth: 1,
* visible: false
* }),
* pathSide: 'left',
* pathStartOffset: 0
* });
* @default
*/
path: null,
/**
* Offset amount for text path starting position
* Only used when text has a path
* @type Number
* @default
*/
pathStartOffset: 0,
/**
* Which side of the path the text should be drawn on.
* Only used when text has a path
* @type {String} 'left|right'
* @default
*/
pathSide: 'left',
/**
* How text is aligned to the path. This property determines
* the perpendicular position of each character relative to the path.
* (one of "baseline", "center", "ascender", "descender")
* This feature is in BETA, and its behavior may change
* @type String
* @default
*/
pathAlign: 'baseline',
/**
* @private
*/
_fontSizeFraction: 0.222,
/**
* @private
*/
offsets: {
underline: 0.10,
linethrough: -0.315,
overline: -0.88
},
/**
* Text Line proportion to font Size (in pixels)
* @type Number
* @default
*/
_fontSizeMult: 1.13,
/**
* additional space between characters
* expressed in thousands of em unit
* @type Number
* @default
*/
charSpacing: 0,
/**
* Object containing character styles - top-level properties -> line numbers,
* 2nd-level properties - character numbers
* @type Object
* @default
*/
styles: null,
/**
* Reference to a context to measure text char or couple of chars
* the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas
* once created it will be referenced on fabric._measuringContext to avoid creating a canvas for every
* text object created.
* @type {CanvasRenderingContext2D}
* @default
*/
_measuringContext: null,
/**
* Baseline shift, styles only, keep at 0 for the main text object
* @type {Number}
* @default
*/
deltaY: 0,
/**
* WARNING: EXPERIMENTAL. NOT SUPPORTED YET
* determine the direction of the text.
* This has to be set manually together with textAlign and originX for proper
* experience.
* some interesting link for the future
* https://www.w3.org/International/questions/qa-bidi-unicode-controls
* @since 4.5.0
* @type {String} 'ltr|rtl'
* @default
*/
direction: 'ltr',
/**
* Array of properties that define a style unit (of 'styles').
* @type {Array}
* @default
*/
_styleProperties: [
'stroke',
'strokeWidth',
'fill',
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'underline',
'overline',
'linethrough',
'deltaY',
'textBackgroundColor',
],
/**
* contains characters bounding boxes
*/
__charBounds: [],
/**
* use this size when measuring text. To avoid IE11 rounding errors
* @type {Number}
* @default
* @readonly
* @private
*/
CACHE_FONT_SIZE: 400,
/**
* contains the min text width to avoid getting 0
* @type {Number}
* @default
*/
MIN_TEXT_WIDTH: 2,
/**
* Constructor
* @param {String} text Text string
* @param {Object} [options] Options object
* @return {fabric.Text} thisArg
*/
initialize: function(text, options) {
this.styles = options ? (options.styles || { }) : { };
this.text = text;
this.__skipDimension = true;
this.callSuper('initialize', options);
if (this.path) {
this.setPathInfo();
}
this.__skipDimension = false;
this.initDimensions();
this.setCoords();
this.setupState({ propertySet: '_dimensionAffectingProps' });
},
/**
* If text has a path, it will add the extra information needed
* for path and text calculations
* @return {fabric.Text} thisArg
*/
setPathInfo: function() {
var path = this.path;
if (path) {
path.segmentsInfo = fabric.util.getPathSegmentsInfo(path.path);
}
},
/**
* Return a context for measurement of text string.
* if created it gets stored for reuse
* this is for internal use, please do not use it
* @private
* @param {String} text Text string
* @param {Object} [options] Options object
* @return {fabric.Text} thisArg
*/
getMeasuringContext: function() {
// if we did not return we have to measure something.
if (!fabric._measuringContext) {
fabric._measuringContext = this.canvas && this.canvas.contextCache ||
fabric.util.createCanvasElement().getContext('2d');
}
return fabric._measuringContext;
},
/**
* @private
* Divides text into lines of text and lines of graphemes.
*/
_splitText: function() {
var newLines = this._splitTextIntoLines(this.text);
this.textLines = newLines.lines;
this._textLines = newLines.graphemeLines;
this._unwrappedTextLines = newLines._unwrappedLines;
this._text = newLines.graphemeText;
return newLines;
},
/**
* Initialize or update text dimensions.
* Updates this.width and this.height with the proper values.
* Does not return dimensions.
*/
initDimensions: function() {
if (this.__skipDimension) {
return;
}
this._splitText();
this._clearCache();
if (this.path) {
this.width = this.path.width;
this.height = this.path.height;
}
else {
this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH;
this.height = this.calcTextHeight();
}
if (this.textAlign.indexOf('justify') !== -1) {
// once text is measured we need to make space fatter to make justified text.
this.enlargeSpaces();
}
this.saveState({ propertySet: '_dimensionAffectingProps' });
},
/**
* Enlarge space boxes and shift the others
*/
enlargeSpaces: function() {
var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces;
for (var i = 0, len = this._textLines.length; i < len; i++) {
if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) {
continue;
}
accumulatedSpace = 0;
line = this._textLines[i];
currentLineWidth = this.getLineWidth(i);
if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) {
numberOfSpaces = spaces.length;
diffSpace = (this.width - currentLineWidth) / numberOfSpaces;
for (var j = 0, jlen = line.length; j <= jlen; j++) {
charBound = this.__charBounds[i][j];
if (this._reSpaceAndTab.test(line[j])) {
charBound.width += diffSpace;
charBound.kernedWidth += diffSpace;
charBound.left += accumulatedSpace;
accumulatedSpace += diffSpace;
}
else {
charBound.left += accumulatedSpace;
}
}
}
}
},
/**
* Detect if the text line is ended with an hard break
* text and itext do not have wrapping, return false
* @return {Boolean}
*/
isEndOfWrapping: function(lineIndex) {
return lineIndex === this._textLines.length - 1;
},
/**
* Detect if a line has a linebreak and so we need to account for it when moving
* and counting style.
* It return always for text and Itext.
* @return Number
*/
missingNewlineOffset: function() {
return 1;
},
/**
* Returns string representation of an instance
* @return {String} String representation of text object
*/
toString: function() {
return '#<fabric.Text (' + this.complexity() +
'): { "text": "' + this.text + '", "fontFamily": "' + this.fontFamily + '" }>';
},
/**
* Return the dimension and the zoom level needed to create a cache canvas
* big enough to host the object to be cached.
* @private
* @param {Object} dim.x width of object to be cached
* @param {Object} dim.y height of object to be cached
* @return {Object}.width width of canvas
* @return {Object}.height height of canvas
* @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache
* @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache
*/
_getCacheCanvasDimensions: function() {
var dims = this.callSuper('_getCacheCanvasDimensions');
var fontSize = this.fontSize;
dims.width += fontSize * dims.zoomX;
dims.height += fontSize * dims.zoomY;
return dims;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
var path = this.path;
path && !path.isNotVisible() && path._render(ctx);
this._setTextStyles(ctx);
this._renderTextLinesBackground(ctx);
this._renderTextDecoration(ctx, 'underline');
this._renderText(ctx);
this._renderTextDecoration(ctx, 'overline');
this._renderTextDecoration(ctx, 'linethrough');
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderText: function(ctx) {
if (this.paintFirst === 'stroke') {
this._renderTextStroke(ctx);
this._renderTextFill(ctx);
}
else {
this._renderTextFill(ctx);
this._renderTextStroke(ctx);
}
},
/**
* Set the font parameter of the context with the object properties or with charStyle
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Object} [charStyle] object with font style properties
* @param {String} [charStyle.fontFamily] Font Family
* @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix )
* @param {String} [charStyle.fontWeight] Font weight
* @param {String} [charStyle.fontStyle] Font style (italic|normal)
*/
_setTextStyles: function(ctx, charStyle, forMeasuring) {
ctx.textBaseline = 'alphabetical';
if (this.path) {
switch (this.pathAlign) {
case 'center':
ctx.textBaseline = 'middle';
break;
case 'ascender':
ctx.textBaseline = 'top';
break;
case 'descender':
ctx.textBaseline = 'bottom';
break;
}
}
ctx.font = this._getFontDeclaration(charStyle, forMeasuring);
},
/**
* calculate and return the text Width measuring each line.
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @return {Number} Maximum width of fabric.Text object
*/
calcTextWidth: function() {
var maxWidth = this.getLineWidth(0);
for (var i = 1, len = this._textLines.length; i < len; i++) {
var currentLineWidth = this.getLineWidth(i);
if (currentLineWidth > maxWidth) {
maxWidth = currentLineWidth;
}
}
return maxWidth;
},
/**
* @private
* @param {String} method Method name ("fillText" or "strokeText")
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line Text to render
* @param {Number} left Left position of text
* @param {Number} top Top position of text
* @param {Number} lineIndex Index of a line in a text
*/
_renderTextLine: function(method, ctx, line, left, top, lineIndex) {
this._renderChars(method, ctx, line, left, top, lineIndex);
},
/**
* Renders the text background for lines, taking care of style
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextLinesBackground: function(ctx) {
if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) {
return;
}
var heightOfLine,
lineLeftOffset, originalFill = ctx.fillStyle,
line, lastColor,
leftOffset = this._getLeftOffset(),
lineTopOffset = this._getTopOffset(),
boxStart = 0, boxWidth = 0, charBox, currentColor, path = this.path,
drawStart;
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this.getHeightOfLine(i);
if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) {
lineTopOffset += heightOfLine;
continue;
}
line = this._textLines[i];
lineLeftOffset = this._getLineLeftOffset(i);
boxWidth = 0;
boxStart = 0;
lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
for (var j = 0, jlen = line.length; j < jlen; j++) {
charBox = this.__charBounds[i][j];
currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
if (path) {
ctx.save();
ctx.translate(charBox.renderLeft, charBox.renderTop);
ctx.rotate(charBox.angle);
ctx.fillStyle = currentColor;
currentColor && ctx.fillRect(
-charBox.width / 2,
-heightOfLine / this.lineHeight * (1 - this._fontSizeFraction),
charBox.width,
heightOfLine / this.lineHeight
);
ctx.restore();
}
else if (currentColor !== lastColor) {
drawStart = leftOffset + lineLeftOffset + boxStart;
if (this.direction === 'rtl') {
drawStart = this.width - drawStart - boxWidth;
}
ctx.fillStyle = lastColor;
lastColor && ctx.fillRect(
drawStart,
lineTopOffset,
boxWidth,
heightOfLine / this.lineHeight
);
boxStart = charBox.left;
boxWidth = charBox.width;
lastColor = currentColor;
}
else {
boxWidth += charBox.kernedWidth;
}
}
if (currentColor && !path) {
drawStart = leftOffset + lineLeftOffset + boxStart;
if (this.direction === 'rtl') {
drawStart = this.width - drawStart - boxWidth;
}
ctx.fillStyle = currentColor;
ctx.fillRect(
drawStart,
lineTopOffset,
boxWidth,
heightOfLine / this.lineHeight
);
}
lineTopOffset += heightOfLine;
}
ctx.fillStyle = originalFill;
// if there is text background color no
// other shadows should be casted
this._removeShadow(ctx);
},
/**
* @private
* @param {Object} decl style declaration for cache
* @param {String} decl.fontFamily fontFamily
* @param {String} decl.fontStyle fontStyle
* @param {String} decl.fontWeight fontWeight
* @return {Object} reference to cache
*/
getFontCache: function(decl) {
var fontFamily = decl.fontFamily.toLowerCase();
if (!fabric.charWidthsCache[fontFamily]) {
fabric.charWidthsCache[fontFamily] = { };
}
var cache = fabric.charWidthsCache[fontFamily],
cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase();
if (!cache[cacheProp]) {
cache[cacheProp] = { };
}
return cache[cacheProp];
},
/**
* measure and return the width of a single character.
* possibly overridden to accommodate different measure logic or
* to hook some external lib for character measurement
* @private
* @param {String} _char, char to be measured
* @param {Object} charStyle style of char to be measured
* @param {String} [previousChar] previous char
* @param {Object} [prevCharStyle] style of previous char
*/
_measureChar: function(_char, charStyle, previousChar, prevCharStyle) {
// first i try to return from cache
var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle),
previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char,
stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth,
fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth;
if (previousChar && fontCache[previousChar] !== undefined) {
previousWidth = fontCache[previousChar];
}
if (fontCache[_char] !== undefined) {
kernedWidth = width = fontCache[_char];
}
if (stylesAreEqual && fontCache[couple] !== undefined) {
coupleWidth = fontCache[couple];
kernedWidth = coupleWidth - previousWidth;
}
if (width === undefined || previousWidth === undefined || coupleWidth === undefined) {
var ctx = this.getMeasuringContext();
// send a TRUE to specify measuring font size CACHE_FONT_SIZE
this._setTextStyles(ctx, charStyle, true);
}
if (width === undefined) {
kernedWidth = width = ctx.measureText(_char).width;
fontCache[_char] = width;
}
if (previousWidth === undefined && stylesAreEqual && previousChar) {
previousWidth = ctx.measureText(previousChar).width;
fontCache[previousChar] = previousWidth;
}
if (stylesAreEqual && coupleWidth === undefined) {
// we can measure the kerning couple and subtract the width of the previous character
coupleWidth = ctx.measureText(couple).width;
fontCache[couple] = coupleWidth;
kernedWidth = coupleWidth - previousWidth;
}
return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier };
},
/**
* Computes height of character at given position
* @param {Number} line the line index number
* @param {Number} _char the character index number
* @return {Number} fontSize of the character
*/
getHeightOfChar: function(line, _char) {
return this.getValueOfPropertyAt(line, _char, 'fontSize');
},
/**
* measure a text line measuring all characters.
* @param {Number} lineIndex line number
* @return {Number} Line width
*/
measureLine: function(lineIndex) {
var lineInfo = this._measureLine(lineIndex);
if (this.charSpacing !== 0) {
lineInfo.width -= this._getWidthOfCharSpacing();
}
if (lineInfo.width < 0) {
lineInfo.width = 0;
}
return lineInfo;
},
/**
* measure every grapheme of a line, populating __charBounds
* @param {Number} lineIndex
* @return {Object} object.width total width of characters
* @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs
*/
_measureLine: function(lineIndex) {
var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme,
graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length),
positionInPath = 0, startingPoint, totalPathLength, path = this.path,
reverse = this.pathSide === 'right';
this.__charBounds[lineIndex] = lineBounds;
for (i = 0; i < line.length; i++) {
grapheme = line[i];
graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme);
lineBounds[i] = graphemeInfo;
width += graphemeInfo.kernedWidth;
prevGrapheme = grapheme;
}
// this latest bound box represent the last character of the line
// to simplify cursor handling in interactive mode.
lineBounds[i] = {
left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0,
width: 0,
kernedWidth: 0,
height: this.fontSize
};
if (path) {
totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length;
startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo);
startingPoint.x += path.pathOffset.x;
startingPoint.y += path.pathOffset.y;
switch (this.textAlign) {
case 'left':
positionInPath = reverse ? (totalPathLength - width) : 0;
break;
case 'center':
positionInPath = (totalPathLength - width) / 2;
break;
case 'right':
positionInPath = reverse ? 0 : (totalPathLength - width);
break;
//todo - add support for justify
}
positionInPath += this.pathStartOffset * (reverse ? -1 : 1);
for (i = reverse ? line.length - 1 : 0;
reverse ? i >= 0 : i < line.length;
reverse ? i-- : i++) {
graphemeInfo = lineBounds[i];
if (positionInPath > totalPathLength) {
positionInPath %= totalPathLength;
}
else if (positionInPath < 0) {
positionInPath += totalPathLength;
}
// it would probably much faster to send all the grapheme position for a line
// and calculate path position/angle at once.
this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint);
positionInPath += graphemeInfo.kernedWidth;
}
}
return { width: width, numOfSpaces: numOfSpaces };
},
/**
* Calculate the angle and the left,top position of the char that follow a path.
* It appends it to graphemeInfo to be reused later at rendering
* @private
* @param {Number} positionInPath to be measured
* @param {Object} graphemeInfo current grapheme box information
* @param {Object} startingPoint position of the point
*/
_setGraphemeOnPath: function(positionInPath, graphemeInfo, startingPoint) {
var centerPosition = positionInPath + graphemeInfo.kernedWidth / 2,
path = this.path;
// we are at currentPositionOnPath. we want to know what point on the path is.
var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo);
graphemeInfo.renderLeft = info.x - startingPoint.x;
graphemeInfo.renderTop = info.y - startingPoint.y;
graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0);
},
/**
* Measure and return the info of a single grapheme.
* needs the the info of previous graphemes already filled
* @private
* @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} [prevGrapheme] character preceding the one to be measured
*/
_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, charSpacing;
if (this.charSpacing !== 0) {
charSpacing = this._getWidthOfCharSpacing();
width += charSpacing;
kernedWidth += charSpacing;
}
var box = {
width: width,
left: 0,
height: style.fontSize,
kernedWidth: kernedWidth,
deltaY: style.deltaY,
};
if (charIndex > 0 && !skipLeft) {
var previousBox = this.__charBounds[lineIndex][charIndex - 1];
box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width;
}
return box;
},
/**
* 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],
// char 0 is measured before the line cycle because it nneds to char
// emptylines
maxHeight = this.getHeightOfChar(lineIndex, 0);
for (var i = 1, len = line.length; i < len; i++) {
maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight);
}
return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult;
},
/**
* Calculate text box height
*/
calcTextHeight: function() {
var lineHeight, height = 0;
for (var i = 0, len = this._textLines.length; i < len; i++) {
lineHeight = this.getHeightOfLine(i);
height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight);
}
return height;
},
/**
* @private
* @return {Number} Left offset
*/
_getLeftOffset: function() {
return this.direction === 'ltr' ? -this.width / 2 : this.width / 2;
},
/**
* @private
* @return {Number} Top offset
*/
_getTopOffset: function() {
return -this.height / 2;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} method Method name ("fillText" or "strokeText")
*/
_renderTextCommon: function(ctx, method) {
ctx.save();
var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset();
for (var i = 0, len = this._textLines.length; i < len; i++) {
var heightOfLine = this.getHeightOfLine(i),
maxHeight = heightOfLine / this.lineHeight,
leftOffset = this._getLineLeftOffset(i);
this._renderTextLine(
method,
ctx,
this._textLines[i],
left + leftOffset,
top + lineHeights + maxHeight,
i
);
lineHeights += heightOfLine;
}
ctx.restore();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextFill: function(ctx) {
if (!this.fill && !this.styleHas('fill')) {
return;
}
this._renderTextCommon(ctx, 'fillText');
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextStroke: function(ctx) {
if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) {
return;
}
if (this.shadow && !this.shadow.affectStroke) {
this._removeShadow(ctx);
}
ctx.save();
this._setLineDash(ctx, this.strokeDashArray);
ctx.beginPath();
this._renderTextCommon(ctx, 'strokeText');
ctx.closePath();
ctx.restore();
},
/**
* @private
* @param {String} method fillText or strokeText.
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} line Content of the line, splitted in an array by grapheme
* @param {Number} left
* @param {Number} top
* @param {Number} lineIndex
*/
_renderChars: function(method, ctx, line, left, top, lineIndex) {
// set proper line offset
var lineHeight = this.getHeightOfLine(lineIndex),
isJustify = this.textAlign.indexOf('justify') !== -1,
actualStyle,
nextStyle,
charsToRender = '',
charBox,
boxWidth = 0,
timeToRender,
path = this.path,
shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path,
isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1,
drawingLeft, currentDirection = ctx.canvas.getAttribute('dir');
ctx.save();
if (currentDirection !== this.direction) {
ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
ctx.direction = isLtr ? 'ltr' : 'rtl';
ctx.textAlign = isLtr ? 'left' : 'right';
}
top -= lineHeight * this._fontSizeFraction / this.lineHeight;
if (shortCut) {
// render all the line in one pass without checking
// drawingLeft = isLtr ? left : left - this.getLineWidth(lineIndex);
this._renderChar(method, ctx, lineIndex, 0, line.join(''), left, top, lineHeight);
ctx.restore();
return;
}
for (var i = 0, len = line.length - 1; i <= len; i++) {
timeToRender = i === len || this.charSpacing || path;
charsToRender += line[i];
charBox = this.__charBounds[lineIndex][i];
if (boxWidth === 0) {
left += sign * (charBox.kernedWidth - charBox.width);
boxWidth += charBox.width;
}
else {
boxWidth += charBox.kernedWidth;
}
if (isJustify && !timeToRender) {
if (this._reSpaceAndTab.test(line[i])) {
timeToRender = true;
}
}
if (!timeToRender) {
// if we have charSpacing, we render char by char
actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
timeToRender = this._hasStyleChanged(actualStyle, nextStyle);
}
if (timeToRender) {
if (path) {
ctx.save();
ctx.translate(charBox.renderLeft, charBox.renderTop);
ctx.rotate(charBox.angle);
this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0, lineHeight);
ctx.restore();
}
else {
drawingLeft = left;
this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top, lineHeight);
}
charsToRender = '';
actualStyle = nextStyle;
left += sign * boxWidth;
boxWidth = 0;
}
}
ctx.restore();
},
/**
* This function try to patch the missing gradientTransform on canvas gradients.
* transforming a context to transform the gradient, is going to transform the stroke too.
* we want to transform the gradient but not the stroke operation, so we create
* a transformed gradient on a pattern and then we use the pattern instead of the gradient.
* this method has drawbacks: is slow, is in low resolution, needs a patch for when the size
* is limited.
* @private
* @param {fabric.Gradient} filler a fabric gradient instance
* @return {CanvasPattern} a pattern to use as fill/stroke style
*/
_applyPatternGradientTransformText: function(filler) {
var pCanvas = fabric.util.createCanvasElement(), pCtx,
// TODO: verify compatibility with strokeUniform
width = this.width + this.strokeWidth, height = this.height + this.strokeWidth;
pCanvas.width = width;
pCanvas.height = height;
pCtx = pCanvas.getContext('2d');
pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height);
pCtx.lineTo(0, height); pCtx.closePath();
pCtx.translate(width / 2, height / 2);
pCtx.fillStyle = filler.toLive(pCtx);
this._applyPatternGradientTransform(pCtx, filler);
pCtx.fill();
return pCtx.createPattern(pCanvas, 'no-repeat');
},
handleFiller: function(ctx, property, filler) {
var offsetX, offsetY;
if (filler.toLive) {
if (filler.gradientUnits === 'percentage' || filler.gradientTransform || filler.patternTransform) {
// need to transform gradient in a pattern.
// this is a slow process. If you are hitting this codepath, and the object
// is not using caching, you should consider switching it on.
// we need a canvas as big as the current object caching canvas.
offsetX = -this.width / 2;
offsetY = -this.height / 2;
ctx.translate(offsetX, offsetY);
ctx[property] = this._applyPatternGradientTransformText(filler);
return { offsetX: offsetX, offsetY: offsetY };
}
else {
// is a simple gradient or pattern
ctx[property] = filler.toLive(ctx, this);
return this._applyPatternGradientTransform(ctx, filler);
}
}
else {
// is a color
ctx[property] = filler;
}
return { offsetX: 0, offsetY: 0 };
},
_setStrokeStyles: function(ctx, decl) {
ctx.lineWidth = decl.strokeWidth;
ctx.lineCap = this.strokeLineCap;
ctx.lineDashOffset = this.strokeDashOffset;
ctx.lineJoin = this.strokeLineJoin;
ctx.miterLimit = this.strokeMiterLimit;
return this.handleFiller(ctx, 'strokeStyle', decl.stroke);
},
_setFillStyles: function(ctx, decl) {
return this.handleFiller(ctx, 'fillStyle', decl.fill);
},
/**
* @private
* @param {String} method
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {String} _char
* @param {Number} left Left coordinate
* @param {Number} top Top coordinate
* @param {Number} lineHeight Height of the line
*/
_renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) {
var decl = this._getStyleDeclaration(lineIndex, charIndex),
fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex),
shouldFill = method === 'fillText' && fullDecl.fill,
shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth,
fillOffsets, strokeOffsets;
if (!shouldStroke && !shouldFill) {
return;
}
ctx.save();
shouldFill && (fillOffsets = this._setFillStyles(ctx, fullDecl));
shouldStroke && (strokeOffsets = this._setStrokeStyles(ctx, fullDecl));
ctx.font = this._getFontDeclaration(fullDecl);
if (decl && decl.textBackgroundColor) {
this._removeShadow(ctx);
}
if (decl && decl.deltaY) {
top += decl.deltaY;
}
shouldFill && ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY);
shouldStroke && ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY);
ctx.restore();
},
/**
* Turns the character into a 'superior figure' (i.e. 'superscript')
* @param {Number} start selection start
* @param {Number} end selection end
* @returns {fabric.Text} thisArg
* @chainable
*/
setSuperscript: function(start, end) {
return this._setScript(start, end, this.superscript);
},
/**
* Turns the character into an 'inferior figure' (i.e. 'subscript')
* @param {Number} start selection start
* @param {Number} end selection end
* @returns {fabric.Text} thisArg
* @chainable
*/
setSubscript: function(start, end) {
return this._setScript(start, end, this.subscript);
},
/**
* Applies 'schema' at given position
* @private
* @param {Number} start selection start
* @param {Number} end selection end
* @param {Number} schema
* @returns {fabric.Text} thisArg
* @chainable
*/
_setScript: function(start, end, schema) {
var loc = this.get2DCursorLocation(start, true),
fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'),
dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'),
style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline };
this.setSelectionStyles(style, start, end);
return this;
},
/**
* @private
* @param {Object} prevStyle
* @param {Object} thisStyle
*/
_hasStyleChanged: function(prevStyle, thisStyle) {
return prevStyle.fill !== thisStyle.fill ||
prevStyle.stroke !== thisStyle.stroke ||
prevStyle.strokeWidth !== thisStyle.strokeWidth ||
prevStyle.fontSize !== thisStyle.fontSize ||
prevStyle.fontFamily !== thisStyle.fontFamily ||
prevStyle.fontWeight !== thisStyle.fontWeight ||
prevStyle.fontStyle !== thisStyle.fontStyle ||
prevStyle.deltaY !== thisStyle.deltaY;
},
/**
* @private
* @param {Object} prevStyle
* @param {Object} thisStyle
*/
_hasStyleChangedForSvg: function(prevStyle, thisStyle) {
return this._hasStyleChanged(prevStyle, thisStyle) ||
prevStyle.overline !== thisStyle.overline ||
prevStyle.underline !== thisStyle.underline ||
prevStyle.linethrough !== thisStyle.linethrough;
},
/**
* @private
* @param {Number} lineIndex index text line
* @return {Number} Line left offset
*/
_getLineLeftOffset: function(lineIndex) {
var lineWidth = this.getLineWidth(lineIndex),
lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction,
isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex);
if (textAlign === 'justify'
|| (textAlign === 'justify-center' && !isEndOfWrapping)
|| (textAlign === 'justify-right' && !isEndOfWrapping)
|| (textAlign === 'justify-left' && !isEndOfWrapping)
) {
return 0;
}
if (textAlign === 'center') {
leftOffset = lineDiff / 2;
}
if (textAlign === 'right') {
leftOffset = lineDiff;
}
if (textAlign === 'justify-center') {
leftOffset = lineDiff / 2;
}
if (textAlign === 'justify-right') {
leftOffset = lineDiff;
}
if (direction === 'rtl') {
leftOffset -= lineDiff;
}
return leftOffset;
},
/**
* @private
*/
_clearCache: function() {
this.__lineWidths = [];
this.__lineHeights = [];
this.__charBounds = [];
},
/**
* @private
*/
_shouldClearDimensionCache: function() {
var shouldClear = this._forceClearCache;
shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps'));
if (shouldClear) {
this.dirty = true;
this._forceClearCache = false;
}
return shouldClear;
},
/**
* Measure a single line given its index. Used to calculate the initial
* text bounding box. The values are calculated and stored in __lineWidths cache.
* @private
* @param {Number} lineIndex line number
* @return {Number} Line width
*/
getLineWidth: function(lineIndex) {
if (this.__lineWidths[lineIndex] !== undefined) {
return this.__lineWidths[lineIndex];
}
var lineInfo = this.measureLine(lineIndex);
var width = lineInfo.width;
this.__lineWidths[lineIndex] = width;
return width;
},
_getWidthOfCharSpacing: function() {
if (this.charSpacing !== 0) {
return this.fontSize * this.charSpacing / 1000;
}
return 0;
},
/**
* Retrieves the value of property at given character position
* @param {Number} lineIndex the line number
* @param {Number} charIndex the character number
* @param {String} property the property name
* @returns the value of 'property'
*/
getValueOfPropertyAt: function(lineIndex, charIndex, property) {
var charStyle = this._getStyleDeclaration(lineIndex, charIndex);
if (charStyle && typeof charStyle[property] !== 'undefined') {
return charStyle[property];
}
return this[property];
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextDecoration: function(ctx, type) {
if (!this[type] && !this.styleHas(type)) {
return;
}
var heightOfLine, size, _size,
lineLeftOffset, dy, _dy,
line, lastDecoration,
leftOffset = this._getLeftOffset(),
topOffset = this._getTopOffset(), top,
boxStart, boxWidth, charBox, currentDecoration,
maxHeight, currentFill, lastFill, path = this.path,
charSpacing = this._getWidthOfCharSpacing(),
offsetY = this.offsets[type];
for (var i = 0, len = this._textLines.length; i < len; i++) {
heightOfLine = this.getHeightOfLine(i);
if (!this[type] && !this.styleHas(type, i)) {
topOffset += heightOfLine;
continue;
}
line = this._textLines[i];
maxHeight = heightOfLine / this.lineHeight;
lineLeftOffset = this._getLineLeftOffset(i);
boxStart = 0;
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');
_size = this.getHeightOfChar(i, j);
_dy = this.getValueOfPropertyAt(i, j, 'deltaY');
if (path && currentDecoration && currentFill) {
ctx.save();
ctx.fillStyle = lastFill;
ctx.translate(charBox.renderLeft, charBox.renderTop);
ctx.rotate(charBox.angle);
ctx.fillRect(
-charBox.kernedWidth / 2,
offsetY * _size + _dy,
charBox.kernedWidth,
this.fontSize / 15
);
ctx.restore();
}
else if (
(currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy)
&& boxWidth > 0
) {
var drawStart = leftOffset + lineLeftOffset + boxStart;
if (this.direction === 'rtl') {
drawStart = this.width - drawStart - boxWidth;
}
if (lastDecoration && lastFill) {
ctx.fillStyle = lastFill;
ctx.fillRect(
drawStart,
top + offsetY * size + dy,
boxWidth,
this.fontSize / 15
);
}
boxStart = charBox.left;
boxWidth = charBox.width;
lastDecoration = currentDecoration;
lastFill = currentFill;
size = _size;
dy = _dy;
}
else {
boxWidth += charBox.kernedWidth;
}
}
var drawStart = leftOffset + lineLeftOffset + boxStart;
if (this.direction === 'rtl') {
drawStart = this.width - drawStart - boxWidth;
}
ctx.fillStyle = currentFill;
currentDecoration && currentFill && ctx.fillRect(
drawStart,
top + offsetY * size + dy,
boxWidth - charSpacing,
this.fontSize / 15
);
topOffset += heightOfLine;
}
// if there is text background color no
// other shadows should be casted
this._removeShadow(ctx);
},
/**
* return font declaration string for canvas context
* @param {Object} [styleObject] object
* @returns {String} font declaration formatted for canvas context.
*/
_getFontDeclaration: function(styleObject, forMeasuring) {
var style = styleObject || this, family = this.fontFamily,
fontIsGeneric = fabric.Text.genericFonts.indexOf(family.toLowerCase()) > -1;
var fontFamily = family === undefined ||
family.indexOf('\'') > -1 || family.indexOf(',') > -1 ||
family.indexOf('"') > -1 || fontIsGeneric
? style.fontFamily : '"' + style.fontFamily + '"';
return [
// node-canvas needs "weight style", while browsers need "style weight"
// verify if this can be fixed in JSDOM
(fabric.isLikelyNode ? style.fontWeight : style.fontStyle),
(fabric.isLikelyNode ? style.fontStyle : style.fontWeight),
forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px',
fontFamily
].join(' ');
},
/**
* Renders text instance on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
render: fun