';
- },
-
- /**
- * 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) {
- 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 = 'alphabetic';
- 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 lineTopOffset = 0, heightOfLine,
- lineLeftOffset, originalFill = ctx.fillStyle,
- line, lastColor,
- leftOffset = this._getLeftOffset(),
- topOffset = this._getTopOffset(),
- boxStart = 0, boxWidth = 0, charBox, currentColor;
-
- 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 (currentColor !== lastColor) {
- ctx.fillStyle = lastColor;
- lastColor && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + lineTopOffset,
- boxWidth,
- heightOfLine / this.lineHeight
- );
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastColor = currentColor;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- if (currentColor) {
- ctx.fillStyle = currentColor;
- ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + 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];
- },
-
- /**
- * apply all the character style to canvas for rendering
- * @private
- * @param {String} _char
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} [decl]
- */
- _applyCharStyles: function(method, ctx, lineIndex, charIndex, styleDeclaration) {
-
- this._setFillStyles(ctx, styleDeclaration);
- this._setStrokeStyles(ctx, styleDeclaration);
-
- ctx.font = this._getFontDeclaration(styleDeclaration);
- },
-
- /**
- * 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);
-
- 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
- };
- return { width: width, numOfSpaces: numOfSpaces };
- },
-
- /**
- * 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.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(),
- offsets = this._applyPatternGradientTransform(ctx, method === 'fillText' ? this.fill : this.stroke);
- 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 - offsets.offsetX,
- top + lineHeights + maxHeight - offsets.offsetY,
- 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
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {String} line Content of the line
- * @param {Number} left
- * @param {Number} top
- * @param {Number} lineIndex
- * @param {Number} charOffset
- */
- _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,
- shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex);
-
- ctx.save();
- top -= lineHeight * this._fontSizeFraction / this.lineHeight;
- if (shortCut) {
- // render all the line in one pass without checking
- this._renderChar(method, ctx, lineIndex, 0, this.textLines[lineIndex], left, top, lineHeight);
- ctx.restore();
- return;
- }
- for (var i = 0, len = line.length - 1; i <= len; i++) {
- timeToRender = i === len || this.charSpacing;
- charsToRender += line[i];
- charBox = this.__charBounds[lineIndex][i];
- if (boxWidth === 0) {
- left += 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) {
- this._renderChar(method, ctx, lineIndex, i, charsToRender, left, top, lineHeight);
- charsToRender = '';
- actualStyle = nextStyle;
- left += boxWidth;
- boxWidth = 0;
- }
- }
- ctx.restore();
- },
-
- /**
- * @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;
-
- if (!shouldStroke && !shouldFill) {
- return;
- }
- decl && ctx.save();
-
- this._applyCharStyles(method, ctx, lineIndex, charIndex, fullDecl);
-
- 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} 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);
- if (this.textAlign === 'center') {
- return (this.width - lineWidth) / 2;
- }
- if (this.textAlign === 'right') {
- return this.width - lineWidth;
- }
- if (this.textAlign === 'justify-center' && this.isEndOfWrapping(lineIndex)) {
- return (this.width - lineWidth) / 2;
- }
- if (this.textAlign === 'justify-right' && this.isEndOfWrapping(lineIndex)) {
- return this.width - lineWidth;
- }
- return 0;
- },
-
- /**
- * @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]) {
- return this.__lineWidths[lineIndex];
- }
-
- var width, line = this._textLines[lineIndex], lineInfo;
-
- if (line === '') {
- width = 0;
- }
- else {
- lineInfo = this.measureLine(lineIndex);
- 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 charater 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,
- charSpacing = this._getWidthOfCharSpacing();
-
- 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 ((currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) &&
- boxWidth > 0) {
- ctx.fillStyle = lastFill;
- lastDecoration && lastFill && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- top + this.offsets[type] * size + dy,
- boxWidth,
- this.fontSize / 15
- );
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastDecoration = currentDecoration;
- lastFill = currentFill;
- size = _size;
- dy = _dy;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- ctx.fillStyle = currentFill;
- currentDecoration && currentFill && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- top + this.offsets[type] * 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: function(ctx) {
- // do not render if object is not visible
- if (!this.visible) {
- return;
- }
- if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) {
- return;
- }
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- }
- this.callSuper('render', ctx);
- },
-
- /**
- * Returns the text as an array of lines.
- * @param {String} text text to split
- * @returns {Array} Lines in the text
- */
- _splitTextIntoLines: function(text) {
- var lines = text.split(this._reNewline),
- newLines = new Array(lines.length),
- newLine = ['\n'],
- newText = [];
- for (var i = 0; i < lines.length; i++) {
- newLines[i] = fabric.util.string.graphemeSplit(lines[i]);
- newText = newText.concat(newLines[i], newLine);
- }
- newText.pop();
- return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines };
- },
-
- /**
- * 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 additionalProperties = [
- 'text',
- 'fontSize',
- 'fontWeight',
- 'fontFamily',
- 'fontStyle',
- 'lineHeight',
- 'underline',
- 'overline',
- 'linethrough',
- 'textAlign',
- 'textBackgroundColor',
- 'charSpacing',
- ].concat(propertiesToInclude);
- var obj = this.callSuper('toObject', additionalProperties);
- obj.styles = clone(this.styles, true);
- return obj;
- },
-
- /**
- * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
- * @param {String|Object} key Property name or object (if object, iterate over the object properties)
- * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one)
- * @return {fabric.Object} thisArg
- * @chainable
- */
- set: function(key, value) {
- this.callSuper('set', key, value);
- var needsDims = false;
- if (typeof key === 'object') {
- for (var _key in key) {
- needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1;
- }
- }
- else {
- needsDims = this._dimensionAffectingProps.indexOf(key) !== -1;
- }
- if (needsDims) {
- this.initDimensions();
- this.setCoords();
- }
- return this;
- },
-
- /**
- * 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 letter-spacing 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 (not yet implemented)
- * @static
- * @memberOf fabric.Text
- * @param {SVGElement} element Element to parse
- * @param {Function} callback callback function invoked after parsing
- * @param {Object} [options] Options object
- */
- fabric.Text.fromElement = function(element, callback, options) {
- if (!element) {
- return callback(null);
- }
-
- var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES),
- parsedAnchor = parsedAttributes.textAnchor || 'left';
- options = fabric.util.object.extend((options ? clone(options) : { }), parsedAttributes);
-
- options.top = options.top || 0;
- options.left = options.left || 0;
- if (parsedAttributes.textDecoration) {
- var textDecoration = parsedAttributes.textDecoration;
- if (textDecoration.indexOf('underline') !== -1) {
- options.underline = true;
- }
- if (textDecoration.indexOf('overline') !== -1) {
- options.overline = true;
- }
- if (textDecoration.indexOf('line-through') !== -1) {
- options.linethrough = true;
- }
- delete options.textDecoration;
- }
- 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;
- }
-
- 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 originalStrokeWidth = options.strokeWidth;
- options.strokeWidth = 0;
-
- var text = new fabric.Text(textContent, options),
- textHeightScaleFactor = text.getScaledHeight() / text.height,
- lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height,
- scaledDiff = lineHeightDiff * textHeightScaleFactor,
- textHeight = text.getScaledHeight() + scaledDiff,
- offX = 0;
- /*
- Adjust positioning:
- x/y attributes in SVG correspond to the bottom-left corner of text bounding box
- fabric output by default at top, left.
- */
- if (parsedAnchor === 'center') {
- offX = text.getScaledWidth() / 2;
- }
- if (parsedAnchor === 'right') {
- offX = text.getScaledWidth();
- }
- text.set({
- left: text.left - offX,
- top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight,
- strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1,
- });
- callback(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
- * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created
- */
- fabric.Text.fromObject = function(object, callback) {
- return fabric.Object._fromObject('Text', object, callback, 'text');
- };
-
- fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace'];
-
- fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text);
-
-})(typeof exports !== 'undefined' ? exports : this);
-
-
-(function() {
- fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
- /**
- * Returns true if object has no styling or no styling in a line
- * @param {Number} lineIndex , lineIndex is on wrapped lines.
- * @return {Boolean}
- */
- isEmptyStyles: function(lineIndex) {
- if (!this.styles) {
- return true;
- }
- if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
- return true;
- }
- var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
- for (var p1 in obj) {
- for (var p2 in obj[p1]) {
- // eslint-disable-next-line no-unused-vars
- for (var p3 in obj[p1][p2]) {
- return false;
- }
- }
- }
- return true;
- },
-
- /**
- * Returns true if object has a style property or has it ina specified line
- * This function is used to detect if a text will use a particular property or not.
- * @param {String} property to check for
- * @param {Number} lineIndex to check the style on
- * @return {Boolean}
- */
- styleHas: function(property, lineIndex) {
- if (!this.styles || !property || property === '') {
- return false;
- }
- if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
- return false;
- }
- var obj = typeof lineIndex === 'undefined' ? this.styles : { 0: this.styles[lineIndex] };
- // eslint-disable-next-line
- for (var p1 in obj) {
- // eslint-disable-next-line
- for (var p2 in obj[p1]) {
- if (typeof obj[p1][p2][property] !== 'undefined') {
- return true;
- }
- }
- }
- return false;
- },
-
- /**
- * Check if characters in a text have a value for a property
- * whose value matches the textbox's value for that property. If so,
- * the character-level property is deleted. If the character
- * has no other properties, then it is also deleted. Finally,
- * if the line containing that character has no other characters
- * then it also is deleted.
- *
- * @param {string} property The property to compare between characters and text.
- */
- cleanStyle: function(property) {
- if (!this.styles || !property || property === '') {
- return false;
- }
- var obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue,
- allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject;
- // eslint-disable-next-line
- for (var p1 in obj) {
- letterCount = 0;
- // eslint-disable-next-line
- for (var p2 in obj[p1]) {
- var styleObject = obj[p1][p2],
- stylePropertyHasBeenSet = styleObject.hasOwnProperty(property);
-
- stylesCount++;
-
- if (stylePropertyHasBeenSet) {
- if (!stylePropertyValue) {
- stylePropertyValue = styleObject[property];
- }
- else if (styleObject[property] !== stylePropertyValue) {
- allStyleObjectPropertiesMatch = false;
- }
-
- if (styleObject[property] === this[property]) {
- delete styleObject[property];
- }
- }
- else {
- allStyleObjectPropertiesMatch = false;
- }
-
- if (Object.keys(styleObject).length !== 0) {
- letterCount++;
- }
- else {
- delete obj[p1][p2];
- }
- }
-
- if (letterCount === 0) {
- delete obj[p1];
- }
- }
- // if every grapheme has the same style set then
- // delete those styles and set it on the parent
- for (var i = 0; i < this._textLines.length; i++) {
- graphemeCount += this._textLines[i].length;
- }
- if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) {
- this[property] = stylePropertyValue;
- this.removeStyle(property);
- }
- },
-
- /**
- * Remove a style property or properties from all individual character styles
- * in a text object. Deletes the character style object if it contains no other style
- * props. Deletes a line style object if it contains no other character styles.
- *
- * @param {String} props The property to remove from character styles.
- */
- removeStyle: function(property) {
- if (!this.styles || !property || property === '') {
- return;
- }
- var obj = this.styles, line, lineNum, charNum;
- for (lineNum in obj) {
- line = obj[lineNum];
- for (charNum in line) {
- delete line[charNum][property];
- if (Object.keys(line[charNum]).length === 0) {
- delete line[charNum];
- }
- }
- if (Object.keys(line).length === 0) {
- delete obj[lineNum];
- }
- }
- },
-
- /**
- * @private
- */
- _extendStyles: function(index, styles) {
- var loc = this.get2DCursorLocation(index);
-
- if (!this._getLineStyle(loc.lineIndex)) {
- this._setLineStyle(loc.lineIndex);
- }
-
- if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) {
- this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {});
- }
-
- fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles);
- },
-
- /**
- * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start)
- * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
- * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. usefull to manage styles.
- */
- get2DCursorLocation: function(selectionStart, skipWrapping) {
- if (typeof selectionStart === 'undefined') {
- selectionStart = this.selectionStart;
- }
- var lines = skipWrapping ? this._unwrappedTextLines : this._textLines,
- len = lines.length;
- for (var i = 0; i < len; i++) {
- if (selectionStart <= lines[i].length) {
- return {
- lineIndex: i,
- charIndex: selectionStart
- };
- }
- selectionStart -= lines[i].length + this.missingNewlineOffset(i);
- }
- return {
- lineIndex: i - 1,
- charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart
- };
- },
-
- /**
- * Gets style of a current selection/cursor (at the start position)
- * if startIndex or endIndex are not provided, slectionStart or selectionEnd will be used.
- * @param {Number} [startIndex] Start index to get styles at
- * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
- * @param {Boolean} [complete] get full style or not
- * @return {Array} styles an array with one, zero or more Style objects
- */
- getSelectionStyles: function(startIndex, endIndex, complete) {
- if (typeof startIndex === 'undefined') {
- startIndex = this.selectionStart || 0;
- }
- if (typeof endIndex === 'undefined') {
- endIndex = this.selectionEnd || startIndex;
- }
- var styles = [];
- for (var i = startIndex; i < endIndex; i++) {
- styles.push(this.getStyleAtPosition(i, complete));
- }
- return styles;
- },
-
- /**
- * Gets style of a current selection/cursor position
- * @param {Number} position to get styles at
- * @param {Boolean} [complete] full style if true
- * @return {Object} style Style object at a specified index
- * @private
- */
- getStyleAtPosition: function(position, complete) {
- var loc = this.get2DCursorLocation(position),
- style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) :
- this._getStyleDeclaration(loc.lineIndex, loc.charIndex);
- return style || {};
- },
-
- /**
- * Sets style of a current selection, if no selection exist, do not set anything.
- * @param {Object} [styles] Styles object
- * @param {Number} [startIndex] Start index to get styles at
- * @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
- * @return {fabric.IText} thisArg
- * @chainable
- */
- setSelectionStyles: function(styles, startIndex, endIndex) {
- if (typeof startIndex === 'undefined') {
- startIndex = this.selectionStart || 0;
- }
- if (typeof endIndex === 'undefined') {
- endIndex = this.selectionEnd || startIndex;
- }
- for (var i = startIndex; i < endIndex; i++) {
- this._extendStyles(i, styles);
- }
- /* not included in _extendStyles to avoid clearing cache more than once */
- this._forceClearCache = true;
- return this;
- },
-
- /**
- * get the reference, not a clone, of the style object for a given character
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @return {Object} style object
- */
- _getStyleDeclaration: function(lineIndex, charIndex) {
- var lineStyle = this.styles && this.styles[lineIndex];
- if (!lineStyle) {
- return null;
- }
- return lineStyle[charIndex];
- },
-
- /**
- * return a new object that contains all the style property for a character
- * the object returned is newly created
- * @param {Number} lineIndex of the line where the character is
- * @param {Number} charIndex position of the character on the line
- * @return {Object} style object
- */
- getCompleteStyleDeclaration: function(lineIndex, charIndex) {
- var style = this._getStyleDeclaration(lineIndex, charIndex) || { },
- styleObject = { }, prop;
- for (var i = 0; i < this._styleProperties.length; i++) {
- prop = this._styleProperties[i];
- styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop];
- }
- return styleObject;
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} style
- * @private
- */
- _setStyleDeclaration: function(lineIndex, charIndex, style) {
- this.styles[lineIndex][charIndex] = style;
- },
-
- /**
- *
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _deleteStyleDeclaration: function(lineIndex, charIndex) {
- delete this.styles[lineIndex][charIndex];
- },
-
- /**
- * @param {Number} lineIndex
- * @return {Boolean} if the line exists or not
- * @private
- */
- _getLineStyle: function(lineIndex) {
- return !!this.styles[lineIndex];
- },
-
- /**
- * Set the line style to an empty object so that is initialized
- * @param {Number} lineIndex
- * @private
- */
- _setLineStyle: function(lineIndex) {
- this.styles[lineIndex] = {};
- },
-
- /**
- * @param {Number} lineIndex
- * @private
- */
- _deleteLineStyle: function(lineIndex) {
- delete this.styles[lineIndex];
- }
- });
-})();
-
-
-(function() {
-
- function parseDecoration(object) {
- if (object.textDecoration) {
- object.textDecoration.indexOf('underline') > -1 && (object.underline = true);
- object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true);
- object.textDecoration.indexOf('overline') > -1 && (object.overline = true);
- delete object.textDecoration;
- }
- }
-
- /**
- * IText class (introduced in v1.4) Events are also fired with "text:"
- * prefix when observing canvas.
- * @class fabric.IText
- * @extends fabric.Text
- * @mixes fabric.Observable
- *
- * @fires changed
- * @fires selection:changed
- * @fires editing:entered
- * @fires editing:exited
- *
- * @return {fabric.IText} thisArg
- * @see {@link fabric.IText#initialize} for constructor definition
- *
- * Supported key combinations:
- *
- * Move cursor: left, right, up, down
- * Select character: shift + left, shift + right
- * Select text vertically: shift + up, shift + down
- * Move cursor by word: alt + left, alt + right
- * Select words: shift + alt + left, shift + alt + right
- * Move cursor to line start/end: cmd + left, cmd + right or home, end
- * Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end
- * Jump to start/end of text: cmd + up, cmd + down
- * Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
- * Delete character: backspace
- * Delete word: alt + backspace
- * Delete line: cmd + backspace
- * Forward delete: delete
- * Copy text: ctrl/cmd + c
- * Paste text: ctrl/cmd + v
- * Cut text: ctrl/cmd + x
- * Select entire text: ctrl/cmd + a
- * Quit editing tab or esc
- *
- *
- * Supported mouse/touch combination
- *
- * Position cursor: click/touch
- * Create selection: click/touch & drag
- * Create selection: click & shift + click
- * Select word: double click
- * Select line: triple click
- *
- */
- fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ {
-
- /**
- * Type of an object
- * @type String
- * @default
- */
- type: 'i-text',
-
- /**
- * Index where text selection starts (or where cursor is when there is no selection)
- * @type Number
- * @default
- */
- selectionStart: 0,
-
- /**
- * Index where text selection ends
- * @type Number
- * @default
- */
- selectionEnd: 0,
-
- /**
- * Color of text selection
- * @type String
- * @default
- */
- selectionColor: 'rgba(17,119,255,0.3)',
-
- /**
- * Indicates whether text is in editing mode
- * @type Boolean
- * @default
- */
- isEditing: false,
-
- /**
- * Indicates whether a text can be edited
- * @type Boolean
- * @default
- */
- editable: true,
-
- /**
- * Border color of text object while it's in editing mode
- * @type String
- * @default
- */
- editingBorderColor: 'rgba(102,153,255,0.25)',
-
- /**
- * Width of cursor (in px)
- * @type Number
- * @default
- */
- cursorWidth: 2,
-
- /**
- * Color of default cursor (when not overwritten by character style)
- * @type String
- * @default
- */
- cursorColor: '#333',
-
- /**
- * Delay between cursor blink (in ms)
- * @type Number
- * @default
- */
- cursorDelay: 1000,
-
- /**
- * Duration of cursor fadein (in ms)
- * @type Number
- * @default
- */
- cursorDuration: 600,
-
- /**
- * Indicates whether internal text char widths can be cached
- * @type Boolean
- * @default
- */
- caching: true,
-
- /**
- * @private
- */
- _reSpace: /\s|\n/,
-
- /**
- * @private
- */
- _currentCursorOpacity: 0,
-
- /**
- * @private
- */
- _selectionDirection: null,
-
- /**
- * @private
- */
- _abortCursorAnimation: false,
-
- /**
- * @private
- */
- __widthOfSpace: [],
-
- /**
- * Helps determining when the text is in composition, so that the cursor
- * rendering is altered.
- */
- inCompositionMode: false,
-
- /**
- * Constructor
- * @param {String} text Text string
- * @param {Object} [options] Options object
- * @return {fabric.IText} thisArg
- */
- initialize: function(text, options) {
- this.callSuper('initialize', text, options);
- this.initBehavior();
- },
-
- /**
- * Sets selection start (left boundary of a selection)
- * @param {Number} index Index to set selection start to
- */
- setSelectionStart: function(index) {
- index = Math.max(index, 0);
- this._updateAndFire('selectionStart', index);
- },
-
- /**
- * Sets selection end (right boundary of a selection)
- * @param {Number} index Index to set selection end to
- */
- setSelectionEnd: function(index) {
- index = Math.min(index, this.text.length);
- this._updateAndFire('selectionEnd', index);
- },
-
- /**
- * @private
- * @param {String} property 'selectionStart' or 'selectionEnd'
- * @param {Number} index new position of property
- */
- _updateAndFire: function(property, index) {
- if (this[property] !== index) {
- this._fireSelectionChanged();
- this[property] = index;
- }
- this._updateTextarea();
- },
-
- /**
- * Fires the even of selection changed
- * @private
- */
- _fireSelectionChanged: function() {
- this.fire('selection:changed');
- this.canvas && this.canvas.fire('text:selection:changed', { target: this });
- },
-
- /**
- * 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.
- * @private
- */
- initDimensions: function() {
- this.isEditing && this.initDelayedCursor();
- this.clearContextTop();
- this.callSuper('initDimensions');
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- render: function(ctx) {
- this.clearContextTop();
- this.callSuper('render', ctx);
- // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
- // the correct position but not at every cursor animation.
- this.cursorOffsetCache = { };
- this.renderCursorOrSelection();
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _render: function(ctx) {
- this.callSuper('_render', ctx);
- },
-
- /**
- * Prepare and clean the contextTop
- */
- clearContextTop: function(skipRestore) {
- if (!this.isEditing || !this.canvas || !this.canvas.contextTop) {
- return;
- }
- var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform;
- ctx.save();
- ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
- this.transform(ctx);
- this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix);
- this._clearTextArea(ctx);
- skipRestore || ctx.restore();
- },
-
- /**
- * Renders cursor or selection (depending on what exists)
- * it does on the contextTop. If contextTop is not available, do nothing.
- */
- renderCursorOrSelection: function() {
- if (!this.isEditing || !this.canvas || !this.canvas.contextTop) {
- return;
- }
- var boundaries = this._getCursorBoundaries(),
- ctx = this.canvas.contextTop;
- this.clearContextTop(true);
- if (this.selectionStart === this.selectionEnd) {
- this.renderCursor(boundaries, ctx);
- }
- else {
- this.renderSelection(boundaries, ctx);
- }
- ctx.restore();
- },
-
- _clearTextArea: function(ctx) {
- // we add 4 pixel, to be sure to do not leave any pixel out
- var width = this.width + 4, height = this.height + 4;
- ctx.clearRect(-width / 2, -height / 2, width, height);
- },
-
- /**
- * Returns cursor boundaries (left, top, leftOffset, topOffset)
- * @private
- * @param {Array} chars Array of characters
- * @param {String} typeOfBoundaries
- */
- _getCursorBoundaries: function(position) {
-
- // left/top are left/top of entire text box
- // leftOffset/topOffset are offset from that left/top point of a text box
-
- if (typeof position === 'undefined') {
- position = this.selectionStart;
- }
-
- var left = this._getLeftOffset(),
- top = this._getTopOffset(),
- offsets = this._getCursorBoundariesOffsets(position);
-
- return {
- left: left,
- top: top,
- leftOffset: offsets.left,
- topOffset: offsets.top
- };
- },
-
- /**
- * @private
- */
- _getCursorBoundariesOffsets: function(position) {
- if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {
- return this.cursorOffsetCache;
- }
- var lineLeftOffset,
- lineIndex,
- charIndex,
- topOffset = 0,
- leftOffset = 0,
- boundaries,
- cursorPosition = this.get2DCursorLocation(position);
- charIndex = cursorPosition.charIndex;
- lineIndex = cursorPosition.lineIndex;
- for (var i = 0; i < lineIndex; i++) {
- topOffset += this.getHeightOfLine(i);
- }
- lineLeftOffset = this._getLineLeftOffset(lineIndex);
- var bound = this.__charBounds[lineIndex][charIndex];
- bound && (leftOffset = bound.left);
- if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {
- leftOffset -= this._getWidthOfCharSpacing();
- }
- boundaries = {
- top: topOffset,
- left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0),
- };
- this.cursorOffsetCache = boundaries;
- return this.cursorOffsetCache;
- },
-
- /**
- * Renders cursor
- * @param {Object} boundaries
- * @param {CanvasRenderingContext2D} ctx transformed context to draw on
- */
- renderCursor: function(boundaries, ctx) {
- var cursorLocation = this.get2DCursorLocation(),
- lineIndex = cursorLocation.lineIndex,
- charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0,
- charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'),
- multiplier = this.scaleX * this.canvas.getZoom(),
- cursorWidth = this.cursorWidth / multiplier,
- topOffset = boundaries.topOffset,
- dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY');
-
- topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight
- - charHeight * (1 - this._fontSizeFraction);
-
- if (this.inCompositionMode) {
- this.renderSelection(boundaries, ctx);
- }
-
- ctx.fillStyle = this.getValueOfPropertyAt(lineIndex, charIndex, 'fill');
- ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;
- ctx.fillRect(
- boundaries.left + boundaries.leftOffset - cursorWidth / 2,
- topOffset + boundaries.top + dy,
- cursorWidth,
- charHeight);
- },
-
- /**
- * Renders text selection
- * @param {Object} boundaries Object with left/top/leftOffset/topOffset
- * @param {CanvasRenderingContext2D} ctx transformed context to draw on
- */
- renderSelection: function(boundaries, ctx) {
-
- var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart,
- selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd,
- isJustify = this.textAlign.indexOf('justify') !== -1,
- start = this.get2DCursorLocation(selectionStart),
- end = this.get2DCursorLocation(selectionEnd),
- startLine = start.lineIndex,
- endLine = end.lineIndex,
- startChar = start.charIndex < 0 ? 0 : start.charIndex,
- endChar = end.charIndex < 0 ? 0 : end.charIndex;
-
- for (var i = startLine; i <= endLine; i++) {
- var lineOffset = this._getLineLeftOffset(i) || 0,
- lineHeight = this.getHeightOfLine(i),
- realLineHeight = 0, boxStart = 0, boxEnd = 0;
-
- if (i === startLine) {
- boxStart = this.__charBounds[startLine][startChar].left;
- }
- if (i >= startLine && i < endLine) {
- boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5?
- }
- else if (i === endLine) {
- if (endChar === 0) {
- boxEnd = this.__charBounds[endLine][endChar].left;
- }
- else {
- var charSpacing = this._getWidthOfCharSpacing();
- boxEnd = this.__charBounds[endLine][endChar - 1].left
- + this.__charBounds[endLine][endChar - 1].width - charSpacing;
- }
- }
- realLineHeight = lineHeight;
- if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) {
- lineHeight /= this.lineHeight;
- }
- if (this.inCompositionMode) {
- ctx.fillStyle = this.compositionColor || 'black';
- ctx.fillRect(
- boundaries.left + lineOffset + boxStart,
- boundaries.top + boundaries.topOffset + lineHeight,
- boxEnd - boxStart,
- 1);
- }
- else {
- ctx.fillStyle = this.selectionColor;
- ctx.fillRect(
- boundaries.left + lineOffset + boxStart,
- boundaries.top + boundaries.topOffset,
- boxEnd - boxStart,
- lineHeight);
- }
-
-
- boundaries.topOffset += realLineHeight;
- }
- },
-
- /**
- * High level function to know the height of the cursor.
- * the currentChar is the one that precedes the cursor
- * Returns fontSize of char at the current cursor
- * @return {Number} Character font size
- */
- getCurrentCharFontSize: function() {
- var cp = this._getCurrentCharIndex();
- return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize');
- },
-
- /**
- * High level function to know the color of the cursor.
- * the currentChar is the one that precedes the cursor
- * Returns color (fill) of char at the current cursor
- * @return {String} Character color (fill)
- */
- getCurrentCharColor: function() {
- var cp = this._getCurrentCharIndex();
- return this.getValueOfPropertyAt(cp.l, cp.c, 'fill');
- },
-
- /**
- * Returns the cursor position for the getCurrent.. functions
- * @private
- */
- _getCurrentCharIndex: function() {
- var cursorPosition = this.get2DCursorLocation(this.selectionStart, true),
- charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0;
- return { l: cursorPosition.lineIndex, c: charIndex };
- }
- });
-
- /**
- * Returns fabric.IText instance from an object representation
- * @static
- * @memberOf fabric.IText
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] invoked with new instance as argument
- */
- fabric.IText.fromObject = function(object, callback) {
- parseDecoration(object);
- if (object.styles) {
- for (var i in object.styles) {
- for (var j in object.styles[i]) {
- parseDecoration(object.styles[i][j]);
- }
- }
- }
- fabric.Object._fromObject('IText', object, callback, 'text');
- };
-})();
-
-
-(function() {
-
- var clone = fabric.util.object.clone;
-
- fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
-
- /**
- * Initializes all the interactive behavior of IText
- */
- initBehavior: function() {
- this.initAddedHandler();
- this.initRemovedHandler();
- this.initCursorSelectionHandlers();
- this.initDoubleClickSimulation();
- this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
- },
-
- onDeselect: function() {
- this.isEditing && this.exitEditing();
- this.selected = false;
- },
-
- /**
- * Initializes "added" event handler
- */
- initAddedHandler: function() {
- var _this = this;
- this.on('added', function() {
- var canvas = _this.canvas;
- if (canvas) {
- if (!canvas._hasITextHandlers) {
- canvas._hasITextHandlers = true;
- _this._initCanvasHandlers(canvas);
- }
- canvas._iTextInstances = canvas._iTextInstances || [];
- canvas._iTextInstances.push(_this);
- }
- });
- },
-
- initRemovedHandler: function() {
- var _this = this;
- this.on('removed', function() {
- var canvas = _this.canvas;
- if (canvas) {
- canvas._iTextInstances = canvas._iTextInstances || [];
- fabric.util.removeFromArray(canvas._iTextInstances, _this);
- if (canvas._iTextInstances.length === 0) {
- canvas._hasITextHandlers = false;
- _this._removeCanvasHandlers(canvas);
- }
- }
- });
- },
-
- /**
- * register canvas event to manage exiting on other instances
- * @private
- */
- _initCanvasHandlers: function(canvas) {
- canvas._mouseUpITextHandler = function() {
- if (canvas._iTextInstances) {
- canvas._iTextInstances.forEach(function(obj) {
- obj.__isMousedown = false;
- });
- }
- };
- canvas.on('mouse:up', canvas._mouseUpITextHandler);
- },
-
- /**
- * remove canvas event to manage exiting on other instances
- * @private
- */
- _removeCanvasHandlers: function(canvas) {
- canvas.off('mouse:up', canvas._mouseUpITextHandler);
- },
-
- /**
- * @private
- */
- _tick: function() {
- this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete');
- },
-
- /**
- * @private
- */
- _animateCursor: function(obj, targetOpacity, duration, completeMethod) {
-
- var tickState;
-
- tickState = {
- isAborted: false,
- abort: function() {
- this.isAborted = true;
- },
- };
-
- obj.animate('_currentCursorOpacity', targetOpacity, {
- duration: duration,
- onComplete: function() {
- if (!tickState.isAborted) {
- obj[completeMethod]();
- }
- },
- onChange: function() {
- // we do not want to animate a selection, only cursor
- if (obj.canvas && obj.selectionStart === obj.selectionEnd) {
- obj.renderCursorOrSelection();
- }
- },
- abort: function() {
- return tickState.isAborted;
- }
- });
- return tickState;
- },
-
- /**
- * @private
- */
- _onTickComplete: function() {
-
- var _this = this;
-
- if (this._cursorTimeout1) {
- clearTimeout(this._cursorTimeout1);
- }
- this._cursorTimeout1 = setTimeout(function() {
- _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick');
- }, 100);
- },
-
- /**
- * Initializes delayed cursor
- */
- initDelayedCursor: function(restart) {
- var _this = this,
- delay = restart ? 0 : this.cursorDelay;
-
- this.abortCursorAnimation();
- this._currentCursorOpacity = 1;
- this._cursorTimeout2 = setTimeout(function() {
- _this._tick();
- }, delay);
- },
-
- /**
- * Aborts cursor animation and clears all timeouts
- */
- abortCursorAnimation: function() {
- var shouldClear = this._currentTickState || this._currentTickCompleteState,
- canvas = this.canvas;
- this._currentTickState && this._currentTickState.abort();
- this._currentTickCompleteState && this._currentTickCompleteState.abort();
-
- clearTimeout(this._cursorTimeout1);
- clearTimeout(this._cursorTimeout2);
-
- this._currentCursorOpacity = 0;
- // to clear just itext area we need to transform the context
- // it may not be worth it
- if (shouldClear && canvas) {
- canvas.clearContext(canvas.contextTop || canvas.contextContainer);
- }
-
- },
-
- /**
- * Selects entire text
- * @return {fabric.IText} thisArg
- * @chainable
- */
- selectAll: function() {
- this.selectionStart = 0;
- this.selectionEnd = this._text.length;
- this._fireSelectionChanged();
- this._updateTextarea();
- return this;
- },
-
- /**
- * Returns selected text
- * @return {String}
- */
- getSelectedText: function() {
- return this._text.slice(this.selectionStart, this.selectionEnd).join('');
- },
-
- /**
- * Find new selection index representing start of current word according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findWordBoundaryLeft: function(startFrom) {
- var offset = 0, index = startFrom - 1;
-
- // remove space before cursor first
- if (this._reSpace.test(this._text[index])) {
- while (this._reSpace.test(this._text[index])) {
- offset++;
- index--;
- }
- }
- while (/\S/.test(this._text[index]) && index > -1) {
- offset++;
- index--;
- }
-
- return startFrom - offset;
- },
-
- /**
- * Find new selection index representing end of current word according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findWordBoundaryRight: function(startFrom) {
- var offset = 0, index = startFrom;
-
- // remove space after cursor first
- if (this._reSpace.test(this._text[index])) {
- while (this._reSpace.test(this._text[index])) {
- offset++;
- index++;
- }
- }
- while (/\S/.test(this._text[index]) && index < this._text.length) {
- offset++;
- index++;
- }
-
- return startFrom + offset;
- },
-
- /**
- * Find new selection index representing start of current line according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findLineBoundaryLeft: function(startFrom) {
- var offset = 0, index = startFrom - 1;
-
- while (!/\n/.test(this._text[index]) && index > -1) {
- offset++;
- index--;
- }
-
- return startFrom - offset;
- },
-
- /**
- * Find new selection index representing end of current line according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findLineBoundaryRight: function(startFrom) {
- var offset = 0, index = startFrom;
-
- while (!/\n/.test(this._text[index]) && index < this._text.length) {
- offset++;
- index++;
- }
-
- return startFrom + offset;
- },
-
- /**
- * Finds index corresponding to beginning or end of a word
- * @param {Number} selectionStart Index of a character
- * @param {Number} direction 1 or -1
- * @return {Number} Index of the beginning or end of a word
- */
- searchWordBoundary: function(selectionStart, direction) {
- var text = this._text,
- index = this._reSpace.test(text[selectionStart]) ? selectionStart - 1 : selectionStart,
- _char = text[index],
- // wrong
- reNonWord = fabric.reNonWord;
-
- while (!reNonWord.test(_char) && index > 0 && index < text.length) {
- index += direction;
- _char = text[index];
- }
- if (reNonWord.test(_char)) {
- index += direction === 1 ? 0 : 1;
- }
- return index;
- },
-
- /**
- * Selects a word based on the index
- * @param {Number} selectionStart Index of a character
- */
- selectWord: function(selectionStart) {
- selectionStart = selectionStart || this.selectionStart;
- var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */
- newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */
-
- this.selectionStart = newSelectionStart;
- this.selectionEnd = newSelectionEnd;
- this._fireSelectionChanged();
- this._updateTextarea();
- this.renderCursorOrSelection();
- },
-
- /**
- * Selects a line based on the index
- * @param {Number} selectionStart Index of a character
- * @return {fabric.IText} thisArg
- * @chainable
- */
- selectLine: function(selectionStart) {
- selectionStart = selectionStart || this.selectionStart;
- var newSelectionStart = this.findLineBoundaryLeft(selectionStart),
- newSelectionEnd = this.findLineBoundaryRight(selectionStart);
-
- this.selectionStart = newSelectionStart;
- this.selectionEnd = newSelectionEnd;
- this._fireSelectionChanged();
- this._updateTextarea();
- return this;
- },
-
- /**
- * Enters editing state
- * @return {fabric.IText} thisArg
- * @chainable
- */
- enterEditing: function(e) {
- if (this.isEditing || !this.editable) {
- return;
- }
-
- if (this.canvas) {
- this.canvas.calcOffset();
- this.exitEditingOnOthers(this.canvas);
- }
-
- this.isEditing = true;
-
- this.initHiddenTextarea(e);
- this.hiddenTextarea.focus();
- this.hiddenTextarea.value = this.text;
- this._updateTextarea();
- this._saveEditingProps();
- this._setEditingProps();
- this._textBeforeEdit = this.text;
-
- this._tick();
- this.fire('editing:entered');
- this._fireSelectionChanged();
- if (!this.canvas) {
- return this;
- }
- this.canvas.fire('text:editing:entered', { target: this });
- this.initMouseMoveHandler();
- this.canvas.requestRenderAll();
- return this;
- },
-
- exitEditingOnOthers: function(canvas) {
- if (canvas._iTextInstances) {
- canvas._iTextInstances.forEach(function(obj) {
- obj.selected = false;
- if (obj.isEditing) {
- obj.exitEditing();
- }
- });
- }
- },
-
- /**
- * Initializes "mousemove" event handler
- */
- initMouseMoveHandler: function() {
- this.canvas.on('mouse:move', this.mouseMoveHandler);
- },
-
- /**
- * @private
- */
- mouseMoveHandler: function(options) {
- if (!this.__isMousedown || !this.isEditing) {
- return;
- }
-
- var newSelectionStart = this.getSelectionStartFromPointer(options.e),
- currentStart = this.selectionStart,
- currentEnd = this.selectionEnd;
- if (
- (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd)
- &&
- (currentStart === newSelectionStart || currentEnd === newSelectionStart)
- ) {
- return;
- }
- if (newSelectionStart > this.__selectionStartOnMouseDown) {
- this.selectionStart = this.__selectionStartOnMouseDown;
- this.selectionEnd = newSelectionStart;
- }
- else {
- this.selectionStart = newSelectionStart;
- this.selectionEnd = this.__selectionStartOnMouseDown;
- }
- if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) {
- this.restartCursorIfNeeded();
- this._fireSelectionChanged();
- this._updateTextarea();
- this.renderCursorOrSelection();
- }
- },
-
- /**
- * @private
- */
- _setEditingProps: function() {
- this.hoverCursor = 'text';
-
- if (this.canvas) {
- this.canvas.defaultCursor = this.canvas.moveCursor = 'text';
- }
-
- this.borderColor = this.editingBorderColor;
- this.hasControls = this.selectable = false;
- this.lockMovementX = this.lockMovementY = true;
- },
-
- /**
- * convert from textarea to grapheme indexes
- */
- fromStringToGraphemeSelection: function(start, end, text) {
- var smallerTextStart = text.slice(0, start),
- graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length;
- if (start === end) {
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
- }
- var smallerTextEnd = text.slice(start, end),
- graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length;
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
- },
-
- /**
- * convert from fabric to textarea values
- */
- fromGraphemeToStringSelection: function(start, end, _text) {
- var smallerTextStart = _text.slice(0, start),
- graphemeStart = smallerTextStart.join('').length;
- if (start === end) {
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
- }
- var smallerTextEnd = _text.slice(start, end),
- graphemeEnd = smallerTextEnd.join('').length;
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
- },
-
- /**
- * @private
- */
- _updateTextarea: function() {
- this.cursorOffsetCache = { };
- if (!this.hiddenTextarea) {
- return;
- }
- if (!this.inCompositionMode) {
- var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text);
- this.hiddenTextarea.selectionStart = newSelection.selectionStart;
- this.hiddenTextarea.selectionEnd = newSelection.selectionEnd;
- }
- this.updateTextareaPosition();
- },
-
- /**
- * @private
- */
- updateFromTextArea: function() {
- if (!this.hiddenTextarea) {
- return;
- }
- this.cursorOffsetCache = { };
- this.text = this.hiddenTextarea.value;
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- var newSelection = this.fromStringToGraphemeSelection(
- this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value);
- this.selectionEnd = this.selectionStart = newSelection.selectionEnd;
- if (!this.inCompositionMode) {
- this.selectionStart = newSelection.selectionStart;
- }
- this.updateTextareaPosition();
- },
-
- /**
- * @private
- */
- updateTextareaPosition: function() {
- if (this.selectionStart === this.selectionEnd) {
- var style = this._calcTextareaPosition();
- this.hiddenTextarea.style.left = style.left;
- this.hiddenTextarea.style.top = style.top;
- }
- },
-
- /**
- * @private
- * @return {Object} style contains style for hiddenTextarea
- */
- _calcTextareaPosition: function() {
- if (!this.canvas) {
- return { x: 1, y: 1 };
- }
- var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart,
- boundaries = this._getCursorBoundaries(desiredPosition),
- cursorLocation = this.get2DCursorLocation(desiredPosition),
- lineIndex = cursorLocation.lineIndex,
- charIndex = cursorLocation.charIndex,
- charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight,
- leftOffset = boundaries.leftOffset,
- m = this.calcTransformMatrix(),
- p = {
- x: boundaries.left + leftOffset,
- y: boundaries.top + boundaries.topOffset + charHeight
- },
- retinaScaling = this.canvas.getRetinaScaling(),
- upperCanvas = this.canvas.upperCanvasEl,
- upperCanvasWidth = upperCanvas.width / retinaScaling,
- upperCanvasHeight = upperCanvas.height / retinaScaling,
- maxWidth = upperCanvasWidth - charHeight,
- maxHeight = upperCanvasHeight - charHeight,
- scaleX = upperCanvas.clientWidth / upperCanvasWidth,
- scaleY = upperCanvas.clientHeight / upperCanvasHeight;
-
- p = fabric.util.transformPoint(p, m);
- p = fabric.util.transformPoint(p, this.canvas.viewportTransform);
- p.x *= scaleX;
- p.y *= scaleY;
- if (p.x < 0) {
- p.x = 0;
- }
- if (p.x > maxWidth) {
- p.x = maxWidth;
- }
- if (p.y < 0) {
- p.y = 0;
- }
- if (p.y > maxHeight) {
- p.y = maxHeight;
- }
-
- // add canvas offset on document
- p.x += this.canvas._offset.left;
- p.y += this.canvas._offset.top;
-
- return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight };
- },
-
- /**
- * @private
- */
- _saveEditingProps: function() {
- this._savedProps = {
- hasControls: this.hasControls,
- borderColor: this.borderColor,
- lockMovementX: this.lockMovementX,
- lockMovementY: this.lockMovementY,
- hoverCursor: this.hoverCursor,
- selectable: this.selectable,
- defaultCursor: this.canvas && this.canvas.defaultCursor,
- moveCursor: this.canvas && this.canvas.moveCursor
- };
- },
-
- /**
- * @private
- */
- _restoreEditingProps: function() {
- if (!this._savedProps) {
- return;
- }
-
- this.hoverCursor = this._savedProps.hoverCursor;
- this.hasControls = this._savedProps.hasControls;
- this.borderColor = this._savedProps.borderColor;
- this.selectable = this._savedProps.selectable;
- this.lockMovementX = this._savedProps.lockMovementX;
- this.lockMovementY = this._savedProps.lockMovementY;
-
- if (this.canvas) {
- this.canvas.defaultCursor = this._savedProps.defaultCursor;
- this.canvas.moveCursor = this._savedProps.moveCursor;
- }
- },
-
- /**
- * Exits from editing state
- * @return {fabric.IText} thisArg
- * @chainable
- */
- exitEditing: function() {
- var isTextChanged = (this._textBeforeEdit !== this.text);
- this.selected = false;
- this.isEditing = false;
-
- this.selectionEnd = this.selectionStart;
-
- if (this.hiddenTextarea) {
- this.hiddenTextarea.blur && this.hiddenTextarea.blur();
- this.canvas && this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea);
- this.hiddenTextarea = null;
- }
-
- this.abortCursorAnimation();
- this._restoreEditingProps();
- this._currentCursorOpacity = 0;
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this.fire('editing:exited');
- isTextChanged && this.fire('modified');
- if (this.canvas) {
- this.canvas.off('mouse:move', this.mouseMoveHandler);
- this.canvas.fire('text:editing:exited', { target: this });
- isTextChanged && this.canvas.fire('object:modified', { target: this });
- }
- return this;
- },
-
- /**
- * @private
- */
- _removeExtraneousStyles: function() {
- for (var prop in this.styles) {
- if (!this._textLines[prop]) {
- delete this.styles[prop];
- }
- }
- },
-
- /**
- * remove and reflow a style block from start to end.
- * @param {Number} start linear start position for removal (included in removal)
- * @param {Number} end linear end position for removal ( excluded from removal )
- */
- removeStyleFromTo: function(start, end) {
- var cursorStart = this.get2DCursorLocation(start, true),
- cursorEnd = this.get2DCursorLocation(end, true),
- lineStart = cursorStart.lineIndex,
- charStart = cursorStart.charIndex,
- lineEnd = cursorEnd.lineIndex,
- charEnd = cursorEnd.charIndex,
- i, styleObj;
- if (lineStart !== lineEnd) {
- // step1 remove the trailing of lineStart
- if (this.styles[lineStart]) {
- for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) {
- delete this.styles[lineStart][i];
- }
- }
- // step2 move the trailing of lineEnd to lineStart if needed
- if (this.styles[lineEnd]) {
- for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) {
- styleObj = this.styles[lineEnd][i];
- if (styleObj) {
- this.styles[lineStart] || (this.styles[lineStart] = { });
- this.styles[lineStart][charStart + i - charEnd] = styleObj;
- }
- }
- }
- // step3 detects lines will be completely removed.
- for (i = lineStart + 1; i <= lineEnd; i++) {
- delete this.styles[i];
- }
- // step4 shift remaining lines.
- this.shiftLineStyles(lineEnd, lineStart - lineEnd);
- }
- else {
- // remove and shift left on the same line
- if (this.styles[lineStart]) {
- styleObj = this.styles[lineStart];
- var diff = charEnd - charStart, numericChar, _char;
- for (i = charStart; i < charEnd; i++) {
- delete styleObj[i];
- }
- for (_char in this.styles[lineStart]) {
- numericChar = parseInt(_char, 10);
- if (numericChar >= charEnd) {
- styleObj[numericChar - diff] = styleObj[_char];
- delete styleObj[_char];
- }
- }
- }
- }
- },
-
- /**
- * Shifts line styles up or down
- * @param {Number} lineIndex Index of a line
- * @param {Number} offset Can any number?
- */
- shiftLineStyles: function(lineIndex, offset) {
- // shift all line styles by offset upward or downward
- // do not clone deep. we need new array, not new style objects
- var clonedStyles = clone(this.styles);
- for (var line in this.styles) {
- var numericLine = parseInt(line, 10);
- if (numericLine > lineIndex) {
- this.styles[numericLine + offset] = clonedStyles[numericLine];
- if (!clonedStyles[numericLine - offset]) {
- delete this.styles[numericLine];
- }
- }
- }
- },
-
- restartCursorIfNeeded: function() {
- if (!this._currentTickState || this._currentTickState.isAborted
- || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted
- ) {
- this.initDelayedCursor();
- }
- },
-
- /**
- * Inserts new style object
- * @param {Number} lineIndex Index of a line
- * @param {Number} charIndex Index of a char
- * @param {Number} qty number of lines to add
- * @param {Array} copiedStyle Array of objects styles
- */
- insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) {
- var currentCharStyle,
- newLineStyles = {},
- somethingAdded = false;
-
- qty || (qty = 1);
- this.shiftLineStyles(lineIndex, qty);
- if (this.styles[lineIndex]) {
- currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1];
- }
-
- // we clone styles of all chars
- // after cursor onto the current line
- for (var index in this.styles[lineIndex]) {
- var numIndex = parseInt(index, 10);
- if (numIndex >= charIndex) {
- somethingAdded = true;
- newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];
- // remove lines from the previous line since they're on a new line now
- delete this.styles[lineIndex][index];
- }
- }
- if (somethingAdded) {
- this.styles[lineIndex + qty] = newLineStyles;
- }
- else {
- delete this.styles[lineIndex + qty];
- }
- // for the other lines
- // we clone current char style onto the next (otherwise empty) line
- while (qty > 1) {
- qty--;
- if (copiedStyle && copiedStyle[qty]) {
- this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty]) };
- }
- else if (currentCharStyle) {
- this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) };
- }
- else {
- delete this.styles[lineIndex + qty];
- }
- }
- this._forceClearCache = true;
- },
-
- /**
- * Inserts style object for a given line/char index
- * @param {Number} lineIndex Index of a line
- * @param {Number} charIndex Index of a char
- * @param {Number} quantity number Style object to insert, if given
- * @param {Array} copiedStyle array of style objects
- */
- insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) {
- if (!this.styles) {
- this.styles = {};
- }
- var currentLineStyles = this.styles[lineIndex],
- currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {};
-
- quantity || (quantity = 1);
- // shift all char styles by quantity forward
- // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4
- for (var index in currentLineStylesCloned) {
- var numericIndex = parseInt(index, 10);
- if (numericIndex >= charIndex) {
- currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex];
- // only delete the style if there was nothing moved there
- if (!currentLineStylesCloned[numericIndex - quantity]) {
- delete currentLineStyles[numericIndex];
- }
- }
- }
- this._forceClearCache = true;
- if (copiedStyle) {
- while (quantity--) {
- if (!Object.keys(copiedStyle[quantity]).length) {
- continue;
- }
- if (!this.styles[lineIndex]) {
- this.styles[lineIndex] = {};
- }
- this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]);
- }
- return;
- }
- if (!currentLineStyles) {
- return;
- }
- var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1];
- while (newStyle && quantity--) {
- this.styles[lineIndex][charIndex + quantity] = clone(newStyle);
- }
- },
-
- /**
- * Inserts style object(s)
- * @param {Array} insertedText Characters at the location where style is inserted
- * @param {Number} start cursor index for inserting style
- * @param {Array} [copiedStyle] array of style objects to insert.
- */
- insertNewStyleBlock: function(insertedText, start, copiedStyle) {
- var cursorLoc = this.get2DCursorLocation(start, true),
- addedLines = [0], linesLength = 0;
- for (var i = 0; i < insertedText.length; i++) {
- if (insertedText[i] === '\n') {
- linesLength++;
- addedLines[linesLength] = 0;
- }
- else {
- addedLines[linesLength]++;
- }
- }
- if (addedLines[0] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle);
- copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1);
- }
- linesLength && this.insertNewlineStyleObject(
- cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLength);
- for (var i = 1; i < linesLength; i++) {
- if (addedLines[i] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
- }
- else if (copiedStyle) {
- this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0];
- }
- copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1);
- }
- // we use i outside the loop to get it like linesLength
- if (addedLines[i] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
- }
- },
-
- /**
- * Set the selectionStart and selectionEnd according to the new position of cursor
- * mimic the key - mouse navigation when shift is pressed.
- */
- setSelectionStartEndWithShift: function(start, end, newSelection) {
- if (newSelection <= start) {
- if (end === start) {
- this._selectionDirection = 'left';
- }
- else if (this._selectionDirection === 'right') {
- this._selectionDirection = 'left';
- this.selectionEnd = start;
- }
- this.selectionStart = newSelection;
- }
- else if (newSelection > start && newSelection < end) {
- if (this._selectionDirection === 'right') {
- this.selectionEnd = newSelection;
- }
- else {
- this.selectionStart = newSelection;
- }
- }
- else {
- // newSelection is > selection start and end
- if (end === start) {
- this._selectionDirection = 'right';
- }
- else if (this._selectionDirection === 'left') {
- this._selectionDirection = 'right';
- this.selectionStart = end;
- }
- this.selectionEnd = newSelection;
- }
- },
-
- setSelectionInBoundaries: function() {
- var length = this.text.length;
- if (this.selectionStart > length) {
- this.selectionStart = length;
- }
- else if (this.selectionStart < 0) {
- this.selectionStart = 0;
- }
- if (this.selectionEnd > length) {
- this.selectionEnd = length;
- }
- else if (this.selectionEnd < 0) {
- this.selectionEnd = 0;
- }
- }
- });
-})();
-
-
-fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
- /**
- * Initializes "dbclick" event handler
- */
- initDoubleClickSimulation: function() {
-
- // for double click
- this.__lastClickTime = +new Date();
-
- // for triple click
- this.__lastLastClickTime = +new Date();
-
- this.__lastPointer = { };
-
- this.on('mousedown', this.onMouseDown);
- },
-
- /**
- * Default event handler to simulate triple click
- * @private
- */
- onMouseDown: function(options) {
- if (!this.canvas) {
- return;
- }
- this.__newClickTime = +new Date();
- var newPointer = options.pointer;
- if (this.isTripleClick(newPointer)) {
- this.fire('tripleclick', options);
- this._stopEvent(options.e);
- }
- this.__lastLastClickTime = this.__lastClickTime;
- this.__lastClickTime = this.__newClickTime;
- this.__lastPointer = newPointer;
- this.__lastIsEditing = this.isEditing;
- this.__lastSelected = this.selected;
- },
-
- isTripleClick: function(newPointer) {
- return this.__newClickTime - this.__lastClickTime < 500 &&
- this.__lastClickTime - this.__lastLastClickTime < 500 &&
- this.__lastPointer.x === newPointer.x &&
- this.__lastPointer.y === newPointer.y;
- },
-
- /**
- * @private
- */
- _stopEvent: function(e) {
- e.preventDefault && e.preventDefault();
- e.stopPropagation && e.stopPropagation();
- },
-
- /**
- * Initializes event handlers related to cursor or selection
- */
- initCursorSelectionHandlers: function() {
- this.initMousedownHandler();
- this.initMouseupHandler();
- this.initClicks();
- },
-
- /**
- * Default handler for double click, select a word
- */
- doubleClickHandler: function(options) {
- if (!this.isEditing) {
- return;
- }
- this.selectWord(this.getSelectionStartFromPointer(options.e));
- },
-
- /**
- * Default handler for triple click, select a line
- */
- tripleClickHandler: function(options) {
- if (!this.isEditing) {
- return;
- }
- this.selectLine(this.getSelectionStartFromPointer(options.e));
- },
-
- /**
- * Initializes double and triple click event handlers
- */
- initClicks: function() {
- this.on('mousedblclick', this.doubleClickHandler);
- this.on('tripleclick', this.tripleClickHandler);
- },
-
- /**
- * Default event handler for the basic functionalities needed on _mouseDown
- * can be overridden to do something different.
- * Scope of this implementation is: find the click position, set selectionStart
- * find selectionEnd, initialize the drawing of either cursor or selection area
- */
- _mouseDownHandler: function(options) {
- if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) {
- return;
- }
-
- this.__isMousedown = true;
-
- if (this.selected) {
- this.setCursorByClick(options.e);
- }
-
- if (this.isEditing) {
- this.__selectionStartOnMouseDown = this.selectionStart;
- if (this.selectionStart === this.selectionEnd) {
- this.abortCursorAnimation();
- }
- this.renderCursorOrSelection();
- }
- },
-
- /**
- * Default event handler for the basic functionalities needed on mousedown:before
- * can be overridden to do something different.
- * Scope of this implementation is: verify the object is already selected when mousing down
- */
- _mouseDownHandlerBefore: function(options) {
- if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) {
- return;
- }
- // we want to avoid that an object that was selected and then becomes unselectable,
- // may trigger editing mode in some way.
- this.selected = this === this.canvas._activeObject;
- },
-
- /**
- * Initializes "mousedown" event handler
- */
- initMousedownHandler: function() {
- this.on('mousedown', this._mouseDownHandler);
- this.on('mousedown:before', this._mouseDownHandlerBefore);
- },
-
- /**
- * Initializes "mouseup" event handler
- */
- initMouseupHandler: function() {
- this.on('mouseup', this.mouseUpHandler);
- },
-
- /**
- * standard hander for mouse up, overridable
- * @private
- */
- mouseUpHandler: function(options) {
- this.__isMousedown = false;
- if (!this.editable || this.group ||
- (options.transform && options.transform.actionPerformed) ||
- (options.e.button && options.e.button !== 1)) {
- return;
- }
-
- if (this.canvas) {
- var currentActive = this.canvas._activeObject;
- if (currentActive && currentActive !== this) {
- // avoid running this logic when there is an active object
- // this because is possible with shift click and fast clicks,
- // to rapidly deselect and reselect this object and trigger an enterEdit
- return;
- }
- }
-
- if (this.__lastSelected && !this.__corner) {
- this.selected = false;
- this.__lastSelected = false;
- this.enterEditing(options.e);
- if (this.selectionStart === this.selectionEnd) {
- this.initDelayedCursor(true);
- }
- else {
- this.renderCursorOrSelection();
- }
- }
- else {
- this.selected = true;
- }
- },
-
- /**
- * Changes cursor location in a text depending on passed pointer (x/y) object
- * @param {Event} e Event object
- */
- setCursorByClick: function(e) {
- var newSelection = this.getSelectionStartFromPointer(e),
- start = this.selectionStart, end = this.selectionEnd;
- if (e.shiftKey) {
- this.setSelectionStartEndWithShift(start, end, newSelection);
- }
- else {
- this.selectionStart = newSelection;
- this.selectionEnd = newSelection;
- }
- if (this.isEditing) {
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Returns index of a character corresponding to where an object was clicked
- * @param {Event} e Event object
- * @return {Number} Index of a character
- */
- getSelectionStartFromPointer: function(e) {
- var mouseOffset = this.getLocalPointer(e),
- prevWidth = 0,
- width = 0,
- height = 0,
- charIndex = 0,
- lineIndex = 0,
- lineLeftOffset,
- line;
-
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- if (height <= mouseOffset.y) {
- height += this.getHeightOfLine(i) * this.scaleY;
- lineIndex = i;
- if (i > 0) {
- charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1);
- }
- }
- else {
- break;
- }
- }
- lineLeftOffset = this._getLineLeftOffset(lineIndex);
- width = lineLeftOffset * this.scaleX;
- line = this._textLines[lineIndex];
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- prevWidth = width;
- // i removed something about flipX here, check.
- width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX;
- if (width <= mouseOffset.x) {
- charIndex++;
- }
- else {
- break;
- }
- }
- return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen);
- },
-
- /**
- * @private
- */
- _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
- // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0
- var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
- distanceBtwNextCharAndCursor = width - mouseOffset.x,
- offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ||
- distanceBtwNextCharAndCursor < 0 ? 0 : 1,
- newSelectionStart = index + offset;
- // if object is horizontally flipped, mirror cursor location from the end
- if (this.flipX) {
- newSelectionStart = jlen - newSelectionStart;
- }
-
- if (newSelectionStart > this._text.length) {
- newSelectionStart = this._text.length;
- }
-
- return newSelectionStart;
- }
-});
-
-
-fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
-
- /**
- * Initializes hidden textarea (needed to bring up keyboard in iOS)
- */
- initHiddenTextarea: function() {
- this.hiddenTextarea = fabric.document.createElement('textarea');
- this.hiddenTextarea.setAttribute('autocapitalize', 'off');
- this.hiddenTextarea.setAttribute('autocorrect', 'off');
- this.hiddenTextarea.setAttribute('autocomplete', 'off');
- this.hiddenTextarea.setAttribute('spellcheck', 'false');
- this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', '');
- this.hiddenTextarea.setAttribute('wrap', 'off');
- var style = this._calcTextareaPosition();
- // line-height: 1px; was removed from the style to fix this:
- // https://bugs.chromium.org/p/chromium/issues/detail?id=870966
- this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top +
- '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' +
- ' paddingーtop: ' + style.fontSize + ';';
- fabric.document.body.appendChild(this.hiddenTextarea);
-
- fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this));
-
- if (!this._clickHandlerInitialized && this.canvas) {
- fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this));
- this._clickHandlerInitialized = true;
- }
- },
-
- /**
- * For functionalities on keyDown
- * Map a special key to a function of the instance/prototype
- * If you need different behaviour for ESC or TAB or arrows, you have to change
- * this map setting the name of a function that you build on the fabric.Itext or
- * your prototype.
- * the map change will affect all Instances unless you need for only some text Instances
- * in that case you have to clone this object and assign your Instance.
- * this.keysMap = fabric.util.object.clone(this.keysMap);
- * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0]
- */
- keysMap: {
- 9: 'exitEditing',
- 27: 'exitEditing',
- 33: 'moveCursorUp',
- 34: 'moveCursorDown',
- 35: 'moveCursorRight',
- 36: 'moveCursorLeft',
- 37: 'moveCursorLeft',
- 38: 'moveCursorUp',
- 39: 'moveCursorRight',
- 40: 'moveCursorDown',
- },
-
- /**
- * For functionalities on keyUp + ctrl || cmd
- */
- ctrlKeysMapUp: {
- 67: 'copy',
- 88: 'cut'
- },
-
- /**
- * For functionalities on keyDown + ctrl || cmd
- */
- ctrlKeysMapDown: {
- 65: 'selectAll'
- },
-
- onClick: function() {
- // No need to trigger click event here, focus is enough to have the keyboard appear on Android
- this.hiddenTextarea && this.hiddenTextarea.focus();
- },
-
- /**
- * Handles keyup event
- * @param {Event} e Event object
- */
- onKeyDown: function(e) {
- if (!this.isEditing || this.inCompositionMode) {
- return;
- }
- if (e.keyCode in this.keysMap) {
- this[this.keysMap[e.keyCode]](e);
- }
- else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {
- this[this.ctrlKeysMapDown[e.keyCode]](e);
- }
- else {
- return;
- }
- e.stopImmediatePropagation();
- e.preventDefault();
- if (e.keyCode >= 33 && e.keyCode <= 40) {
- // if i press an arrow key just update selection
- this.clearContextTop();
- this.renderCursorOrSelection();
- }
- else {
- this.canvas && this.canvas.requestRenderAll();
- }
- },
-
- /**
- * Handles keyup event
- * We handle KeyUp because ie11 and edge have difficulties copy/pasting
- * if a copy/cut event fired, keyup is dismissed
- * @param {Event} e Event object
- */
- onKeyUp: function(e) {
- if (!this.isEditing || this._copyDone || this.inCompositionMode) {
- this._copyDone = false;
- return;
- }
- if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {
- this[this.ctrlKeysMapUp[e.keyCode]](e);
- }
- else {
- return;
- }
- e.stopImmediatePropagation();
- e.preventDefault();
- this.canvas && this.canvas.requestRenderAll();
- },
-
- /**
- * Handles onInput event
- * @param {Event} e Event object
- */
- onInput: function(e) {
- var fromPaste = this.fromPaste;
- this.fromPaste = false;
- e && e.stopPropagation();
- if (!this.isEditing) {
- return;
- }
- // decisions about style changes.
- var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,
- charCount = this._text.length,
- nextCharCount = nextText.length,
- removedText, insertedText,
- charDiff = nextCharCount - charCount;
- if (this.hiddenTextarea.value === '') {
- this.styles = { };
- this.updateFromTextArea();
- this.fire('changed');
- if (this.canvas) {
- this.canvas.fire('text:changed', { target: this });
- this.canvas.requestRenderAll();
- }
- return;
- }
-
- var textareaSelection = this.fromStringToGraphemeSelection(
- this.hiddenTextarea.selectionStart,
- this.hiddenTextarea.selectionEnd,
- this.hiddenTextarea.value
- );
- var backDelete = this.selectionStart > textareaSelection.selectionStart;
-
- if (this.selectionStart !== this.selectionEnd) {
- removedText = this._text.slice(this.selectionStart, this.selectionEnd);
- charDiff += this.selectionEnd - this.selectionStart;
- }
- else if (nextCharCount < charCount) {
- if (backDelete) {
- removedText = this._text.slice(this.selectionEnd + charDiff, this.selectionEnd);
- }
- else {
- removedText = this._text.slice(this.selectionStart, this.selectionStart - charDiff);
- }
- }
- insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd);
- if (removedText && removedText.length) {
- if (this.selectionStart !== this.selectionEnd) {
- this.removeStyleFromTo(this.selectionStart, this.selectionEnd);
- }
- else if (backDelete) {
- // detect differencies between forwardDelete and backDelete
- this.removeStyleFromTo(this.selectionEnd - removedText.length, this.selectionEnd);
- }
- else {
- this.removeStyleFromTo(this.selectionEnd, this.selectionEnd + removedText.length);
- }
- }
- if (insertedText.length) {
- if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) {
- this.insertNewStyleBlock(insertedText, this.selectionStart, fabric.copiedTextStyle);
- }
- else {
- this.insertNewStyleBlock(insertedText, this.selectionStart);
- }
- }
- this.updateFromTextArea();
- this.fire('changed');
- if (this.canvas) {
- this.canvas.fire('text:changed', { target: this });
- this.canvas.requestRenderAll();
- }
- },
- /**
- * Composition start
- */
- onCompositionStart: function() {
- this.inCompositionMode = true;
- },
-
- /**
- * Composition end
- */
- onCompositionEnd: function() {
- this.inCompositionMode = false;
- },
-
- // /**
- // * Composition update
- // */
- onCompositionUpdate: function(e) {
- this.compositionStart = e.target.selectionStart;
- this.compositionEnd = e.target.selectionEnd;
- this.updateTextareaPosition();
- },
-
- /**
- * Copies selected text
- * @param {Event} e Event object
- */
- copy: function() {
- if (this.selectionStart === this.selectionEnd) {
- //do not cut-copy if no selection
- return;
- }
-
- fabric.copiedText = this.getSelectedText();
- if (!fabric.disableStyleCopyPaste) {
- fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true);
- }
- else {
- fabric.copiedTextStyle = null;
- }
- this._copyDone = true;
- },
-
- /**
- * Pastes text
- * @param {Event} e Event object
- */
- paste: function() {
- this.fromPaste = true;
- },
-
- /**
- * @private
- * @param {Event} e Event object
- * @return {Object} Clipboard data object
- */
- _getClipboardData: function(e) {
- return (e && e.clipboardData) || fabric.window.clipboardData;
- },
-
- /**
- * Finds the width in pixels before the cursor on the same line
- * @private
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @return {Number} widthBeforeCursor width before cursor
- */
- _getWidthBeforeCursor: function(lineIndex, charIndex) {
- var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound;
-
- if (charIndex > 0) {
- bound = this.__charBounds[lineIndex][charIndex - 1];
- widthBeforeCursor += bound.left + bound.width;
- }
- return widthBeforeCursor;
- },
-
- /**
- * Gets start offset of a selection
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- getDownCursorOffset: function(e, isRight) {
- var selectionProp = this._getSelectionForOffset(e, isRight),
- cursorLocation = this.get2DCursorLocation(selectionProp),
- lineIndex = cursorLocation.lineIndex;
- // if on last line, down cursor goes to end of line
- if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {
- // move to the end of a text
- return this._text.length - selectionProp;
- }
- var charIndex = cursorLocation.charIndex,
- widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
- indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor),
- textAfterCursor = this._textLines[lineIndex].slice(charIndex);
- return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex);
- },
-
- /**
- * private
- * Helps finding if the offset should be counted from Start or End
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- _getSelectionForOffset: function(e, isRight) {
- if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) {
- return this.selectionEnd;
- }
- else {
- return this.selectionStart;
- }
- },
-
- /**
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- getUpCursorOffset: function(e, isRight) {
- var selectionProp = this._getSelectionForOffset(e, isRight),
- cursorLocation = this.get2DCursorLocation(selectionProp),
- lineIndex = cursorLocation.lineIndex;
- if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {
- // if on first line, up cursor goes to start of line
- return -selectionProp;
- }
- var charIndex = cursorLocation.charIndex,
- widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
- indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor),
- textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex),
- missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1);
- // return a negative offset
- return -this._textLines[lineIndex - 1].length
- + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset);
- },
-
- /**
- * for a given width it founds the matching character.
- * @private
- */
- _getIndexOnLine: function(lineIndex, width) {
-
- var line = this._textLines[lineIndex],
- lineLeftOffset = this._getLineLeftOffset(lineIndex),
- widthOfCharsOnLine = lineLeftOffset,
- indexOnLine = 0, charWidth, foundMatch;
-
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charWidth = this.__charBounds[lineIndex][j].width;
- widthOfCharsOnLine += charWidth;
- if (widthOfCharsOnLine > width) {
- foundMatch = true;
- var leftEdge = widthOfCharsOnLine - charWidth,
- rightEdge = widthOfCharsOnLine,
- offsetFromLeftEdge = Math.abs(leftEdge - width),
- offsetFromRightEdge = Math.abs(rightEdge - width);
-
- indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
- break;
- }
- }
-
- // reached end
- if (!foundMatch) {
- indexOnLine = line.length - 1;
- }
-
- return indexOnLine;
- },
-
-
- /**
- * Moves cursor down
- * @param {Event} e Event object
- */
- moveCursorDown: function(e) {
- if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
- return;
- }
- this._moveCursorUpOrDown('Down', e);
- },
-
- /**
- * Moves cursor up
- * @param {Event} e Event object
- */
- moveCursorUp: function(e) {
- if (this.selectionStart === 0 && this.selectionEnd === 0) {
- return;
- }
- this._moveCursorUpOrDown('Up', e);
- },
-
- /**
- * Moves cursor up or down, fires the events
- * @param {String} direction 'Up' or 'Down'
- * @param {Event} e Event object
- */
- _moveCursorUpOrDown: function(direction, e) {
- // getUpCursorOffset
- // getDownCursorOffset
- var action = 'get' + direction + 'CursorOffset',
- offset = this[action](e, this._selectionDirection === 'right');
- if (e.shiftKey) {
- this.moveCursorWithShift(offset);
- }
- else {
- this.moveCursorWithoutShift(offset);
- }
- if (offset !== 0) {
- this.setSelectionInBoundaries();
- this.abortCursorAnimation();
- this._currentCursorOpacity = 1;
- this.initDelayedCursor();
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Moves cursor with shift
- * @param {Number} offset
- */
- moveCursorWithShift: function(offset) {
- var newSelection = this._selectionDirection === 'left'
- ? this.selectionStart + offset
- : this.selectionEnd + offset;
- this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection);
- return offset !== 0;
- },
-
- /**
- * Moves cursor up without shift
- * @param {Number} offset
- */
- moveCursorWithoutShift: function(offset) {
- if (offset < 0) {
- this.selectionStart += offset;
- this.selectionEnd = this.selectionStart;
- }
- else {
- this.selectionEnd += offset;
- this.selectionStart = this.selectionEnd;
- }
- return offset !== 0;
- },
-
- /**
- * Moves cursor left
- * @param {Event} e Event object
- */
- moveCursorLeft: function(e) {
- if (this.selectionStart === 0 && this.selectionEnd === 0) {
- return;
- }
- this._moveCursorLeftOrRight('Left', e);
- },
-
- /**
- * @private
- * @return {Boolean} true if a change happened
- */
- _move: function(e, prop, direction) {
- var newValue;
- if (e.altKey) {
- newValue = this['findWordBoundary' + direction](this[prop]);
- }
- else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) {
- newValue = this['findLineBoundary' + direction](this[prop]);
- }
- else {
- this[prop] += direction === 'Left' ? -1 : 1;
- return true;
- }
- if (typeof newValue !== undefined && this[prop] !== newValue) {
- this[prop] = newValue;
- return true;
- }
- },
-
- /**
- * @private
- */
- _moveLeft: function(e, prop) {
- return this._move(e, prop, 'Left');
- },
-
- /**
- * @private
- */
- _moveRight: function(e, prop) {
- return this._move(e, prop, 'Right');
- },
-
- /**
- * Moves cursor left without keeping selection
- * @param {Event} e
- */
- moveCursorLeftWithoutShift: function(e) {
- var change = true;
- this._selectionDirection = 'left';
-
- // only move cursor when there is no selection,
- // otherwise we discard it, and leave cursor on same place
- if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) {
- change = this._moveLeft(e, 'selectionStart');
-
- }
- this.selectionEnd = this.selectionStart;
- return change;
- },
-
- /**
- * Moves cursor left while keeping selection
- * @param {Event} e
- */
- moveCursorLeftWithShift: function(e) {
- if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) {
- return this._moveLeft(e, 'selectionEnd');
- }
- else if (this.selectionStart !== 0){
- this._selectionDirection = 'left';
- return this._moveLeft(e, 'selectionStart');
- }
- },
-
- /**
- * Moves cursor right
- * @param {Event} e Event object
- */
- moveCursorRight: function(e) {
- if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
- return;
- }
- this._moveCursorLeftOrRight('Right', e);
- },
-
- /**
- * Moves cursor right or Left, fires event
- * @param {String} direction 'Left', 'Right'
- * @param {Event} e Event object
- */
- _moveCursorLeftOrRight: function(direction, e) {
- var actionName = 'moveCursor' + direction + 'With';
- this._currentCursorOpacity = 1;
-
- if (e.shiftKey) {
- actionName += 'Shift';
- }
- else {
- actionName += 'outShift';
- }
- if (this[actionName](e)) {
- this.abortCursorAnimation();
- this.initDelayedCursor();
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Moves cursor right while keeping selection
- * @param {Event} e
- */
- moveCursorRightWithShift: function(e) {
- if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {
- return this._moveRight(e, 'selectionStart');
- }
- else if (this.selectionEnd !== this._text.length) {
- this._selectionDirection = 'right';
- return this._moveRight(e, 'selectionEnd');
- }
- },
-
- /**
- * Moves cursor right without keeping selection
- * @param {Event} e Event object
- */
- moveCursorRightWithoutShift: function(e) {
- var changed = true;
- this._selectionDirection = 'right';
-
- if (this.selectionStart === this.selectionEnd) {
- changed = this._moveRight(e, 'selectionStart');
- this.selectionEnd = this.selectionStart;
- }
- else {
- this.selectionStart = this.selectionEnd;
- }
- return changed;
- },
-
- /**
- * Removes characters from start/end
- * start/end ar per grapheme position in _text array.
- *
- * @param {Number} start
- * @param {Number} end default to start + 1
- */
- removeChars: function(start, end) {
- if (typeof end === 'undefined') {
- end = start + 1;
- }
- this.removeStyleFromTo(start, end);
- this._text.splice(start, end - start);
- this.text = this._text.join('');
- this.set('dirty', true);
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this._removeExtraneousStyles();
- },
-
- /**
- * insert characters at start position, before start position.
- * start equal 1 it means the text get inserted between actual grapheme 0 and 1
- * if style array is provided, it must be as the same length of text in graphemes
- * if end is provided and is bigger than start, old text is replaced.
- * start/end ar per grapheme position in _text array.
- *
- * @param {String} text text to insert
- * @param {Array} style array of style objects
- * @param {Number} start
- * @param {Number} end default to start + 1
- */
- insertChars: function(text, style, start, end) {
- if (typeof end === 'undefined') {
- end = start;
- }
- if (end > start) {
- this.removeStyleFromTo(start, end);
- }
- var graphemes = fabric.util.string.graphemeSplit(text);
- this.insertNewStyleBlock(graphemes, start, style);
- this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end));
- this.text = this._text.join('');
- this.set('dirty', true);
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this._removeExtraneousStyles();
- },
-
-});
-
-
-/* _TO_SVG_START_ */
-(function() {
- var toFixed = fabric.util.toFixed,
- multipleSpacesRegex = / +/g;
-
- fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
-
- /**
- * 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() {
- var offsets = this._getSVGLeftTopOffsets(),
- textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
- return this._wrapSVGTextAndBg(textAndBg);
- },
-
- /**
- * 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) {
- return this._createBaseSVGMarkup(
- this._toSVG(),
- { reviver: reviver, noStyle: true, withShadow: true }
- );
- },
-
- /**
- * @private
- */
- _getSVGLeftTopOffsets: function() {
- return {
- textLeft: -this.width / 2,
- textTop: -this.height / 2,
- lineTop: this.getHeightOfLine(0)
- };
- },
-
- /**
- * @private
- */
- _wrapSVGTextAndBg: function(textAndBg) {
- var noShadow = true,
- textDecoration = this.getSvgTextDecoration(this);
- return [
- textAndBg.textBgRects.join(''),
- '\t\t',
- textAndBg.textSpans.join(''),
- '\n'
- ];
- },
-
- /**
- * @private
- * @param {Number} textTopOffset Text top offset
- * @param {Number} textLeftOffset Text left offset
- * @return {Object}
- */
- _getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
- var textSpans = [],
- textBgRects = [],
- height = textTopOffset, lineOffset;
- // bounding-box background
- this._setSVGBg(textBgRects);
-
- // text and text-background
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- lineOffset = this._getLineLeftOffset(i);
- if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) {
- this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height);
- }
- this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height);
- height += this.getHeightOfLine(i);
- }
-
- return {
- textSpans: textSpans,
- textBgRects: textBgRects
- };
- },
-
- /**
- * @private
- */
- _createTextCharSpan: function(_char, styleDecl, left, top) {
- var shouldUseWhitespace = _char !== _char.trim() || _char.match(multipleSpacesRegex),
- styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace),
- fillStyles = styleProps ? 'style="' + styleProps + '"' : '',
- dy = styleDecl.deltaY, dySpan = '',
- NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
- if (dy) {
- dySpan = ' dy="' + toFixed(dy, NUM_FRACTION_DIGITS) + '" ';
- }
- return [
- '',
- fabric.util.string.escapeXml(_char),
- ''
- ].join('');
- },
-
- _setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) {
- // set proper line offset
- var lineHeight = this.getHeightOfLine(lineIndex),
- isJustify = this.textAlign.indexOf('justify') !== -1,
- actualStyle,
- nextStyle,
- charsToRender = '',
- charBox, style,
- boxWidth = 0,
- line = this._textLines[lineIndex],
- timeToRender;
-
- textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight;
- for (var i = 0, len = line.length - 1; i <= len; i++) {
- timeToRender = i === len || this.charSpacing;
- charsToRender += line[i];
- charBox = this.__charBounds[lineIndex][i];
- if (boxWidth === 0) {
- textLeftOffset += 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._hasStyleChangedForSvg(actualStyle, nextStyle);
- }
- if (timeToRender) {
- style = this._getStyleDeclaration(lineIndex, i) || { };
- textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset));
- charsToRender = '';
- actualStyle = nextStyle;
- textLeftOffset += boxWidth;
- boxWidth = 0;
- }
- }
- },
-
- _pushTextBgRect: function(textBgRects, color, left, top, width, height) {
- var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
- textBgRects.push(
- '\t\t\n');
- },
-
- _setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) {
- var line = this._textLines[i],
- heightOfLine = this.getHeightOfLine(i) / this.lineHeight,
- boxWidth = 0,
- boxStart = 0,
- charBox, currentColor,
- 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 (currentColor !== lastColor) {
- lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart,
- textTopOffset, boxWidth, heightOfLine);
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastColor = currentColor;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart,
- textTopOffset, boxWidth, heightOfLine);
- },
-
- /**
- * 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 {*} 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() + '"';
- },
-
- /**
- * @private
- */
- _getSVGLineTopOffset: function(lineIndex) {
- var lineTopOffset = 0, lastHeight = 0;
- for (var j = 0; j < lineIndex; j++) {
- lineTopOffset += this.getHeightOfLine(j);
- }
- lastHeight = this.getHeightOfLine(j);
- return {
- lineTop: lineTopOffset,
- offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)
- };
- },
-
- /**
- * Returns styles-string for svg-export
- * @param {Boolean} skipShadow a boolean to skip shadow filter output
- * @return {String}
- */
- getSvgStyles: function(skipShadow) {
- var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow);
- return svgStyle + ' white-space: pre;';
- },
- });
-})();
-/* _TO_SVG_END_ */
-
-
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = {});
-
- /**
- * Textbox class, based on IText, allows the user to resize the text rectangle
- * and wraps lines automatically. Textboxes have their Y scaling locked, the
- * user can only change width. Height is adjusted automatically based on the
- * wrapping of lines.
- * @class fabric.Textbox
- * @extends fabric.IText
- * @mixes fabric.Observable
- * @return {fabric.Textbox} thisArg
- * @see {@link fabric.Textbox#initialize} for constructor definition
- */
- fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {
-
- /**
- * Type of an object
- * @type String
- * @default
- */
- type: 'textbox',
-
- /**
- * Minimum width of textbox, in pixels.
- * @type Number
- * @default
- */
- minWidth: 20,
-
- /**
- * Minimum calculated width of a textbox, in pixels.
- * fixed to 2 so that an empty textbox cannot go to 0
- * and is still selectable without text.
- * @type Number
- * @default
- */
- dynamicMinWidth: 2,
-
- /**
- * Cached array of text wrapping.
- * @type Array
- */
- __cachedLines: null,
-
- /**
- * Override standard Object class values
- */
- lockScalingFlip: true,
-
- /**
- * Override standard Object class values
- * Textbox needs this on false
- */
- noScaleCache: false,
-
- /**
- * Properties which when set cause object to change dimensions
- * @type Object
- * @private
- */
- _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'),
-
- /**
- * Use this regular expression to split strings in breakable lines
- * @private
- */
- _wordJoiners: /[ \t\r]/,
-
- /**
- * Use this boolean property in order to split strings that have no white space concept.
- * this is a cheap way to help with chinese/japaense
- * @type Boolean
- * @since 2.6.0
- */
- splitByGrapheme: false,
-
- /**
- * Unlike superclass's version of this function, Textbox does not update
- * its width.
- * @private
- * @override
- */
- initDimensions: function() {
- if (this.__skipDimension) {
- return;
- }
- this.isEditing && this.initDelayedCursor();
- this.clearContextTop();
- this._clearCache();
- // clear dynamicMinWidth as it will be different after we re-wrap line
- this.dynamicMinWidth = 0;
- // wrap lines
- this._styleMap = this._generateStyleMap(this._splitText());
- // 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);
- }
- if (this.textAlign.indexOf('justify') !== -1) {
- // once text is measured we need to make space fatter to make justified text.
- this.enlargeSpaces();
- }
- // clear cache and re-calculate height
- this.height = this.calcTextHeight();
- this.saveState({ propertySet: '_dimensionAffectingProps' });
- },
-
- /**
- * Generate an object that translates the style object so that it is
- * broken up by visual lines (new lines and automatic wrapping).
- * The original text styles object is broken up by actual lines (new lines only),
- * which is only sufficient for Text / IText
- * @private
- */
- _generateStyleMap: function(textInfo) {
- var realLineCount = 0,
- realLineCharCount = 0,
- charCount = 0,
- map = {};
-
- for (var i = 0; i < textInfo.graphemeLines.length; i++) {
- if (textInfo.graphemeText[charCount] === '\n' && i > 0) {
- realLineCharCount = 0;
- charCount++;
- realLineCount++;
- }
- else if (!this.splitByGrapheme && this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) {
- // this case deals with space's that are removed from end of lines when wrapping
- realLineCharCount++;
- charCount++;
- }
-
- map[i] = { line: realLineCount, offset: realLineCharCount };
-
- charCount += textInfo.graphemeLines[i].length;
- realLineCharCount += textInfo.graphemeLines[i].length;
- }
-
- return map;
- },
-
- /**
- * Returns true if object has a style property or has it on a specified line
- * @param {Number} lineIndex
- * @return {Boolean}
- */
- styleHas: function(property, lineIndex) {
- if (this._styleMap && !this.isWrapping) {
- var map = this._styleMap[lineIndex];
- if (map) {
- lineIndex = map.line;
- }
- }
- return fabric.Text.prototype.styleHas.call(this, property, lineIndex);
- },
-
- /**
- * Returns true if object has no styling or no styling in a line
- * @param {Number} lineIndex , lineIndex is on wrapped lines.
- * @return {Boolean}
- */
- isEmptyStyles: function(lineIndex) {
- if (!this.styles) {
- return true;
- }
- var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false,
- map = this._styleMap[lineIndex], mapNextLine = this._styleMap[lineIndex + 1];
- if (map) {
- lineIndex = map.line;
- offset = map.offset;
- }
- if (mapNextLine) {
- nextLineIndex = mapNextLine.line;
- shouldLimit = nextLineIndex === lineIndex;
- nextOffset = mapNextLine.offset;
- }
- obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
- for (var p1 in obj) {
- for (var p2 in obj[p1]) {
- if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) {
- // eslint-disable-next-line no-unused-vars
- for (var p3 in obj[p1][p2]) {
- return false;
- }
- }
- }
- }
- return true;
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _getStyleDeclaration: function(lineIndex, charIndex) {
- if (this._styleMap && !this.isWrapping) {
- var map = this._styleMap[lineIndex];
- if (!map) {
- return null;
- }
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
- }
- return this.callSuper('_getStyleDeclaration', lineIndex, charIndex);
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} style
- * @private
- */
- _setStyleDeclaration: function(lineIndex, charIndex, style) {
- var map = this._styleMap[lineIndex];
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
-
- this.styles[lineIndex][charIndex] = style;
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _deleteStyleDeclaration: function(lineIndex, charIndex) {
- var map = this._styleMap[lineIndex];
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
- delete this.styles[lineIndex][charIndex];
- },
-
- /**
- * probably broken need a fix
- * Returns the real style line that correspond to the wrapped lineIndex line
- * Used just to verify if the line does exist or not.
- * @param {Number} lineIndex
- * @returns {Boolean} if the line exists or not
- * @private
- */
- _getLineStyle: function(lineIndex) {
- var map = this._styleMap[lineIndex];
- return !!this.styles[map.line];
- },
-
- /**
- * Set the line style to an empty object so that is initialized
- * @param {Number} lineIndex
- * @param {Object} style
- * @private
- */
- _setLineStyle: function(lineIndex) {
- var map = this._styleMap[lineIndex];
- this.styles[map.line] = {};
- },
-
- /**
- * Wraps text using the 'width' property of Textbox. First this function
- * splits text on newlines, so we preserve newlines entered by the user.
- * Then it wraps each line using the width of the Textbox by calling
- * _wrapLine().
- * @param {Array} lines The string array of text that is split into lines
- * @param {Number} desiredWidth width you want to wrap to
- * @returns {Array} Array of lines
- */
- _wrapText: function(lines, desiredWidth) {
- var wrapped = [], i;
- this.isWrapping = true;
- for (i = 0; i < lines.length; i++) {
- wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth));
- }
- this.isWrapping = false;
- return wrapped;
- },
-
- /**
- * Helper function to measure a string of text, given its lineIndex and charIndex offset
- * it gets called when charBounds are not available yet.
- * @param {CanvasRenderingContext2D} ctx
- * @param {String} text
- * @param {number} lineIndex
- * @param {number} charOffset
- * @returns {number}
- * @private
- */
- _measureWord: function(word, lineIndex, charOffset) {
- var width = 0, prevGrapheme, skipLeft = true;
- charOffset = charOffset || 0;
- for (var i = 0, len = word.length; i < len; i++) {
- var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft);
- width += box.kernedWidth;
- prevGrapheme = word[i];
- }
- return width;
- },
-
- /**
- * Wraps a line of text using the width of the Textbox and a context.
- * @param {Array} line The grapheme array that represent the line
- * @param {Number} lineIndex
- * @param {Number} desiredWidth width you want to wrap the line to
- * @param {Number} reservedSpace space to remove from wrapping for custom functionalities
- * @returns {Array} Array of line(s) into which the given text is wrapped
- * to.
- */
- _wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) {
- var lineWidth = 0,
- splitByGrapheme = this.splitByGrapheme,
- graphemeLines = [],
- line = [],
- // spaces in different languges?
- words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners),
- word = '',
- offset = 0,
- infix = splitByGrapheme ? '' : ' ',
- wordWidth = 0,
- infixWidth = 0,
- largestWordWidth = 0,
- lineJustStarted = true,
- additionalSpace = splitByGrapheme ? 0 : this._getWidthOfCharSpacing(),
- reservedSpace = reservedSpace || 0;
- // fix a difference between split and graphemeSplit
- if (words.length === 0) {
- words.push([]);
- }
- desiredWidth -= reservedSpace;
- for (var i = 0; i < words.length; i++) {
- // if using splitByGrapheme words are already in graphemes.
- word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]);
- wordWidth = this._measureWord(word, lineIndex, offset);
- offset += word.length;
-
- lineWidth += infixWidth + wordWidth - additionalSpace;
-
- if (lineWidth >= desiredWidth && !lineJustStarted) {
- graphemeLines.push(line);
- line = [];
- lineWidth = wordWidth;
- lineJustStarted = true;
- }
- else {
- lineWidth += additionalSpace;
- }
-
- if (!lineJustStarted && !splitByGrapheme) {
- line.push(infix);
- }
- line = line.concat(word);
-
- infixWidth = this._measureWord([infix], lineIndex, offset);
- offset++;
- lineJustStarted = false;
- // keep track of largest word
- if (wordWidth > largestWordWidth) {
- largestWordWidth = wordWidth;
- }
- }
-
- i && graphemeLines.push(line);
-
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
- this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
- }
-
- return graphemeLines;
- },
-
- /**
- * Detect if the text line is ended with an hard break
- * text and itext do not have wrapping, return false
- * @param {Number} lineIndex text to split
- * @return {Boolean}
- */
- isEndOfWrapping: function(lineIndex) {
- if (!this._styleMap[lineIndex + 1]) {
- // is last line, return true;
- return true;
- }
- if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) {
- // this is last line before a line break, return true;
- return true;
- }
- return false;
- },
-
- /**
- * Detect if a line has a linebreak and so we need to account for it when moving
- * and counting style.
- * @return Number
- */
- missingNewlineOffset: function(lineIndex) {
- if (this.splitByGrapheme) {
- return this.isEndOfWrapping(lineIndex) ? 1 : 0;
- }
- return 1;
- },
-
- /**
- * Gets lines of text to render in the Textbox. This function calculates
- * text wrapping on the fly every time it is called.
- * @param {String} text text to split
- * @returns {Array} Array of lines in the Textbox.
- * @override
- */
- _splitTextIntoLines: function(text) {
- var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text),
- graphemeLines = this._wrapText(newText.lines, this.width),
- lines = new Array(graphemeLines.length);
- for (var i = 0; i < graphemeLines.length; i++) {
- lines[i] = graphemeLines[i].join('');
- }
- newText.lines = lines;
- newText.graphemeLines = graphemeLines;
- return newText;
- },
-
- getMinWidth: function() {
- return Math.max(this.minWidth, this.dynamicMinWidth);
- },
-
- _removeExtraneousStyles: function() {
- var linesToKeep = {};
- for (var prop in this._styleMap) {
- if (this._textLines[prop]) {
- linesToKeep[this._styleMap[prop].line] = 1;
- }
- }
- for (var prop in this.styles) {
- if (!linesToKeep[prop]) {
- delete this.styles[prop];
- }
- }
- },
-
- /**
- * Returns object representation of an instance
- * @method toObject
- * @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) {
- return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude));
- }
- });
-
- /**
- * Returns fabric.Textbox instance from an object representation
- * @static
- * @memberOf fabric.Textbox
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created
- */
- fabric.Textbox.fromObject = function(object, callback) {
- return fabric.Object._fromObject('Textbox', object, callback, 'text');
- };
-})(typeof exports !== 'undefined' ? exports : this);
-
+/* build: `node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs` */
+/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */
+
+var fabric = fabric || { version: '3.6.0' };
+if (typeof exports !== 'undefined') {
+ exports.fabric = fabric;
+}
+/* _AMD_START_ */
+else if (typeof define === 'function' && define.amd) {
+ define([], function() { return fabric; });
+}
+/* _AMD_END_ */
+if (typeof document !== 'undefined' && typeof window !== 'undefined') {
+ if (document instanceof (typeof HTMLDocument !== 'undefined' ? HTMLDocument : Document)) {
+ fabric.document = document;
+ }
+ else {
+ fabric.document = document.implementation.createHTMLDocument('');
+ }
+ fabric.window = window;
+}
+else {
+ // assume we're running under node.js when document/window are not present
+ var jsdom = require('jsdom');
+ var virtualWindow = new jsdom.JSDOM(
+ decodeURIComponent('%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E'),
+ {
+ features: {
+ FetchExternalResources: ['img']
+ },
+ resources: 'usable'
+ }).window;
+ fabric.document = virtualWindow.document;
+ fabric.jsdomImplForWrapper = require('jsdom/lib/jsdom/living/generated/utils').implForWrapper;
+ fabric.nodeCanvas = require('jsdom/lib/jsdom/utils').Canvas;
+ fabric.window = virtualWindow;
+ DOMParser = fabric.window.DOMParser;
+}
+
+/**
+ * True when in environment that supports touch events
+ * @type boolean
+ */
+fabric.isTouchSupported = 'ontouchstart' in fabric.window || 'ontouchstart' in fabric.document ||
+ (fabric.window && fabric.window.navigator && fabric.window.navigator.maxTouchPoints > 0);
+
+/**
+ * True when in environment that's probably Node.js
+ * @type boolean
+ */
+fabric.isLikelyNode = typeof Buffer !== 'undefined' &&
+ typeof window === 'undefined';
+
+/* _FROM_SVG_START_ */
+/**
+ * Attributes parsed from all SVG elements
+ * @type array
+ */
+fabric.SHARED_ATTRIBUTES = [
+ 'display',
+ 'transform',
+ 'fill', 'fill-opacity', 'fill-rule',
+ 'opacity',
+ 'stroke', 'stroke-dasharray', 'stroke-linecap', 'stroke-dashoffset',
+ 'stroke-linejoin', 'stroke-miterlimit',
+ 'stroke-opacity', 'stroke-width',
+ 'id', 'paint-order', 'vector-effect',
+ 'instantiated_by_use', 'clip-path'
+];
+/* _FROM_SVG_END_ */
+
+/**
+ * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion.
+ */
+fabric.DPI = 96;
+fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)';
+fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig;
+fabric.reNonWord = /[ \n\.,;!\?\-]/;
+fabric.fontPaths = { };
+fabric.iMatrix = [1, 0, 0, 1, 0, 0];
+fabric.svgNS = 'http://www.w3.org/2000/svg';
+
+/**
+ * Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine.
+ * @since 1.7.14
+ * @type Number
+ * @default
+ */
+fabric.perfLimitSizeTotal = 2097152;
+
+/**
+ * Pixel limit for cache canvases width or height. IE fixes the maximum at 5000
+ * @since 1.7.14
+ * @type Number
+ * @default
+ */
+fabric.maxCacheSideLimit = 4096;
+
+/**
+ * Lowest pixel limit for cache canvases, set at 256PX
+ * @since 1.7.14
+ * @type Number
+ * @default
+ */
+fabric.minCacheSideLimit = 256;
+
+/**
+ * Cache Object for widths of chars in text rendering.
+ */
+fabric.charWidthsCache = { };
+
+/**
+ * if webgl is enabled and available, textureSize will determine the size
+ * of the canvas backend
+ * @since 2.0.0
+ * @type Number
+ * @default
+ */
+fabric.textureSize = 2048;
+
+/**
+ * When 'true', style information is not retained when copy/pasting text, making
+ * pasted text use destination style.
+ * Defaults to 'false'.
+ * @type Boolean
+ * @default
+ */
+fabric.disableStyleCopyPaste = false;
+
+/**
+ * Enable webgl for filtering picture is available
+ * A filtering backend will be initialized, this will both take memory and
+ * time since a default 2048x2048 canvas will be created for the gl context
+ * @since 2.0.0
+ * @type Boolean
+ * @default
+ */
+fabric.enableGLFiltering = true;
+
+/**
+ * Device Pixel Ratio
+ * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html
+ */
+fabric.devicePixelRatio = fabric.window.devicePixelRatio ||
+ fabric.window.webkitDevicePixelRatio ||
+ fabric.window.mozDevicePixelRatio ||
+ 1;
+/**
+ * Browser-specific constant to adjust CanvasRenderingContext2D.shadowBlur value,
+ * which is unitless and not rendered equally across browsers.
+ *
+ * Values that work quite well (as of October 2017) are:
+ * - Chrome: 1.5
+ * - Edge: 1.75
+ * - Firefox: 0.9
+ * - Safari: 0.95
+ *
+ * @since 2.0.0
+ * @type Number
+ * @default 1
+ */
+fabric.browserShadowBlurConstant = 1;
+
+/**
+ * This object contains the result of arc to beizer conversion for faster retrieving if the same arc needs to be converted again.
+ * It was an internal variable, is accessible since version 2.3.4
+ */
+fabric.arcToSegmentsCache = { };
+
+/**
+ * This object keeps the results of the boundsOfCurve calculation mapped by the joined arguments necessary to calculate it.
+ * It does speed up calculation, if you parse and add always the same paths, but in case of heavy usage of freedrawing
+ * you do not get any speed benefit and you get a big object in memory.
+ * The object was a private variable before, while now is appended to the lib so that you have access to it and you
+ * can eventually clear it.
+ * It was an internal variable, is accessible since version 2.3.4
+ */
+fabric.boundsOfCurveCache = { };
+
+/**
+ * If disabled boundsOfCurveCache is not used. For apps that make heavy usage of pencil drawing probably disabling it is better
+ * @default true
+ */
+fabric.cachesBoundsOfCurve = true;
+
+/**
+ * Skip performance testing of setupGLContext and force the use of putImageData that seems to be the one that works best on
+ * Chrome + old hardware. if your users are experiencing empty images after filtering you may try to force this to true
+ * this has to be set before instantiating the filtering backend ( before filtering the first image )
+ * @type Boolean
+ * @default false
+ */
+fabric.forceGLPutImageData = false;
+
+fabric.initFilterBackend = function() {
+ if (fabric.enableGLFiltering && fabric.isWebglSupported && fabric.isWebglSupported(fabric.textureSize)) {
+ console.log('max texture size: ' + fabric.maxTextureSize);
+ return (new fabric.WebglFilterBackend({ tileSize: fabric.textureSize }));
+ }
+ else if (fabric.Canvas2dFilterBackend) {
+ return (new fabric.Canvas2dFilterBackend());
+ }
+};
+
+
+if (typeof document !== 'undefined' && typeof window !== 'undefined') {
+ // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system)
+ window.fabric = fabric;
+}
+
+
+(function() {
+
+ /**
+ * @private
+ * @param {String} eventName
+ * @param {Function} handler
+ */
+ function _removeEventListener(eventName, handler) {
+ if (!this.__eventListeners[eventName]) {
+ return;
+ }
+ var eventListener = this.__eventListeners[eventName];
+ if (handler) {
+ eventListener[eventListener.indexOf(handler)] = false;
+ }
+ else {
+ fabric.util.array.fill(eventListener, false);
+ }
+ }
+
+ /**
+ * Observes specified event
+ * @deprecated `observe` deprecated since 0.8.34 (use `on` instead)
+ * @memberOf fabric.Observable
+ * @alias on
+ * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler})
+ * @param {Function} handler Function that receives a notification when an event of the specified type occurs
+ * @return {Self} thisArg
+ * @chainable
+ */
+ function observe(eventName, handler) {
+ if (!this.__eventListeners) {
+ this.__eventListeners = { };
+ }
+ // one object with key/value pairs was passed
+ if (arguments.length === 1) {
+ for (var prop in eventName) {
+ this.on(prop, eventName[prop]);
+ }
+ }
+ else {
+ if (!this.__eventListeners[eventName]) {
+ this.__eventListeners[eventName] = [];
+ }
+ this.__eventListeners[eventName].push(handler);
+ }
+ return this;
+ }
+
+ /**
+ * Stops event observing for a particular event handler. Calling this method
+ * without arguments removes all handlers for all events
+ * @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead)
+ * @memberOf fabric.Observable
+ * @alias off
+ * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler})
+ * @param {Function} handler Function to be deleted from EventListeners
+ * @return {Self} thisArg
+ * @chainable
+ */
+ function stopObserving(eventName, handler) {
+ if (!this.__eventListeners) {
+ return this;
+ }
+
+ // remove all key/value pairs (event name -> event handler)
+ if (arguments.length === 0) {
+ for (eventName in this.__eventListeners) {
+ _removeEventListener.call(this, eventName);
+ }
+ }
+ // one object with key/value pairs was passed
+ else if (arguments.length === 1 && typeof arguments[0] === 'object') {
+ for (var prop in eventName) {
+ _removeEventListener.call(this, prop, eventName[prop]);
+ }
+ }
+ else {
+ _removeEventListener.call(this, eventName, handler);
+ }
+ return this;
+ }
+
+ /**
+ * Fires event with an optional options object
+ * @deprecated `fire` deprecated since 1.0.7 (use `trigger` instead)
+ * @memberOf fabric.Observable
+ * @alias trigger
+ * @param {String} eventName Event name to fire
+ * @param {Object} [options] Options object
+ * @return {Self} thisArg
+ * @chainable
+ */
+ function fire(eventName, options) {
+ if (!this.__eventListeners) {
+ return this;
+ }
+
+ var listenersForEvent = this.__eventListeners[eventName];
+ if (!listenersForEvent) {
+ return this;
+ }
+
+ for (var i = 0, len = listenersForEvent.length; i < len; i++) {
+ listenersForEvent[i] && listenersForEvent[i].call(this, options || { });
+ }
+ this.__eventListeners[eventName] = listenersForEvent.filter(function(value) {
+ return value !== false;
+ });
+ return this;
+ }
+
+ /**
+ * @namespace fabric.Observable
+ * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#events}
+ * @see {@link http://fabricjs.com/events|Events demo}
+ */
+ fabric.Observable = {
+ observe: observe,
+ stopObserving: stopObserving,
+ fire: fire,
+
+ on: observe,
+ off: stopObserving,
+ trigger: fire
+ };
+})();
+
+
+/**
+ * @namespace fabric.Collection
+ */
+fabric.Collection = {
+
+ _objects: [],
+
+ /**
+ * Adds objects to collection, Canvas or Group, then renders canvas
+ * (if `renderOnAddRemove` is not `false`).
+ * in case of Group no changes to bounding box are made.
+ * Objects should be instances of (or inherit from) fabric.Object
+ * Use of this function is highly discouraged for groups.
+ * you can add a bunch of objects with the add method but then you NEED
+ * to run a addWithUpdate call for the Group class or position/bbox will be wrong.
+ * @param {...fabric.Object} object Zero or more fabric instances
+ * @return {Self} thisArg
+ * @chainable
+ */
+ add: function () {
+ this._objects.push.apply(this._objects, arguments);
+ if (this._onObjectAdded) {
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ this._onObjectAdded(arguments[i]);
+ }
+ }
+ this.renderOnAddRemove && this.requestRenderAll();
+ return this;
+ },
+
+ /**
+ * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`)
+ * An object should be an instance of (or inherit from) fabric.Object
+ * Use of this function is highly discouraged for groups.
+ * you can add a bunch of objects with the insertAt method but then you NEED
+ * to run a addWithUpdate call for the Group class or position/bbox will be wrong.
+ * @param {Object} object Object to insert
+ * @param {Number} index Index to insert object at
+ * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs
+ * @return {Self} thisArg
+ * @chainable
+ */
+ insertAt: function (object, index, nonSplicing) {
+ var objects = this._objects;
+ if (nonSplicing) {
+ objects[index] = object;
+ }
+ else {
+ objects.splice(index, 0, object);
+ }
+ this._onObjectAdded && this._onObjectAdded(object);
+ this.renderOnAddRemove && this.requestRenderAll();
+ return this;
+ },
+
+ /**
+ * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`)
+ * @param {...fabric.Object} object Zero or more fabric instances
+ * @return {Self} thisArg
+ * @chainable
+ */
+ remove: function() {
+ var objects = this._objects,
+ index, somethingRemoved = false;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ index = objects.indexOf(arguments[i]);
+
+ // only call onObjectRemoved if an object was actually removed
+ if (index !== -1) {
+ somethingRemoved = true;
+ objects.splice(index, 1);
+ this._onObjectRemoved && this._onObjectRemoved(arguments[i]);
+ }
+ }
+
+ this.renderOnAddRemove && somethingRemoved && this.requestRenderAll();
+ return this;
+ },
+
+ /**
+ * Executes given function for each object in this group
+ * @param {Function} callback
+ * Callback invoked with current object as first argument,
+ * index - as second and an array of all objects - as third.
+ * Callback is invoked in a context of Global Object (e.g. `window`)
+ * when no `context` argument is given
+ *
+ * @param {Object} context Context (aka thisObject)
+ * @return {Self} thisArg
+ * @chainable
+ */
+ forEachObject: function(callback, context) {
+ var objects = this.getObjects();
+ for (var i = 0, len = objects.length; i < len; i++) {
+ callback.call(context, objects[i], i, objects);
+ }
+ return this;
+ },
+
+ /**
+ * Returns an array of children objects of this instance
+ * Type parameter introduced in 1.3.10
+ * since 2.3.5 this method return always a COPY of the array;
+ * @param {String} [type] When specified, only objects of this type are returned
+ * @return {Array}
+ */
+ getObjects: function(type) {
+ if (typeof type === 'undefined') {
+ return this._objects.concat();
+ }
+ return this._objects.filter(function(o) {
+ return o.type === type;
+ });
+ },
+
+ /**
+ * Returns object at specified index
+ * @param {Number} index
+ * @return {Self} thisArg
+ */
+ item: function (index) {
+ return this._objects[index];
+ },
+
+ /**
+ * Returns true if collection contains no objects
+ * @return {Boolean} true if collection is empty
+ */
+ isEmpty: function () {
+ return this._objects.length === 0;
+ },
+
+ /**
+ * Returns a size of a collection (i.e: length of an array containing its objects)
+ * @return {Number} Collection size
+ */
+ size: function() {
+ return this._objects.length;
+ },
+
+ /**
+ * Returns true if collection contains an object
+ * @param {Object} object Object to check against
+ * @return {Boolean} `true` if collection contains an object
+ */
+ contains: function(object) {
+ return this._objects.indexOf(object) > -1;
+ },
+
+ /**
+ * Returns number representation of a collection complexity
+ * @return {Number} complexity
+ */
+ complexity: function () {
+ return this._objects.reduce(function (memo, current) {
+ memo += current.complexity ? current.complexity() : 0;
+ return memo;
+ }, 0);
+ }
+};
+
+
+/**
+ * @namespace fabric.CommonMethods
+ */
+fabric.CommonMethods = {
+
+ /**
+ * Sets object's properties from options
+ * @param {Object} [options] Options object
+ */
+ _setOptions: function(options) {
+ for (var prop in options) {
+ this.set(prop, options[prop]);
+ }
+ },
+
+ /**
+ * @private
+ * @param {Object} [filler] Options object
+ * @param {String} [property] property to set the Gradient to
+ */
+ _initGradient: function(filler, property) {
+ if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) {
+ this.set(property, new fabric.Gradient(filler));
+ }
+ },
+
+ /**
+ * @private
+ * @param {Object} [filler] Options object
+ * @param {String} [property] property to set the Pattern to
+ * @param {Function} [callback] callback to invoke after pattern load
+ */
+ _initPattern: function(filler, property, callback) {
+ if (filler && filler.source && !(filler instanceof fabric.Pattern)) {
+ this.set(property, new fabric.Pattern(filler, callback));
+ }
+ else {
+ callback && callback();
+ }
+ },
+
+ /**
+ * @private
+ * @param {Object} [options] Options object
+ */
+ _initClipping: function(options) {
+ if (!options.clipTo || typeof options.clipTo !== 'string') {
+ return;
+ }
+
+ var functionBody = fabric.util.getFunctionBody(options.clipTo);
+ if (typeof functionBody !== 'undefined') {
+ this.clipTo = new Function('ctx', functionBody);
+ }
+ },
+
+ /**
+ * @private
+ */
+ _setObject: function(obj) {
+ for (var prop in obj) {
+ this._set(prop, obj[prop]);
+ }
+ },
+
+ /**
+ * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
+ * @param {String|Object} key Property name or object (if object, iterate over the object properties)
+ * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one)
+ * @return {fabric.Object} thisArg
+ * @chainable
+ */
+ set: function(key, value) {
+ if (typeof key === 'object') {
+ this._setObject(key);
+ }
+ else {
+ if (typeof value === 'function' && key !== 'clipTo') {
+ this._set(key, value(this.get(key)));
+ }
+ else {
+ this._set(key, value);
+ }
+ }
+ return this;
+ },
+
+ _set: function(key, value) {
+ this[key] = value;
+ },
+
+ /**
+ * Toggles specified property from `true` to `false` or from `false` to `true`
+ * @param {String} property Property to toggle
+ * @return {fabric.Object} thisArg
+ * @chainable
+ */
+ toggle: function(property) {
+ var value = this.get(property);
+ if (typeof value === 'boolean') {
+ this.set(property, !value);
+ }
+ return this;
+ },
+
+ /**
+ * Basic getter
+ * @param {String} property Property name
+ * @return {*} value of a property
+ */
+ get: function(property) {
+ return this[property];
+ }
+};
+
+
+(function(global) {
+
+ var sqrt = Math.sqrt,
+ atan2 = Math.atan2,
+ pow = Math.pow,
+ PiBy180 = Math.PI / 180,
+ PiBy2 = Math.PI / 2;
+
+ /**
+ * @namespace fabric.util
+ */
+ fabric.util = {
+
+ /**
+ * Calculate the cos of an angle, avoiding returning floats for known results
+ * @static
+ * @memberOf fabric.util
+ * @param {Number} angle the angle in radians or in degree
+ * @return {Number}
+ */
+ cos: function(angle) {
+ if (angle === 0) { return 1; }
+ if (angle < 0) {
+ // cos(a) = cos(-a)
+ angle = -angle;
+ }
+ var angleSlice = angle / PiBy2;
+ switch (angleSlice) {
+ case 1: case 3: return 0;
+ case 2: return -1;
+ }
+ return Math.cos(angle);
+ },
+
+ /**
+ * Calculate the sin of an angle, avoiding returning floats for known results
+ * @static
+ * @memberOf fabric.util
+ * @param {Number} angle the angle in radians or in degree
+ * @return {Number}
+ */
+ sin: function(angle) {
+ if (angle === 0) { return 0; }
+ var angleSlice = angle / PiBy2, sign = 1;
+ if (angle < 0) {
+ // sin(-a) = -sin(a)
+ sign = -1;
+ }
+ switch (angleSlice) {
+ case 1: return sign;
+ case 2: return 0;
+ case 3: return -sign;
+ }
+ return Math.sin(angle);
+ },
+
+ /**
+ * Removes value from an array.
+ * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf`
+ * @static
+ * @memberOf fabric.util
+ * @param {Array} array
+ * @param {*} value
+ * @return {Array} original array
+ */
+ removeFromArray: function(array, value) {
+ var idx = array.indexOf(value);
+ if (idx !== -1) {
+ array.splice(idx, 1);
+ }
+ return array;
+ },
+
+ /**
+ * Returns random number between 2 specified ones.
+ * @static
+ * @memberOf fabric.util
+ * @param {Number} min lower limit
+ * @param {Number} max upper limit
+ * @return {Number} random value (between min and max)
+ */
+ getRandomInt: function(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ },
+
+ /**
+ * Transforms degrees to radians.
+ * @static
+ * @memberOf fabric.util
+ * @param {Number} degrees value in degrees
+ * @return {Number} value in radians
+ */
+ degreesToRadians: function(degrees) {
+ return degrees * PiBy180;
+ },
+
+ /**
+ * Transforms radians to degrees.
+ * @static
+ * @memberOf fabric.util
+ * @param {Number} radians value in radians
+ * @return {Number} value in degrees
+ */
+ radiansToDegrees: function(radians) {
+ return radians / PiBy180;
+ },
+
+ /**
+ * Rotates `point` around `origin` with `radians`
+ * @static
+ * @memberOf fabric.util
+ * @param {fabric.Point} point The point to rotate
+ * @param {fabric.Point} origin The origin of the rotation
+ * @param {Number} radians The radians of the angle for the rotation
+ * @return {fabric.Point} The new rotated point
+ */
+ rotatePoint: function(point, origin, radians) {
+ point.subtractEquals(origin);
+ var v = fabric.util.rotateVector(point, radians);
+ return new fabric.Point(v.x, v.y).addEquals(origin);
+ },
+
+ /**
+ * Rotates `vector` with `radians`
+ * @static
+ * @memberOf fabric.util
+ * @param {Object} vector The vector to rotate (x and y)
+ * @param {Number} radians The radians of the angle for the rotation
+ * @return {Object} The new rotated point
+ */
+ rotateVector: function(vector, radians) {
+ var sin = fabric.util.sin(radians),
+ cos = fabric.util.cos(radians),
+ rx = vector.x * cos - vector.y * sin,
+ ry = vector.x * sin + vector.y * cos;
+ return {
+ x: rx,
+ y: ry
+ };
+ },
+
+ /**
+ * Apply transform t to point p
+ * @static
+ * @memberOf fabric.util
+ * @param {fabric.Point} p The point to transform
+ * @param {Array} t The transform
+ * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied
+ * @return {fabric.Point} The transformed point
+ */
+ transformPoint: function(p, t, ignoreOffset) {
+ if (ignoreOffset) {
+ return new fabric.Point(
+ t[0] * p.x + t[2] * p.y,
+ t[1] * p.x + t[3] * p.y
+ );
+ }
+ return new fabric.Point(
+ t[0] * p.x + t[2] * p.y + t[4],
+ t[1] * p.x + t[3] * p.y + t[5]
+ );
+ },
+
+ /**
+ * Returns coordinates of points's bounding rectangle (left, top, width, height)
+ * @param {Array} points 4 points array
+ * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix
+ * @return {Object} Object with left, top, width, height properties
+ */
+ makeBoundingBoxFromPoints: function(points, transform) {
+ if (transform) {
+ for (var i = 0; i < points.length; i++) {
+ points[i] = fabric.util.transformPoint(points[i], transform);
+ }
+ }
+ var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x],
+ minX = fabric.util.array.min(xPoints),
+ maxX = fabric.util.array.max(xPoints),
+ width = maxX - minX,
+ yPoints = [points[0].y, points[1].y, points[2].y, points[3].y],
+ minY = fabric.util.array.min(yPoints),
+ maxY = fabric.util.array.max(yPoints),
+ height = maxY - minY;
+
+ return {
+ left: minX,
+ top: minY,
+ width: width,
+ height: height
+ };
+ },
+
+ /**
+ * Invert transformation t
+ * @static
+ * @memberOf fabric.util
+ * @param {Array} t The transform
+ * @return {Array} The inverted transform
+ */
+ invertTransform: function(t) {
+ var a = 1 / (t[0] * t[3] - t[1] * t[2]),
+ r = [a * t[3], -a * t[1], -a * t[2], a * t[0]],
+ o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r, true);
+ r[4] = -o.x;
+ r[5] = -o.y;
+ return r;
+ },
+
+ /**
+ * A wrapper around Number#toFixed, which contrary to native method returns number, not string.
+ * @static
+ * @memberOf fabric.util
+ * @param {Number|String} number number to operate on
+ * @param {Number} fractionDigits number of fraction digits to "leave"
+ * @return {Number}
+ */
+ toFixed: function(number, fractionDigits) {
+ return parseFloat(Number(number).toFixed(fractionDigits));
+ },
+
+ /**
+ * Converts from attribute value to pixel value if applicable.
+ * Returns converted pixels or original value not converted.
+ * @param {Number|String} value number to operate on
+ * @param {Number} fontSize
+ * @return {Number|String}
+ */
+ parseUnit: function(value, fontSize) {
+ var unit = /\D{0,2}$/.exec(value),
+ number = parseFloat(value);
+ if (!fontSize) {
+ fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;
+ }
+ switch (unit[0]) {
+ case 'mm':
+ return number * fabric.DPI / 25.4;
+
+ case 'cm':
+ return number * fabric.DPI / 2.54;
+
+ case 'in':
+ return number * fabric.DPI;
+
+ case 'pt':
+ return number * fabric.DPI / 72; // or * 4 / 3
+
+ case 'pc':
+ return number * fabric.DPI / 72 * 12; // or * 16
+
+ case 'em':
+ return number * fontSize;
+
+ default:
+ return number;
+ }
+ },
+
+ /**
+ * Function which always returns `false`.
+ * @static
+ * @memberOf fabric.util
+ * @return {Boolean}
+ */
+ falseFunction: function() {
+ return false;
+ },
+
+ /**
+ * Returns klass "Class" object of given namespace
+ * @memberOf fabric.util
+ * @param {String} type Type of object (eg. 'circle')
+ * @param {String} namespace Namespace to get klass "Class" object from
+ * @return {Object} klass "Class"
+ */
+ getKlass: function(type, namespace) {
+ // capitalize first letter only
+ type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1));
+ return fabric.util.resolveNamespace(namespace)[type];
+ },
+
+ /**
+ * Returns array of attributes for given svg that fabric parses
+ * @memberOf fabric.util
+ * @param {String} type Type of svg element (eg. 'circle')
+ * @return {Array} string names of supported attributes
+ */
+ getSvgAttributes: function(type) {
+ var attributes = [
+ 'instantiated_by_use',
+ 'style',
+ 'id',
+ 'class'
+ ];
+ switch (type) {
+ case 'linearGradient':
+ attributes = attributes.concat(['x1', 'y1', 'x2', 'y2', 'gradientUnits', 'gradientTransform']);
+ break;
+ case 'radialGradient':
+ attributes = attributes.concat(['gradientUnits', 'gradientTransform', 'cx', 'cy', 'r', 'fx', 'fy', 'fr']);
+ break;
+ case 'stop':
+ attributes = attributes.concat(['offset', 'stop-color', 'stop-opacity']);
+ break;
+ }
+ return attributes;
+ },
+
+ /**
+ * Returns object of given namespace
+ * @memberOf fabric.util
+ * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric'
+ * @return {Object} Object for given namespace (default fabric)
+ */
+ resolveNamespace: function(namespace) {
+ if (!namespace) {
+ return fabric;
+ }
+
+ var parts = namespace.split('.'),
+ len = parts.length, i,
+ obj = global || fabric.window;
+
+ for (i = 0; i < len; ++i) {
+ obj = obj[parts[i]];
+ }
+
+ return obj;
+ },
+
+ /**
+ * Loads image element from given url and passes it to a callback
+ * @memberOf fabric.util
+ * @param {String} url URL representing an image
+ * @param {Function} callback Callback; invoked with loaded image
+ * @param {*} [context] Context to invoke callback in
+ * @param {Object} [crossOrigin] crossOrigin value to set image element to
+ */
+ loadImage: function(url, callback, context, crossOrigin) {
+ if (!url) {
+ callback && callback.call(context, url);
+ return;
+ }
+
+ var img = fabric.util.createImage();
+
+ /** @ignore */
+ var onLoadCallback = function () {
+ callback && callback.call(context, img);
+ img = img.onload = img.onerror = null;
+ };
+
+ img.onload = onLoadCallback;
+ /** @ignore */
+ img.onerror = function() {
+ fabric.log('Error loading ' + img.src);
+ callback && callback.call(context, null, true);
+ img = img.onload = img.onerror = null;
+ };
+
+ // data-urls appear to be buggy with crossOrigin
+ // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767
+ // see https://code.google.com/p/chromium/issues/detail?id=315152
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=935069
+ if (url.indexOf('data') !== 0 && crossOrigin) {
+ img.crossOrigin = crossOrigin;
+ }
+
+ // IE10 / IE11-Fix: SVG contents from data: URI
+ // will only be available if the IMG is present
+ // in the DOM (and visible)
+ if (url.substring(0,14) === 'data:image/svg') {
+ img.onload = null;
+ fabric.util.loadImageInDom(img, onLoadCallback);
+ }
+
+ img.src = url;
+ },
+
+ /**
+ * Attaches SVG image with data: URL to the dom
+ * @memberOf fabric.util
+ * @param {Object} img Image object with data:image/svg src
+ * @param {Function} callback Callback; invoked with loaded image
+ * @return {Object} DOM element (div containing the SVG image)
+ */
+ loadImageInDom: function(img, onLoadCallback) {
+ var div = fabric.document.createElement('div');
+ div.style.width = div.style.height = '1px';
+ div.style.left = div.style.top = '-100%';
+ div.style.position = 'absolute';
+ div.appendChild(img);
+ fabric.document.querySelector('body').appendChild(div);
+ /**
+ * Wrap in function to:
+ * 1. Call existing callback
+ * 2. Cleanup DOM
+ */
+ img.onload = function () {
+ onLoadCallback();
+ div.parentNode.removeChild(div);
+ div = null;
+ };
+ },
+
+ /**
+ * Creates corresponding fabric instances from their object representations
+ * @static
+ * @memberOf fabric.util
+ * @param {Array} objects Objects to enliven
+ * @param {Function} callback Callback to invoke when all objects are created
+ * @param {String} namespace Namespace to get klass "Class" object from
+ * @param {Function} reviver Method for further parsing of object elements,
+ * called after each fabric object created.
+ */
+ enlivenObjects: function(objects, callback, namespace, reviver) {
+ objects = objects || [];
+
+ var enlivenedObjects = [],
+ numLoadedObjects = 0,
+ numTotalObjects = objects.length;
+
+ function onLoaded() {
+ if (++numLoadedObjects === numTotalObjects) {
+ callback && callback(enlivenedObjects.filter(function(obj) {
+ // filter out undefined objects (objects that gave error)
+ return obj;
+ }));
+ }
+ }
+
+ if (!numTotalObjects) {
+ callback && callback(enlivenedObjects);
+ return;
+ }
+
+ objects.forEach(function (o, index) {
+ // if sparse array
+ if (!o || !o.type) {
+ onLoaded();
+ return;
+ }
+ var klass = fabric.util.getKlass(o.type, namespace);
+ klass.fromObject(o, function (obj, error) {
+ error || (enlivenedObjects[index] = obj);
+ reviver && reviver(o, obj, error);
+ onLoaded();
+ });
+ });
+ },
+
+ /**
+ * Create and wait for loading of patterns
+ * @static
+ * @memberOf fabric.util
+ * @param {Array} patterns Objects to enliven
+ * @param {Function} callback Callback to invoke when all objects are created
+ * called after each fabric object created.
+ */
+ enlivenPatterns: function(patterns, callback) {
+ patterns = patterns || [];
+
+ function onLoaded() {
+ if (++numLoadedPatterns === numPatterns) {
+ callback && callback(enlivenedPatterns);
+ }
+ }
+
+ var enlivenedPatterns = [],
+ numLoadedPatterns = 0,
+ numPatterns = patterns.length;
+
+ if (!numPatterns) {
+ callback && callback(enlivenedPatterns);
+ return;
+ }
+
+ patterns.forEach(function (p, index) {
+ if (p && p.source) {
+ new fabric.Pattern(p, function(pattern) {
+ enlivenedPatterns[index] = pattern;
+ onLoaded();
+ });
+ }
+ else {
+ enlivenedPatterns[index] = p;
+ onLoaded();
+ }
+ });
+ },
+
+ /**
+ * Groups SVG elements (usually those retrieved from SVG document)
+ * @static
+ * @memberOf fabric.util
+ * @param {Array} elements SVG elements to group
+ * @param {Object} [options] Options object
+ * @param {String} path Value to set sourcePath to
+ * @return {fabric.Object|fabric.Group}
+ */
+ groupSVGElements: function(elements, options, path) {
+ var object;
+ if (elements && elements.length === 1) {
+ return elements[0];
+ }
+ if (options) {
+ if (options.width && options.height) {
+ options.centerPoint = {
+ x: options.width / 2,
+ y: options.height / 2
+ };
+ }
+ else {
+ delete options.width;
+ delete options.height;
+ }
+ }
+ object = new fabric.Group(elements, options);
+ if (typeof path !== 'undefined') {
+ object.sourcePath = path;
+ }
+ return object;
+ },
+
+ /**
+ * Populates an object with properties of another object
+ * @static
+ * @memberOf fabric.util
+ * @param {Object} source Source object
+ * @param {Object} destination Destination object
+ * @return {Array} properties Properties names to include
+ */
+ populateWithProperties: function(source, destination, properties) {
+ if (properties && Object.prototype.toString.call(properties) === '[object Array]') {
+ for (var i = 0, len = properties.length; i < len; i++) {
+ if (properties[i] in source) {
+ destination[properties[i]] = source[properties[i]];
+ }
+ }
+ }
+ },
+
+ /**
+ * Draws a dashed line between two points
+ *
+ * This method is used to draw dashed line around selection area.
+ * See dotted stroke in canvas
+ *
+ * @param {CanvasRenderingContext2D} ctx context
+ * @param {Number} x start x coordinate
+ * @param {Number} y start y coordinate
+ * @param {Number} x2 end x coordinate
+ * @param {Number} y2 end y coordinate
+ * @param {Array} da dash array pattern
+ */
+ drawDashedLine: function(ctx, x, y, x2, y2, da) {
+ var dx = x2 - x,
+ dy = y2 - y,
+ len = sqrt(dx * dx + dy * dy),
+ rot = atan2(dy, dx),
+ dc = da.length,
+ di = 0,
+ draw = true;
+
+ ctx.save();
+ ctx.translate(x, y);
+ ctx.moveTo(0, 0);
+ ctx.rotate(rot);
+
+ x = 0;
+ while (len > x) {
+ x += da[di++ % dc];
+ if (x > len) {
+ x = len;
+ }
+ ctx[draw ? 'lineTo' : 'moveTo'](x, 0);
+ draw = !draw;
+ }
+
+ ctx.restore();
+ },
+
+ /**
+ * Creates canvas element
+ * @static
+ * @memberOf fabric.util
+ * @return {CanvasElement} initialized canvas element
+ */
+ createCanvasElement: function() {
+ return fabric.document.createElement('canvas');
+ },
+
+ /**
+ * Creates a canvas element that is a copy of another and is also painted
+ * @param {CanvasElement} canvas to copy size and content of
+ * @static
+ * @memberOf fabric.util
+ * @return {CanvasElement} initialized canvas element
+ */
+ copyCanvasElement: function(canvas) {
+ var newCanvas = fabric.util.createCanvasElement();
+ newCanvas.width = canvas.width;
+ newCanvas.height = canvas.height;
+ newCanvas.getContext('2d').drawImage(canvas, 0, 0);
+ return newCanvas;
+ },
+
+ /**
+ * since 2.6.0 moved from canvas instance to utility.
+ * @param {CanvasElement} canvasEl to copy size and content of
+ * @param {String} format 'jpeg' or 'png', in some browsers 'webp' is ok too
+ * @param {Number} quality <= 1 and > 0
+ * @static
+ * @memberOf fabric.util
+ * @return {String} data url
+ */
+ toDataURL: function(canvasEl, format, quality) {
+ return canvasEl.toDataURL('image/' + format, quality);
+ },
+
+ /**
+ * Creates image element (works on client and node)
+ * @static
+ * @memberOf fabric.util
+ * @return {HTMLImageElement} HTML image element
+ */
+ createImage: function() {
+ return fabric.document.createElement('img');
+ },
+
+ /**
+ * @static
+ * @memberOf fabric.util
+ * @deprecated since 2.0.0
+ * @param {fabric.Object} receiver Object implementing `clipTo` method
+ * @param {CanvasRenderingContext2D} ctx Context to clip
+ */
+ clipContext: function(receiver, ctx) {
+ ctx.save();
+ ctx.beginPath();
+ receiver.clipTo(ctx);
+ ctx.clip();
+ },
+
+ /**
+ * Multiply matrix A by matrix B to nest transformations
+ * @static
+ * @memberOf fabric.util
+ * @param {Array} a First transformMatrix
+ * @param {Array} b Second transformMatrix
+ * @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices
+ * @return {Array} The product of the two transform matrices
+ */
+ multiplyTransformMatrices: function(a, b, is2x2) {
+ // Matrix multiply a * b
+ return [
+ a[0] * b[0] + a[2] * b[1],
+ a[1] * b[0] + a[3] * b[1],
+ a[0] * b[2] + a[2] * b[3],
+ a[1] * b[2] + a[3] * b[3],
+ is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4],
+ is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5]
+ ];
+ },
+
+ /**
+ * Decomposes standard 2x3 matrix into transform components
+ * @static
+ * @memberOf fabric.util
+ * @param {Array} a transformMatrix
+ * @return {Object} Components of transform
+ */
+ qrDecompose: function(a) {
+ var angle = atan2(a[1], a[0]),
+ denom = pow(a[0], 2) + pow(a[1], 2),
+ scaleX = sqrt(denom),
+ scaleY = (a[0] * a[3] - a[2] * a [1]) / scaleX,
+ skewX = atan2(a[0] * a[2] + a[1] * a [3], denom);
+ return {
+ angle: angle / PiBy180,
+ scaleX: scaleX,
+ scaleY: scaleY,
+ skewX: skewX / PiBy180,
+ skewY: 0,
+ translateX: a[4],
+ translateY: a[5]
+ };
+ },
+
+ /**
+ * Returns a transform matrix starting from an object of the same kind of
+ * the one returned from qrDecompose, useful also if you want to calculate some
+ * transformations from an object that is not enlived yet
+ * @static
+ * @memberOf fabric.util
+ * @param {Object} options
+ * @param {Number} [options.angle] angle in degrees
+ * @return {Number[]} transform matrix
+ */
+ calcRotateMatrix: function(options) {
+ if (!options.angle) {
+ return fabric.iMatrix.concat();
+ }
+ var theta = fabric.util.degreesToRadians(options.angle),
+ cos = fabric.util.cos(theta),
+ sin = fabric.util.sin(theta);
+ return [cos, sin, -sin, cos, 0, 0];
+ },
+
+ /**
+ * Returns a transform matrix starting from an object of the same kind of
+ * the one returned from qrDecompose, useful also if you want to calculate some
+ * transformations from an object that is not enlived yet.
+ * is called DimensionsTransformMatrix because those properties are the one that influence
+ * the size of the resulting box of the object.
+ * @static
+ * @memberOf fabric.util
+ * @param {Object} options
+ * @param {Number} [options.scaleX]
+ * @param {Number} [options.scaleY]
+ * @param {Boolean} [options.flipX]
+ * @param {Boolean} [options.flipY]
+ * @param {Number} [options.skewX]
+ * @param {Number} [options.skewX]
+ * @return {Number[]} transform matrix
+ */
+ calcDimensionsMatrix: function(options) {
+ var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX,
+ scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY,
+ scaleMatrix = [
+ options.flipX ? -scaleX : scaleX,
+ 0,
+ 0,
+ options.flipY ? -scaleY : scaleY,
+ 0,
+ 0],
+ multiply = fabric.util.multiplyTransformMatrices,
+ degreesToRadians = fabric.util.degreesToRadians;
+ if (options.skewX) {
+ scaleMatrix = multiply(
+ scaleMatrix,
+ [1, 0, Math.tan(degreesToRadians(options.skewX)), 1],
+ true);
+ }
+ if (options.skewY) {
+ scaleMatrix = multiply(
+ scaleMatrix,
+ [1, Math.tan(degreesToRadians(options.skewY)), 0, 1],
+ true);
+ }
+ return scaleMatrix;
+ },
+
+ /**
+ * Returns a transform matrix starting from an object of the same kind of
+ * the one returned from qrDecompose, useful also if you want to calculate some
+ * transformations from an object that is not enlived yet
+ * @static
+ * @memberOf fabric.util
+ * @param {Object} options
+ * @param {Number} [options.angle]
+ * @param {Number} [options.scaleX]
+ * @param {Number} [options.scaleY]
+ * @param {Boolean} [options.flipX]
+ * @param {Boolean} [options.flipY]
+ * @param {Number} [options.skewX]
+ * @param {Number} [options.skewX]
+ * @param {Number} [options.translateX]
+ * @param {Number} [options.translateY]
+ * @return {Number[]} transform matrix
+ */
+ composeMatrix: function(options) {
+ var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0],
+ multiply = fabric.util.multiplyTransformMatrices;
+ if (options.angle) {
+ matrix = multiply(matrix, fabric.util.calcRotateMatrix(options));
+ }
+ if (options.scaleX || options.scaleY || options.skewX || options.skewY || options.flipX || options.flipY) {
+ matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options));
+ }
+ return matrix;
+ },
+
+ /**
+ * Returns a transform matrix that has the same effect of scaleX, scaleY and skewX.
+ * Is deprecated for composeMatrix. Please do not use it.
+ * @static
+ * @deprecated since 3.4.0
+ * @memberOf fabric.util
+ * @param {Number} scaleX
+ * @param {Number} scaleY
+ * @param {Number} skewX
+ * @return {Number[]} transform matrix
+ */
+ customTransformMatrix: function(scaleX, scaleY, skewX) {
+ return fabric.util.composeMatrix({ scaleX: scaleX, scaleY: scaleY, skewX: skewX });
+ },
+
+ /**
+ * reset an object transform state to neutral. Top and left are not accounted for
+ * @static
+ * @memberOf fabric.util
+ * @param {fabric.Object} target object to transform
+ */
+ resetObjectTransform: function (target) {
+ target.scaleX = 1;
+ target.scaleY = 1;
+ target.skewX = 0;
+ target.skewY = 0;
+ target.flipX = false;
+ target.flipY = false;
+ target.rotate(0);
+ },
+
+ /**
+ * Extract Object transform values
+ * @static
+ * @memberOf fabric.util
+ * @param {fabric.Object} target object to read from
+ * @return {Object} Components of transform
+ */
+ saveObjectTransform: function (target) {
+ return {
+ scaleX: target.scaleX,
+ scaleY: target.scaleY,
+ skewX: target.skewX,
+ skewY: target.skewY,
+ angle: target.angle,
+ left: target.left,
+ flipX: target.flipX,
+ flipY: target.flipY,
+ top: target.top
+ };
+ },
+
+ /**
+ * Returns string representation of function body
+ * @param {Function} fn Function to get body of
+ * @return {String} Function body
+ */
+ getFunctionBody: function(fn) {
+ return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1];
+ },
+
+ /**
+ * Returns true if context has transparent pixel
+ * at specified location (taking tolerance into account)
+ * @param {CanvasRenderingContext2D} ctx context
+ * @param {Number} x x coordinate
+ * @param {Number} y y coordinate
+ * @param {Number} tolerance Tolerance
+ */
+ isTransparent: function(ctx, x, y, tolerance) {
+
+ // If tolerance is > 0 adjust start coords to take into account.
+ // If moves off Canvas fix to 0
+ if (tolerance > 0) {
+ if (x > tolerance) {
+ x -= tolerance;
+ }
+ else {
+ x = 0;
+ }
+ if (y > tolerance) {
+ y -= tolerance;
+ }
+ else {
+ y = 0;
+ }
+ }
+
+ var _isTransparent = true, i, temp,
+ imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1),
+ l = imageData.data.length;
+
+ // Split image data - for tolerance > 1, pixelDataSize = 4;
+ for (i = 3; i < l; i += 4) {
+ temp = imageData.data[i];
+ _isTransparent = temp <= 0;
+ if (_isTransparent === false) {
+ break; // Stop if colour found
+ }
+ }
+
+ imageData = null;
+
+ return _isTransparent;
+ },
+
+ /**
+ * Parse preserveAspectRatio attribute from element
+ * @param {string} attribute to be parsed
+ * @return {Object} an object containing align and meetOrSlice attribute
+ */
+ parsePreserveAspectRatioAttribute: function(attribute) {
+ var meetOrSlice = 'meet', alignX = 'Mid', alignY = 'Mid',
+ aspectRatioAttrs = attribute.split(' '), align;
+
+ if (aspectRatioAttrs && aspectRatioAttrs.length) {
+ meetOrSlice = aspectRatioAttrs.pop();
+ if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') {
+ align = meetOrSlice;
+ meetOrSlice = 'meet';
+ }
+ else if (aspectRatioAttrs.length) {
+ align = aspectRatioAttrs.pop();
+ }
+ }
+ //divide align in alignX and alignY
+ alignX = align !== 'none' ? align.slice(1, 4) : 'none';
+ alignY = align !== 'none' ? align.slice(5, 8) : 'none';
+ return {
+ meetOrSlice: meetOrSlice,
+ alignX: alignX,
+ alignY: alignY
+ };
+ },
+
+ /**
+ * Clear char widths cache for the given font family or all the cache if no
+ * fontFamily is specified.
+ * Use it if you know you are loading fonts in a lazy way and you are not waiting
+ * for custom fonts to load properly when adding text objects to the canvas.
+ * If a text object is added when its own font is not loaded yet, you will get wrong
+ * measurement and so wrong bounding boxes.
+ * After the font cache is cleared, either change the textObject text content or call
+ * initDimensions() to trigger a recalculation
+ * @memberOf fabric.util
+ * @param {String} [fontFamily] font family to clear
+ */
+ clearFabricFontCache: function(fontFamily) {
+ fontFamily = (fontFamily || '').toLowerCase();
+ if (!fontFamily) {
+ fabric.charWidthsCache = { };
+ }
+ else if (fabric.charWidthsCache[fontFamily]) {
+ delete fabric.charWidthsCache[fontFamily];
+ }
+ },
+
+ /**
+ * Given current aspect ratio, determines the max width and height that can
+ * respect the total allowed area for the cache.
+ * @memberOf fabric.util
+ * @param {Number} ar aspect ratio
+ * @param {Number} maximumArea Maximum area you want to achieve
+ * @return {Object.x} Limited dimensions by X
+ * @return {Object.y} Limited dimensions by Y
+ */
+ limitDimsByArea: function(ar, maximumArea) {
+ var roughWidth = Math.sqrt(maximumArea * ar),
+ perfLimitSizeY = Math.floor(maximumArea / roughWidth);
+ return { x: Math.floor(roughWidth), y: perfLimitSizeY };
+ },
+
+ capValue: function(min, value, max) {
+ return Math.max(min, Math.min(value, max));
+ },
+
+ findScaleToFit: function(source, destination) {
+ return Math.min(destination.width / source.width, destination.height / source.height);
+ },
+
+ findScaleToCover: function(source, destination) {
+ return Math.max(destination.width / source.width, destination.height / source.height);
+ },
+
+ /**
+ * given an array of 6 number returns something like `"matrix(...numbers)"`
+ * @memberOf fabric.util
+ * @param {Array} trasnform an array with 6 numbers
+ * @return {String} transform matrix for svg
+ * @return {Object.y} Limited dimensions by Y
+ */
+ matrixToSVG: function(transform) {
+ return 'matrix(' + transform.map(function(value) {
+ return fabric.util.toFixed(value, fabric.Object.NUM_FRACTION_DIGITS);
+ }).join(' ') + ')';
+ }
+ };
+})(typeof exports !== 'undefined' ? exports : this);
+
+
+(function() {
+
+ var _join = Array.prototype.join;
+
+ /* Adapted from http://dxr.mozilla.org/mozilla-central/source/content/svg/content/src/nsSVGPathDataParser.cpp
+ * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here
+ * http://mozilla.org/MPL/2.0/
+ */
+ function arcToSegments(toX, toY, rx, ry, large, sweep, rotateX) {
+ var argsString = _join.call(arguments);
+ if (fabric.arcToSegmentsCache[argsString]) {
+ return fabric.arcToSegmentsCache[argsString];
+ }
+
+ var PI = Math.PI, th = rotateX * PI / 180,
+ sinTh = fabric.util.sin(th),
+ cosTh = fabric.util.cos(th),
+ fromX = 0, fromY = 0;
+
+ rx = Math.abs(rx);
+ ry = Math.abs(ry);
+
+ var px = -cosTh * toX * 0.5 - sinTh * toY * 0.5,
+ py = -cosTh * toY * 0.5 + sinTh * toX * 0.5,
+ rx2 = rx * rx, ry2 = ry * ry, py2 = py * py, px2 = px * px,
+ pl = rx2 * ry2 - rx2 * py2 - ry2 * px2,
+ root = 0;
+
+ if (pl < 0) {
+ var s = Math.sqrt(1 - pl / (rx2 * ry2));
+ rx *= s;
+ ry *= s;
+ }
+ else {
+ root = (large === sweep ? -1.0 : 1.0) *
+ Math.sqrt( pl / (rx2 * py2 + ry2 * px2));
+ }
+
+ var cx = root * rx * py / ry,
+ cy = -root * ry * px / rx,
+ cx1 = cosTh * cx - sinTh * cy + toX * 0.5,
+ cy1 = sinTh * cx + cosTh * cy + toY * 0.5,
+ mTheta = calcVectorAngle(1, 0, (px - cx) / rx, (py - cy) / ry),
+ dtheta = calcVectorAngle((px - cx) / rx, (py - cy) / ry, (-px - cx) / rx, (-py - cy) / ry);
+
+ if (sweep === 0 && dtheta > 0) {
+ dtheta -= 2 * PI;
+ }
+ else if (sweep === 1 && dtheta < 0) {
+ dtheta += 2 * PI;
+ }
+
+ // Convert into cubic bezier segments <= 90deg
+ var segments = Math.ceil(Math.abs(dtheta / PI * 2)),
+ result = [], mDelta = dtheta / segments,
+ mT = 8 / 3 * Math.sin(mDelta / 4) * Math.sin(mDelta / 4) / Math.sin(mDelta / 2),
+ th3 = mTheta + mDelta;
+
+ for (var i = 0; i < segments; i++) {
+ result[i] = segmentToBezier(mTheta, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY);
+ fromX = result[i][4];
+ fromY = result[i][5];
+ mTheta = th3;
+ th3 += mDelta;
+ }
+ fabric.arcToSegmentsCache[argsString] = result;
+ return result;
+ }
+
+ function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) {
+ var costh2 = fabric.util.cos(th2),
+ sinth2 = fabric.util.sin(th2),
+ costh3 = fabric.util.cos(th3),
+ sinth3 = fabric.util.sin(th3),
+ toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1,
+ toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1,
+ cp1X = fromX + mT * ( -cosTh * rx * sinth2 - sinTh * ry * costh2),
+ cp1Y = fromY + mT * ( -sinTh * rx * sinth2 + cosTh * ry * costh2),
+ cp2X = toX + mT * ( cosTh * rx * sinth3 + sinTh * ry * costh3),
+ cp2Y = toY + mT * ( sinTh * rx * sinth3 - cosTh * ry * costh3);
+
+ return [
+ cp1X, cp1Y,
+ cp2X, cp2Y,
+ toX, toY
+ ];
+ }
+
+ /*
+ * Private
+ */
+ function calcVectorAngle(ux, uy, vx, vy) {
+ var ta = Math.atan2(uy, ux),
+ tb = Math.atan2(vy, vx);
+ if (tb >= ta) {
+ return tb - ta;
+ }
+ else {
+ return 2 * Math.PI - (ta - tb);
+ }
+ }
+
+ /**
+ * Draws arc
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} fx
+ * @param {Number} fy
+ * @param {Array} coords
+ */
+ fabric.util.drawArc = function(ctx, fx, fy, coords) {
+ var rx = coords[0],
+ ry = coords[1],
+ rot = coords[2],
+ large = coords[3],
+ sweep = coords[4],
+ tx = coords[5],
+ ty = coords[6],
+ segs = [[], [], [], []],
+ segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot);
+
+ for (var i = 0, len = segsNorm.length; i < len; i++) {
+ segs[i][0] = segsNorm[i][0] + fx;
+ segs[i][1] = segsNorm[i][1] + fy;
+ segs[i][2] = segsNorm[i][2] + fx;
+ segs[i][3] = segsNorm[i][3] + fy;
+ segs[i][4] = segsNorm[i][4] + fx;
+ segs[i][5] = segsNorm[i][5] + fy;
+ ctx.bezierCurveTo.apply(ctx, segs[i]);
+ }
+ };
+
+ /**
+ * Calculate bounding box of a elliptic-arc
+ * @param {Number} fx start point of arc
+ * @param {Number} fy
+ * @param {Number} rx horizontal radius
+ * @param {Number} ry vertical radius
+ * @param {Number} rot angle of horizontal axe
+ * @param {Number} large 1 or 0, whatever the arc is the big or the small on the 2 points
+ * @param {Number} sweep 1 or 0, 1 clockwise or counterclockwise direction
+ * @param {Number} tx end point of arc
+ * @param {Number} ty
+ */
+ fabric.util.getBoundsOfArc = function(fx, fy, rx, ry, rot, large, sweep, tx, ty) {
+
+ var fromX = 0, fromY = 0, bound, bounds = [],
+ segs = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot);
+
+ for (var i = 0, len = segs.length; i < len; i++) {
+ bound = getBoundsOfCurve(fromX, fromY, segs[i][0], segs[i][1], segs[i][2], segs[i][3], segs[i][4], segs[i][5]);
+ bounds.push({ x: bound[0].x + fx, y: bound[0].y + fy });
+ bounds.push({ x: bound[1].x + fx, y: bound[1].y + fy });
+ fromX = segs[i][4];
+ fromY = segs[i][5];
+ }
+ return bounds;
+ };
+
+ /**
+ * Calculate bounding box of a beziercurve
+ * @param {Number} x0 starting point
+ * @param {Number} y0
+ * @param {Number} x1 first control point
+ * @param {Number} y1
+ * @param {Number} x2 secondo control point
+ * @param {Number} y2
+ * @param {Number} x3 end of beizer
+ * @param {Number} y3
+ */
+ // taken from http://jsbin.com/ivomiq/56/edit no credits available for that.
+ function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) {
+ var argsString;
+ if (fabric.cachesBoundsOfCurve) {
+ argsString = _join.call(arguments);
+ if (fabric.boundsOfCurveCache[argsString]) {
+ return fabric.boundsOfCurveCache[argsString];
+ }
+ }
+
+ var sqrt = Math.sqrt,
+ min = Math.min, max = Math.max,
+ abs = Math.abs, tvalues = [],
+ bounds = [[], []],
+ a, b, c, t, t1, t2, b2ac, sqrtb2ac;
+
+ b = 6 * x0 - 12 * x1 + 6 * x2;
+ a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
+ c = 3 * x1 - 3 * x0;
+
+ for (var i = 0; i < 2; ++i) {
+ if (i > 0) {
+ b = 6 * y0 - 12 * y1 + 6 * y2;
+ a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
+ c = 3 * y1 - 3 * y0;
+ }
+
+ if (abs(a) < 1e-12) {
+ if (abs(b) < 1e-12) {
+ continue;
+ }
+ t = -c / b;
+ if (0 < t && t < 1) {
+ tvalues.push(t);
+ }
+ continue;
+ }
+ b2ac = b * b - 4 * c * a;
+ if (b2ac < 0) {
+ continue;
+ }
+ sqrtb2ac = sqrt(b2ac);
+ t1 = (-b + sqrtb2ac) / (2 * a);
+ if (0 < t1 && t1 < 1) {
+ tvalues.push(t1);
+ }
+ t2 = (-b - sqrtb2ac) / (2 * a);
+ if (0 < t2 && t2 < 1) {
+ tvalues.push(t2);
+ }
+ }
+
+ var x, y, j = tvalues.length, jlen = j, mt;
+ while (j--) {
+ t = tvalues[j];
+ mt = 1 - t;
+ x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3);
+ bounds[0][j] = x;
+
+ y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3);
+ bounds[1][j] = y;
+ }
+
+ bounds[0][jlen] = x0;
+ bounds[1][jlen] = y0;
+ bounds[0][jlen + 1] = x3;
+ bounds[1][jlen + 1] = y3;
+ var result = [
+ {
+ x: min.apply(null, bounds[0]),
+ y: min.apply(null, bounds[1])
+ },
+ {
+ x: max.apply(null, bounds[0]),
+ y: max.apply(null, bounds[1])
+ }
+ ];
+ if (fabric.cachesBoundsOfCurve) {
+ fabric.boundsOfCurveCache[argsString] = result;
+ }
+ return result;
+ }
+
+ fabric.util.getBoundsOfCurve = getBoundsOfCurve;
+
+})();
+
+
+(function() {
+
+ var slice = Array.prototype.slice;
+
+ /**
+ * Invokes method on all items in a given array
+ * @memberOf fabric.util.array
+ * @param {Array} array Array to iterate over
+ * @param {String} method Name of a method to invoke
+ * @return {Array}
+ */
+ function invoke(array, method) {
+ var args = slice.call(arguments, 2), result = [];
+ for (var i = 0, len = array.length; i < len; i++) {
+ result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]);
+ }
+ return result;
+ }
+
+ /**
+ * Finds maximum value in array (not necessarily "first" one)
+ * @memberOf fabric.util.array
+ * @param {Array} array Array to iterate over
+ * @param {String} byProperty
+ * @return {*}
+ */
+ function max(array, byProperty) {
+ return find(array, byProperty, function(value1, value2) {
+ return value1 >= value2;
+ });
+ }
+
+ /**
+ * Finds minimum value in array (not necessarily "first" one)
+ * @memberOf fabric.util.array
+ * @param {Array} array Array to iterate over
+ * @param {String} byProperty
+ * @return {*}
+ */
+ function min(array, byProperty) {
+ return find(array, byProperty, function(value1, value2) {
+ return value1 < value2;
+ });
+ }
+
+ /**
+ * @private
+ */
+ function fill(array, value) {
+ var k = array.length;
+ while (k--) {
+ array[k] = value;
+ }
+ return array;
+ }
+
+ /**
+ * @private
+ */
+ function find(array, byProperty, condition) {
+ if (!array || array.length === 0) {
+ return;
+ }
+
+ var i = array.length - 1,
+ result = byProperty ? array[i][byProperty] : array[i];
+ if (byProperty) {
+ while (i--) {
+ if (condition(array[i][byProperty], result)) {
+ result = array[i][byProperty];
+ }
+ }
+ }
+ else {
+ while (i--) {
+ if (condition(array[i], result)) {
+ result = array[i];
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @namespace fabric.util.array
+ */
+ fabric.util.array = {
+ fill: fill,
+ invoke: invoke,
+ min: min,
+ max: max
+ };
+
+})();
+
+
+(function() {
+ /**
+ * Copies all enumerable properties of one js object to another
+ * this does not and cannot compete with generic utils.
+ * Does not clone or extend fabric.Object subclasses.
+ * This is mostly for internal use and has extra handling for fabricJS objects
+ * it skips the canvas property in deep cloning.
+ * @memberOf fabric.util.object
+ * @param {Object} destination Where to copy to
+ * @param {Object} source Where to copy from
+ * @return {Object}
+ */
+
+ function extend(destination, source, deep) {
+ // JScript DontEnum bug is not taken care of
+ // the deep clone is for internal use, is not meant to avoid
+ // javascript traps or cloning html element or self referenced objects.
+ if (deep) {
+ if (!fabric.isLikelyNode && source instanceof Element) {
+ // avoid cloning deep images, canvases,
+ destination = source;
+ }
+ else if (source instanceof Array) {
+ destination = [];
+ for (var i = 0, len = source.length; i < len; i++) {
+ destination[i] = extend({ }, source[i], deep);
+ }
+ }
+ else if (source && typeof source === 'object') {
+ for (var property in source) {
+ if (property === 'canvas') {
+ destination[property] = extend({ }, source[property]);
+ }
+ else if (source.hasOwnProperty(property)) {
+ destination[property] = extend({ }, source[property], deep);
+ }
+ }
+ }
+ else {
+ // this sounds odd for an extend but is ok for recursive use
+ destination = source;
+ }
+ }
+ else {
+ for (var property in source) {
+ destination[property] = source[property];
+ }
+ }
+ return destination;
+ }
+
+ /**
+ * Creates an empty object and copies all enumerable properties of another object to it
+ * @memberOf fabric.util.object
+ * TODO: this function return an empty object if you try to clone null
+ * @param {Object} object Object to clone
+ * @return {Object}
+ */
+ function clone(object, deep) {
+ return extend({ }, object, deep);
+ }
+
+ /** @namespace fabric.util.object */
+ fabric.util.object = {
+ extend: extend,
+ clone: clone
+ };
+ fabric.util.object.extend(fabric.util, fabric.Observable);
+})();
+
+
+(function() {
+
+ /**
+ * Camelizes a string
+ * @memberOf fabric.util.string
+ * @param {String} string String to camelize
+ * @return {String} Camelized version of a string
+ */
+ function camelize(string) {
+ return string.replace(/-+(.)?/g, function(match, character) {
+ return character ? character.toUpperCase() : '';
+ });
+ }
+
+ /**
+ * Capitalizes a string
+ * @memberOf fabric.util.string
+ * @param {String} string String to capitalize
+ * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized
+ * and other letters stay untouched, if false first letter is capitalized
+ * and other letters are converted to lowercase.
+ * @return {String} Capitalized version of a string
+ */
+ function capitalize(string, firstLetterOnly) {
+ return string.charAt(0).toUpperCase() +
+ (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase());
+ }
+
+ /**
+ * Escapes XML in a string
+ * @memberOf fabric.util.string
+ * @param {String} string String to escape
+ * @return {String} Escaped version of a string
+ */
+ function escapeXml(string) {
+ return string.replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ .replace(/