mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-06 15:30:59 +00:00
1207 lines
34 KiB
JavaScript
1207 lines
34 KiB
JavaScript
(function(global) {
|
|
|
|
'use strict';
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
extend = fabric.util.object.extend,
|
|
clone = fabric.util.object.clone,
|
|
toFixed = fabric.util.toFixed,
|
|
supportsLineDash = fabric.StaticCanvas.supports('setLineDash'),
|
|
NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
|
|
|
|
if (fabric.Text) {
|
|
fabric.warn('fabric.Text is already defined');
|
|
return;
|
|
}
|
|
|
|
var stateProperties = fabric.Object.prototype.stateProperties.concat();
|
|
stateProperties.push(
|
|
'fontFamily',
|
|
'fontWeight',
|
|
'fontSize',
|
|
'text',
|
|
'textDecoration',
|
|
'textAlign',
|
|
'fontStyle',
|
|
'lineHeight',
|
|
'textBackgroundColor'
|
|
);
|
|
|
|
/**
|
|
* 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 Object
|
|
* @private
|
|
*/
|
|
_dimensionAffectingProps: {
|
|
fontSize: true,
|
|
fontWeight: true,
|
|
fontFamily: true,
|
|
fontStyle: true,
|
|
lineHeight: true,
|
|
stroke: true,
|
|
strokeWidth: true,
|
|
text: true,
|
|
textAlign: true
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_reNewline: /\r?\n/,
|
|
|
|
/**
|
|
* Use this regular expression to filter for whitespace that is not a new line.
|
|
* Mostly used when text is 'justify' aligned.
|
|
* @private
|
|
*/
|
|
_reSpacesAndTabs: /[ \t\r]+/g,
|
|
|
|
/**
|
|
* Retrieves object's fontSize
|
|
* @method getFontSize
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {String} Font size (in pixels)
|
|
*/
|
|
|
|
/**
|
|
* Sets object's fontSize
|
|
* Does not update the object .width and .height,
|
|
* call ._initDimensions() to update the values.
|
|
* @method setFontSize
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {Number} fontSize Font size (in pixels)
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's fontWeight
|
|
* @method getFontWeight
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {(String|Number)} Font weight
|
|
*/
|
|
|
|
/**
|
|
* Sets object's fontWeight
|
|
* Does not update the object .width and .height,
|
|
* call ._initDimensions() to update the values.
|
|
* @method setFontWeight
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {(Number|String)} fontWeight Font weight
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's fontFamily
|
|
* @method getFontFamily
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {String} Font family
|
|
*/
|
|
|
|
/**
|
|
* Sets object's fontFamily
|
|
* Does not update the object .width and .height,
|
|
* call ._initDimensions() to update the values.
|
|
* @method setFontFamily
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {String} fontFamily Font family
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's text
|
|
* @method getText
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {String} text
|
|
*/
|
|
|
|
/**
|
|
* Sets object's text
|
|
* Does not update the object .width and .height,
|
|
* call ._initDimensions() to update the values.
|
|
* @method setText
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {String} text Text
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's textDecoration
|
|
* @method getTextDecoration
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {String} Text decoration
|
|
*/
|
|
|
|
/**
|
|
* Sets object's textDecoration
|
|
* @method setTextDecoration
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {String} textDecoration Text decoration
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's fontStyle
|
|
* @method getFontStyle
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {String} Font style
|
|
*/
|
|
|
|
/**
|
|
* Sets object's fontStyle
|
|
* Does not update the object .width and .height,
|
|
* call ._initDimensions() to update the values.
|
|
* @method setFontStyle
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {String} fontStyle Font style
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's lineHeight
|
|
* @method getLineHeight
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {Number} Line height
|
|
*/
|
|
|
|
/**
|
|
* Sets object's lineHeight
|
|
* @method setLineHeight
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {Number} lineHeight Line height
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's textAlign
|
|
* @method getTextAlign
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {String} Text alignment
|
|
*/
|
|
|
|
/**
|
|
* Sets object's textAlign
|
|
* @method setTextAlign
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {String} textAlign Text alignment
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* Retrieves object's textBackgroundColor
|
|
* @method getTextBackgroundColor
|
|
* @memberOf fabric.Text.prototype
|
|
* @return {String} Text background color
|
|
*/
|
|
|
|
/**
|
|
* Sets object's textBackgroundColor
|
|
* @method setTextBackgroundColor
|
|
* @memberOf fabric.Text.prototype
|
|
* @param {String} textBackgroundColor Text background color
|
|
* @return {fabric.Text}
|
|
* @chainable
|
|
*/
|
|
|
|
/**
|
|
* 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 Possible values: "", "underline", "overline" or "line-through".
|
|
* @type String
|
|
* @default
|
|
*/
|
|
textDecoration: '',
|
|
|
|
/**
|
|
* Text alignment. Possible values: "left", "center", "right" or "justify".
|
|
* @type String
|
|
* @default
|
|
*/
|
|
textAlign: 'left',
|
|
|
|
/**
|
|
* Font style . Possible values: "", "normal", "italic" or "oblique".
|
|
* @type String
|
|
* @default
|
|
*/
|
|
fontStyle: '',
|
|
|
|
/**
|
|
* Line height
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
lineHeight: 1.16,
|
|
|
|
/**
|
|
* 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: stateProperties,
|
|
|
|
/**
|
|
* 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,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_fontSizeFraction: 0.25,
|
|
|
|
/**
|
|
* Text Line proportion to font Size (in pixels)
|
|
* @type Number
|
|
* @default
|
|
*/
|
|
_fontSizeMult: 1.13,
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {String} text Text string
|
|
* @param {Object} [options] Options object
|
|
* @return {fabric.Text} thisArg
|
|
*/
|
|
initialize: function(text, options) {
|
|
options = options || { };
|
|
this.text = text;
|
|
this.__skipDimension = true;
|
|
this.setOptions(options);
|
|
this.__skipDimension = false;
|
|
this._initDimensions();
|
|
},
|
|
|
|
/**
|
|
* Initialize text dimensions. Render all text on given context
|
|
* or on a offscreen canvas to get the text width with measureText.
|
|
* Updates this.width and this.height with the proper values.
|
|
* Does not return dimensions.
|
|
* @param {CanvasRenderingContext2D} [ctx] Context to render on
|
|
* @private
|
|
*/
|
|
_initDimensions: function(ctx) {
|
|
if (this.__skipDimension) {
|
|
return;
|
|
}
|
|
if (!ctx) {
|
|
ctx = fabric.util.createCanvasElement().getContext('2d');
|
|
this._setTextStyles(ctx);
|
|
}
|
|
this._textLines = this._splitTextIntoLines();
|
|
this._clearCache();
|
|
this.width = this._getTextWidth(ctx);
|
|
this.height = this._getTextHeight(ctx);
|
|
},
|
|
|
|
/**
|
|
* 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 + '" }>';
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_render: function(ctx) {
|
|
this.clipTo && fabric.util.clipContext(this, ctx);
|
|
this._setOpacity(ctx);
|
|
this._setShadow(ctx);
|
|
this._setupCompositeOperation(ctx);
|
|
this._renderTextBackground(ctx);
|
|
this._setStrokeStyles(ctx);
|
|
this._setFillStyles(ctx);
|
|
this._renderText(ctx);
|
|
this._renderTextDecoration(ctx);
|
|
this.clipTo && ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_setTextStyles: function(ctx) {
|
|
ctx.textBaseline = 'alphabetic';
|
|
if (!this.skipTextAlign) {
|
|
ctx.textAlign = this.textAlign;
|
|
}
|
|
ctx.font = this._getFontDeclaration();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @return {Number} Height of fabric.Text object
|
|
*/
|
|
_getTextHeight: function() {
|
|
return this._textLines.length * this._getHeightOfLine();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @return {Number} Maximum width of fabric.Text object
|
|
*/
|
|
_getTextWidth: function(ctx) {
|
|
var maxWidth = this._getLineWidth(ctx, 0);
|
|
|
|
for (var i = 1, len = this._textLines.length; i < len; i++) {
|
|
var currentLineWidth = this._getLineWidth(ctx, i);
|
|
if (currentLineWidth > maxWidth) {
|
|
maxWidth = currentLineWidth;
|
|
}
|
|
}
|
|
return maxWidth;
|
|
},
|
|
|
|
/*
|
|
* Calculate object dimensions from its properties
|
|
* @override
|
|
* @private
|
|
*/
|
|
_getNonTransformedDimensions: function() {
|
|
return { x: this.width, y: this.height };
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {String} method Method name ("fillText" or "strokeText")
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {String} chars Chars to render
|
|
* @param {Number} left Left position of text
|
|
* @param {Number} top Top position of text
|
|
*/
|
|
_renderChars: function(method, ctx, chars, left, top) {
|
|
// remove Text word from method var
|
|
var shortM = method.slice(0, -4);
|
|
if (this[shortM].toLive) {
|
|
var offsetX = -this.width / 2 + this[shortM].offsetX || 0,
|
|
offsetY = -this.height / 2 + this[shortM].offsetY || 0;
|
|
ctx.save();
|
|
ctx.translate(offsetX, offsetY);
|
|
left -= offsetX;
|
|
top -= offsetY;
|
|
}
|
|
ctx[method](chars, left, top);
|
|
this[shortM].toLive && ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* @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) {
|
|
// lift the line by quarter of fontSize
|
|
top -= this.fontSize * this._fontSizeFraction;
|
|
|
|
// short-circuit
|
|
var lineWidth = this._getLineWidth(ctx, lineIndex);
|
|
if (this.textAlign !== 'justify' || this.width < lineWidth) {
|
|
this._renderChars(method, ctx, line, left, top, lineIndex);
|
|
return;
|
|
}
|
|
|
|
// stretch the line
|
|
var words = line.split(/\s+/),
|
|
charOffset = 0,
|
|
wordsWidth = this._getWidthOfWords(ctx, line, lineIndex, 0),
|
|
widthDiff = this.width - wordsWidth,
|
|
numSpaces = words.length - 1,
|
|
spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0,
|
|
leftOffset = 0, word;
|
|
|
|
for (var i = 0, len = words.length; i < len; i++) {
|
|
while (line[charOffset] === ' ' && charOffset < line.length) {
|
|
charOffset++;
|
|
}
|
|
word = words[i];
|
|
this._renderChars(method, ctx, word, left + leftOffset, top, lineIndex, charOffset);
|
|
leftOffset += this._getWidthOfWords(ctx, word, lineIndex, charOffset) + spaceWidth;
|
|
charOffset += word.length;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {Number} line
|
|
*/
|
|
_getWidthOfWords: function (ctx, line) {
|
|
return ctx.measureText(line.replace(/\s+/g, '')).width;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @return {Number} Left offset
|
|
*/
|
|
_getLeftOffset: function() {
|
|
return -this.width / 2;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @return {Number} Top offset
|
|
*/
|
|
_getTopOffset: function() {
|
|
return -this.height / 2;
|
|
},
|
|
|
|
/**
|
|
* Returns true because text has no style
|
|
*/
|
|
isEmptyStyles: function() {
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderTextFill: function(ctx) {
|
|
if (!this.fill && this.isEmptyStyles()) {
|
|
return;
|
|
}
|
|
|
|
var lineHeights = 0;
|
|
|
|
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,
|
|
this._textLines[i],
|
|
this._getLeftOffset(),
|
|
this._getTopOffset() + lineHeights + maxHeight,
|
|
i
|
|
);
|
|
lineHeights += heightOfLine;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderTextStroke: function(ctx) {
|
|
if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) {
|
|
return;
|
|
}
|
|
|
|
var lineHeights = 0;
|
|
|
|
if (this.shadow && !this.shadow.affectStroke) {
|
|
this._removeShadow(ctx);
|
|
}
|
|
|
|
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) {
|
|
this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray);
|
|
}
|
|
supportsLineDash && ctx.setLineDash(this.strokeDashArray);
|
|
}
|
|
|
|
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;
|
|
}
|
|
ctx.closePath();
|
|
ctx.restore();
|
|
},
|
|
|
|
_getHeightOfLine: function() {
|
|
return this.fontSize * this._fontSizeMult * this.lineHeight;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {Array} textLines Array of all text lines
|
|
*/
|
|
_renderTextBackground: function(ctx) {
|
|
this._renderTextBoxBackground(ctx);
|
|
this._renderTextLinesBackground(ctx);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderTextBoxBackground: function(ctx) {
|
|
if (!this.backgroundColor) {
|
|
return;
|
|
}
|
|
|
|
ctx.fillStyle = this.backgroundColor;
|
|
|
|
ctx.fillRect(
|
|
this._getLeftOffset(),
|
|
this._getTopOffset(),
|
|
this.width,
|
|
this.height
|
|
);
|
|
// if there is background color no other shadows
|
|
// should be casted
|
|
this._removeShadow(ctx);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderTextLinesBackground: function(ctx) {
|
|
if (!this.textBackgroundColor) {
|
|
return;
|
|
}
|
|
var lineTopOffset = 0, heightOfLine,
|
|
lineWidth, lineLeftOffset;
|
|
|
|
ctx.fillStyle = this.textBackgroundColor;
|
|
for (var i = 0, len = this._textLines.length; i < len; i++) {
|
|
heightOfLine = this._getHeightOfLine(ctx, i);
|
|
lineWidth = this._getLineWidth(ctx, i);
|
|
if (lineWidth > 0) {
|
|
lineLeftOffset = this._getLineLeftOffset(lineWidth);
|
|
ctx.fillRect(
|
|
this._getLeftOffset() + lineLeftOffset,
|
|
this._getTopOffset() + lineTopOffset,
|
|
lineWidth,
|
|
heightOfLine / this.lineHeight
|
|
);
|
|
}
|
|
lineTopOffset += heightOfLine;
|
|
}
|
|
// if there is text background color no
|
|
// other shadows should be casted
|
|
this._removeShadow(ctx);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Number} lineWidth Width of text line
|
|
* @return {Number} Line left offset
|
|
*/
|
|
_getLineLeftOffset: function(lineWidth) {
|
|
if (this.textAlign === 'center') {
|
|
return (this.width - lineWidth) / 2;
|
|
}
|
|
if (this.textAlign === 'right') {
|
|
return this.width - lineWidth;
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_clearCache: function() {
|
|
this.__lineWidths = [ ];
|
|
this.__lineHeights = [ ];
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_shouldClearCache: function() {
|
|
var shouldClear = false;
|
|
if (this._forceClearCache) {
|
|
this._forceClearCache = false;
|
|
return true;
|
|
}
|
|
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 {Number} lineIndex line number
|
|
* @return {Number} Line width
|
|
*/
|
|
_getLineWidth: function(ctx, lineIndex) {
|
|
if (this.__lineWidths[lineIndex]) {
|
|
return this.__lineWidths[lineIndex] === -1 ? this.width : this.__lineWidths[lineIndex];
|
|
}
|
|
|
|
var width, wordCount, line = this._textLines[lineIndex];
|
|
|
|
if (line === '') {
|
|
width = 0;
|
|
}
|
|
else {
|
|
width = this._measureLine(ctx, lineIndex);
|
|
}
|
|
this.__lineWidths[lineIndex] = width;
|
|
|
|
if (width && this.textAlign === 'justify') {
|
|
wordCount = line.split(/\s+/);
|
|
if (wordCount.length > 1) {
|
|
this.__lineWidths[lineIndex] = -1;
|
|
}
|
|
}
|
|
return width;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
* @param {Number} lineIndex line number
|
|
* @return {Number} Line width
|
|
*/
|
|
_measureLine: function(ctx, lineIndex) {
|
|
return ctx.measureText(this._textLines[lineIndex]).width;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_renderTextDecoration: function(ctx) {
|
|
if (!this.textDecoration) {
|
|
return;
|
|
}
|
|
|
|
var halfOfVerticalBox = this.height / 2,
|
|
_this = this, offsets = [];
|
|
|
|
/** @ignore */
|
|
function renderLinesAtOffset(offsets) {
|
|
var i, lineHeight = 0, len, j, oLen, lineWidth,
|
|
lineLeftOffset, heightOfLine;
|
|
|
|
for (i = 0, len = _this._textLines.length; i < len; i++) {
|
|
|
|
lineWidth = _this._getLineWidth(ctx, i),
|
|
lineLeftOffset = _this._getLineLeftOffset(lineWidth),
|
|
heightOfLine = _this._getHeightOfLine(ctx, i);
|
|
|
|
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) {
|
|
offsets.push(0.85); // 1 - 3/16
|
|
}
|
|
if (this.textDecoration.indexOf('line-through') > -1) {
|
|
offsets.push(0.43);
|
|
}
|
|
if (this.textDecoration.indexOf('overline') > -1) {
|
|
offsets.push(-0.12);
|
|
}
|
|
if (offsets.length > 0) {
|
|
renderLinesAtOffset(offsets);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_getFontDeclaration: function() {
|
|
return [
|
|
// node-canvas needs "weight style", while browsers need "style weight"
|
|
(fabric.isLikelyNode ? this.fontWeight : this.fontStyle),
|
|
(fabric.isLikelyNode ? this.fontStyle : this.fontWeight),
|
|
this.fontSize + 'px',
|
|
(fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily)
|
|
].join(' ');
|
|
},
|
|
|
|
/**
|
|
* Renders text instance on a specified context
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
render: function(ctx, noTransform) {
|
|
// do not render if object is not visible
|
|
if (!this.visible) {
|
|
return;
|
|
}
|
|
|
|
ctx.save();
|
|
this._setTextStyles(ctx);
|
|
|
|
if (this._shouldClearCache()) {
|
|
this._initDimensions(ctx);
|
|
}
|
|
this.drawSelectionBackground(ctx);
|
|
if (!noTransform) {
|
|
this.transform(ctx);
|
|
}
|
|
if (this.transformMatrix) {
|
|
ctx.transform.apply(ctx, this.transformMatrix);
|
|
}
|
|
if (this.group && this.group.type === 'path-group') {
|
|
ctx.translate(this.left, this.top);
|
|
}
|
|
this._render(ctx);
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Returns the text as an array of lines.
|
|
* @returns {Array} Lines in the text
|
|
*/
|
|
_splitTextIntoLines: function() {
|
|
return this.text.split(this._reNewline);
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of an instance
|
|
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
|
|
* @return {Object} Object representation of an instance
|
|
*/
|
|
toObject: function(propertiesToInclude) {
|
|
var object = extend(this.callSuper('toObject', propertiesToInclude), {
|
|
text: this.text,
|
|
fontSize: this.fontSize,
|
|
fontWeight: this.fontWeight,
|
|
fontFamily: this.fontFamily,
|
|
fontStyle: this.fontStyle,
|
|
lineHeight: this.lineHeight,
|
|
textDecoration: this.textDecoration,
|
|
textAlign: this.textAlign,
|
|
textBackgroundColor: this.textBackgroundColor
|
|
});
|
|
if (!this.includeDefaultValues) {
|
|
this._removeDefaultValues(object);
|
|
}
|
|
return object;
|
|
},
|
|
|
|
/* _TO_SVG_START_ */
|
|
/**
|
|
* Returns SVG representation of an instance
|
|
* @param {Function} [reviver] Method for further parsing of svg representation.
|
|
* @return {String} svg representation of an instance
|
|
*/
|
|
toSVG: function(reviver) {
|
|
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('');
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_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),
|
|
lineTop: lineTop
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_wrapSVGTextAndBg: function(markup, textAndBg) {
|
|
var noShadow = true, filter = this.getSvgFilter(),
|
|
style = filter === '' ? '' : ' style="' + filter + '"';
|
|
|
|
markup.push(
|
|
'\t<g ', this.getSvgId(), 'transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"',
|
|
style, '>\n',
|
|
textAndBg.textBgRects.join(''),
|
|
'\t\t<text ',
|
|
(this.fontFamily ? 'font-family="' + this.fontFamily.replace(/"/g, '\'') + '" ': ''),
|
|
(this.fontSize ? 'font-size="' + this.fontSize + '" ': ''),
|
|
(this.fontStyle ? 'font-style="' + this.fontStyle + '" ': ''),
|
|
(this.fontWeight ? 'font-weight="' + this.fontWeight + '" ': ''),
|
|
(this.textDecoration ? 'text-decoration="' + this.textDecoration + '" ': ''),
|
|
'style="', this.getSvgStyles(noShadow), '" >\n',
|
|
textAndBg.textSpans.join(''),
|
|
'\t\t</text>\n',
|
|
'\t</g>\n'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @param {Number} textTopOffset Text top offset
|
|
* @param {Number} textLeftOffset Text left offset
|
|
* @return {Object}
|
|
*/
|
|
_getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
|
|
var textSpans = [ ],
|
|
textBgRects = [ ],
|
|
height = 0;
|
|
// bounding-box background
|
|
this._setSVGBg(textBgRects);
|
|
|
|
// text and text-background
|
|
for (var i = 0, len = this._textLines.length; i < len; i++) {
|
|
if (this.textBackgroundColor) {
|
|
this._setSVGTextLineBg(textBgRects, i, textLeftOffset, textTopOffset, height);
|
|
}
|
|
this._setSVGTextLineText(i, textSpans, height, textLeftOffset, textTopOffset, textBgRects);
|
|
height += this._getHeightOfLine(this.ctx, i);
|
|
}
|
|
|
|
return {
|
|
textSpans: textSpans,
|
|
textBgRects: textBgRects
|
|
};
|
|
},
|
|
|
|
_setSVGTextLineText: function(i, textSpans, height, textLeftOffset, textTopOffset) {
|
|
var yPos = this.fontSize * (this._fontSizeMult - this._fontSizeFraction)
|
|
- textTopOffset + height - this.height / 2;
|
|
if (this.textAlign === 'justify') {
|
|
// i call from here to do not intefere with IText
|
|
this._setSVGTextLineJustifed(i, textSpans, yPos, textLeftOffset);
|
|
return;
|
|
}
|
|
textSpans.push(
|
|
'\t\t\t<tspan x="',
|
|
toFixed(textLeftOffset + this._getLineLeftOffset(this._getLineWidth(this.ctx, i)), NUM_FRACTION_DIGITS), '" ',
|
|
'y="',
|
|
toFixed(yPos, NUM_FRACTION_DIGITS),
|
|
'" ',
|
|
// doing this on <tspan> elements since setting opacity
|
|
// on containing <text> one doesn't work in Illustrator
|
|
this._getFillAttributes(this.fill), '>',
|
|
fabric.util.string.escapeXml(this._textLines[i]),
|
|
'</tspan>\n'
|
|
);
|
|
},
|
|
|
|
_setSVGTextLineJustifed: function(i, textSpans, yPos, textLeftOffset) {
|
|
var ctx = fabric.util.createCanvasElement().getContext('2d');
|
|
|
|
this._setTextStyles(ctx);
|
|
|
|
var line = this._textLines[i],
|
|
words = line.split(/\s+/),
|
|
wordsWidth = this._getWidthOfWords(ctx, line),
|
|
widthDiff = this.width - wordsWidth,
|
|
numSpaces = words.length - 1,
|
|
spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0,
|
|
word, attributes = this._getFillAttributes(this.fill),
|
|
len;
|
|
|
|
textLeftOffset += this._getLineLeftOffset(this._getLineWidth(ctx, i));
|
|
|
|
for (i = 0, len = words.length; i < len; i++) {
|
|
word = words[i];
|
|
textSpans.push(
|
|
'\t\t\t<tspan x="',
|
|
toFixed(textLeftOffset, NUM_FRACTION_DIGITS), '" ',
|
|
'y="',
|
|
toFixed(yPos, NUM_FRACTION_DIGITS),
|
|
'" ',
|
|
// doing this on <tspan> elements since setting opacity
|
|
// on containing <text> one doesn't work in Illustrator
|
|
attributes, '>',
|
|
fabric.util.string.escapeXml(word),
|
|
'</tspan>\n'
|
|
);
|
|
textLeftOffset += this._getWidthOfWords(ctx, word) + spaceWidth;
|
|
}
|
|
},
|
|
|
|
_setSVGTextLineBg: function(textBgRects, i, textLeftOffset, textTopOffset, height) {
|
|
textBgRects.push(
|
|
'\t\t<rect ',
|
|
this._getFillAttributes(this.textBackgroundColor),
|
|
' x="',
|
|
toFixed(textLeftOffset + this._getLineLeftOffset(this._getLineWidth(this.ctx, i)), NUM_FRACTION_DIGITS),
|
|
'" y="',
|
|
toFixed(height - this.height / 2, NUM_FRACTION_DIGITS),
|
|
'" width="',
|
|
toFixed(this._getLineWidth(this.ctx, i), NUM_FRACTION_DIGITS),
|
|
'" height="',
|
|
toFixed(this._getHeightOfLine(this.ctx, i) / this.lineHeight, NUM_FRACTION_DIGITS),
|
|
'"></rect>\n');
|
|
},
|
|
|
|
_setSVGBg: function(textBgRects) {
|
|
if (this.backgroundColor) {
|
|
textBgRects.push(
|
|
'\t\t<rect ',
|
|
this._getFillAttributes(this.backgroundColor),
|
|
' x="',
|
|
toFixed(-this.width / 2, NUM_FRACTION_DIGITS),
|
|
'" y="',
|
|
toFixed(-this.height / 2, NUM_FRACTION_DIGITS),
|
|
'" width="',
|
|
toFixed(this.width, NUM_FRACTION_DIGITS),
|
|
'" height="',
|
|
toFixed(this.height, NUM_FRACTION_DIGITS),
|
|
'"></rect>\n');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values
|
|
* we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1
|
|
*
|
|
* @private
|
|
* @param {Any} value
|
|
* @return {String}
|
|
*/
|
|
_getFillAttributes: function(value) {
|
|
var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : '';
|
|
if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) {
|
|
return 'fill="' + value + '"';
|
|
}
|
|
return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"';
|
|
},
|
|
/* _TO_SVG_END_ */
|
|
|
|
/**
|
|
* Sets specified property to a specified value
|
|
* @param {String} key
|
|
* @param {Any} value
|
|
* @return {fabric.Text} thisArg
|
|
* @chainable
|
|
*/
|
|
_set: function(key, value) {
|
|
this.callSuper('_set', key, value);
|
|
|
|
if (key in this._dimensionAffectingProps) {
|
|
this._initDimensions();
|
|
this.setCoords();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns complexity of an instance
|
|
* @return {Number} complexity
|
|
*/
|
|
complexity: function() {
|
|
return 1;
|
|
}
|
|
});
|
|
|
|
/* _FROM_SVG_START_ */
|
|
/**
|
|
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement})
|
|
* @static
|
|
* @memberOf fabric.Text
|
|
* @see: http://www.w3.org/TR/SVG/text.html#TextElement
|
|
*/
|
|
fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(
|
|
'x y dx dy font-family font-style font-weight font-size text-decoration text-anchor'.split(' '));
|
|
|
|
/**
|
|
* Default SVG font size
|
|
* @static
|
|
* @memberOf fabric.Text
|
|
*/
|
|
fabric.Text.DEFAULT_SVG_FONT_SIZE = 16;
|
|
|
|
/**
|
|
* Returns fabric.Text instance from an SVG element (<b>not yet implemented</b>)
|
|
* @static
|
|
* @memberOf fabric.Text
|
|
* @param {SVGElement} element Element to parse
|
|
* @param {Object} [options] Options object
|
|
* @return {fabric.Text} Instance of fabric.Text
|
|
*/
|
|
fabric.Text.fromElement = function(element, options) {
|
|
if (!element) {
|
|
return null;
|
|
}
|
|
|
|
var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES);
|
|
options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes);
|
|
|
|
options.top = options.top || 0;
|
|
options.left = options.left || 0;
|
|
if ('dx' in parsedAttributes) {
|
|
options.left += parsedAttributes.dx;
|
|
}
|
|
if ('dy' in parsedAttributes) {
|
|
options.top += parsedAttributes.dy;
|
|
}
|
|
if (!('fontSize' in options)) {
|
|
options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;
|
|
}
|
|
|
|
if (!options.originX) {
|
|
options.originX = 'left';
|
|
}
|
|
|
|
var textContent = '';
|
|
|
|
// The XML is not properly parsed in IE9 so a workaround to get
|
|
// textContent is through firstChild.data. Another workaround would be
|
|
// to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does)
|
|
if (!('textContent' in element)) {
|
|
if ('firstChild' in element && element.firstChild !== null) {
|
|
if ('data' in element.firstChild && element.firstChild.data !== null) {
|
|
textContent = element.firstChild.data;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
textContent = element.textContent;
|
|
}
|
|
|
|
textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' ');
|
|
|
|
var text = new fabric.Text(textContent, options),
|
|
/*
|
|
Adjust positioning:
|
|
x/y attributes in SVG correspond to the bottom-left corner of text bounding box
|
|
top/left properties in Fabric correspond to center point of text bounding box
|
|
*/
|
|
offX = 0;
|
|
|
|
if (text.originX === 'left') {
|
|
offX = text.getWidth() / 2;
|
|
}
|
|
if (text.originX === 'right') {
|
|
offX = -text.getWidth() / 2;
|
|
}
|
|
text.set({
|
|
left: text.getLeft() + offX,
|
|
top: text.getTop() - text.getHeight() / 2 + text.fontSize * (0.18 + text._fontSizeFraction) /* 0.3 is the old lineHeight */
|
|
});
|
|
|
|
return text;
|
|
};
|
|
/* _FROM_SVG_END_ */
|
|
|
|
/**
|
|
* Returns fabric.Text instance from an object representation
|
|
* @static
|
|
* @memberOf fabric.Text
|
|
* @param {Object} object Object to create an instance from
|
|
* @return {fabric.Text} Instance of fabric.Text
|
|
*/
|
|
fabric.Text.fromObject = function(object) {
|
|
return new fabric.Text(object.text, clone(object));
|
|
};
|
|
|
|
fabric.util.createAccessors(fabric.Text);
|
|
|
|
})(typeof exports !== 'undefined' ? exports : this);
|