From 66e0f08579906f191b7527809d9fda7ce0a95005 Mon Sep 17 00:00:00 2001 From: Alban Date: Thu, 14 Nov 2019 09:23:20 +0100 Subject: [PATCH] [wip] --- img_generator.sh | 2 +- public/background_tshirt.png | Bin 0 -> 259024 bytes public/batman_small.png | Bin 0 -> 3442 bytes public/fabric.js | 29387 +++++++++++++++++++++++++++++++++ public/index.php | 218 + stamps/red_activites.png | Bin 0 -> 58986 bytes stamps/red_all.png | Bin 0 -> 45501 bytes stamps/red_dates.png | Bin 0 -> 27418 bytes stamps/red_details.png | Bin 0 -> 35017 bytes stamps/red_lieux.png | Bin 0 -> 35028 bytes stamps/yellow_activites.png | Bin 0 -> 52874 bytes 11 files changed, 29606 insertions(+), 1 deletion(-) create mode 100644 public/background_tshirt.png create mode 100644 public/batman_small.png create mode 100644 public/fabric.js create mode 100644 public/index.php create mode 100644 stamps/red_activites.png create mode 100644 stamps/red_all.png create mode 100644 stamps/red_dates.png create mode 100644 stamps/red_details.png create mode 100644 stamps/red_lieux.png create mode 100644 stamps/yellow_activites.png diff --git a/img_generator.sh b/img_generator.sh index 1fca4a8..492a57b 100755 --- a/img_generator.sh +++ b/img_generator.sh @@ -19,7 +19,7 @@ TMPFILE=$(mktemp) # Search images for i in {1..99}; do - RAND=$( echo | awk ' { srand(); print int(rand() * 234937 ) } ') + RAND=$( echo | awk ' { srand(); print int(rand() * 99171 ) } ') WORD=$(awk "NR==$RAND" /usr/share/dict/words); [ -n "$TAG" ] && WORD=$TAG URL="https://api.flickr.com/services/feeds/photos_public.gne?tags=$WORD&tagmode=any"; diff --git a/public/background_tshirt.png b/public/background_tshirt.png new file mode 100644 index 0000000000000000000000000000000000000000..f654c391b723ea0f7e29b9cb6f2a6825288db629 GIT binary patch literal 259024 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 z

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

EBNT7M~fXL4)g%E;mW6 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

(J((E!-$J4F?MCf#UByUfZMivdl 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 z

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 index = this._reSpace.test(this._text[selectionStart]) ? selectionStart - 1 : selectionStart, + _char = this._text[index], + reNonWord = /[ \n\.,;!\?\-]/; + + while (!reNonWord.test(_char) && index > 0 && index < this._text.length) { + index += direction; + _char = this._text[index]; + } + if (reNonWord.test(_char) && _char !== '\n') { + 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 + }, + upperCanvas = this.canvas.upperCanvasEl, + upperCanvasWidth = upperCanvas.width, + upperCanvasHeight = upperCanvas.height, + 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(); + }, + + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('mousedblclick', function(options) { + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }); + this.on('tripleclick', function(options) { + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }); + }, + + /** + * 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; + } + if (this === this.canvas._activeObject) { + this.selected = true; + } + }, + + /** + * 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) { + var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false; + var map = this._styleMap[lineIndex]; + var 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); +(function() { + + /** + * Override _setObjectScale and add Textbox specific resizing behavior. Resizing + * a Textbox doesn't scale text, it only changes width and makes text wrap automatically. + */ + var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale; + + fabric.Canvas.prototype._setObjectScale = function(localMouse, transform, + lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { + + var t = transform.target, scaled, + scaleX = localMouse.x * t.scaleX / _dim.x, + scaleY = localMouse.y * t.scaleY / _dim.y; + if (by === 'x' && t instanceof fabric.Textbox) { + var tw = t._getTransformedDimensions().x; + var w = t.width * (localMouse.x / tw); + transform.newScaleX = scaleX; + transform.newScaleY = scaleY; + if (w >= t.getMinWidth()) { + scaled = w !== t.width; + t.set('width', w); + return scaled; + } + } + else { + return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, + lockScalingX, lockScalingY, by, lockScalingFlip, _dim); + } + }; + + fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.IText.prototype */ { + /** + * @private + */ + _removeExtraneousStyles: function() { + for (var prop in this._styleMap) { + if (!this._textLines[prop]) { + delete this.styles[this._styleMap[prop].line]; + } + } + }, + + }); +})(); diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..1536d07 --- /dev/null +++ b/public/index.php @@ -0,0 +1,218 @@ +soulless feelings"; + $stampsImgList=scandir("../stamps"); + foreach( $stampsImgList as $file ){ + if( strpos( "$file", ".") != 0 ){ + $stampsSelect .= ""; + } + } + + + $bgdImgList = scandir("../synthesis"); + $randomKeys = array_rand( $bgdImgList , 13 ); + $bgdSelect=""; + foreach( $randomKeys as $key ){ + $file = $bgdImgList[ $key ]; + if( strpos( "$file", ".") != 0 ){ + $key = substr( $file, 11, -4 ); + // $bgdArr[$key] =""; + } + + +?> + + + + + + + Make your own flyer + + + + +
+ +
+
+ +
+
+
+ +

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 literal 0 HcmV?d00001 diff --git a/public/batman_small.png b/public/batman_small.png new file mode 100644 index 0000000000000000000000000000000000000000..90e4cc06426ff188765247a24693a660d0873e1d GIT binary patch literal 3442 zcmd^?{Xf(D8^`tSoH{wC#GKq`D4D});zTI+*+x?4W+FE^$POY+rjd0cW|%XHbfi=wG)Hs){ao8f|l9K-CahrYP|fn+Oy@EUXaqY*^73g^;_|O)o{tqCGGxX1?EH@Raz6E?p=`R zkl*b&5wJy=PpSp3rj{lSq`}0|8Ux->w4sYLqsdrYWZ7!B*}WqBe#xlOk@EL7)n=2v zhzBlEfOo=6t3Xf76KU zt+CeBp63T0_A!#+h=~#}zOl{)Csp2^^9K>>IVRiM@YHlySF&@}?-3rZ6Bli0i7;aw z2nnXyF_z<4;qHcxnu+)=poku=x>pO4FD!>nEE%=QDqnbAmHgIGZdYt)i(zii7+M&b zY^V5jj%lJPgJRQrrlqxYA1weO58pem$h_>+{MX0|`TX%FBLqnoIpA;BDPC_T_uGVC z?7yHl?U<94$2j=)hegGYC2Q|a2^KmsMV;v%2g6tTxQ%xWM7`X!k+6lqx*PWtS>CCN zcTrm#6&)?XnWDe=7nU57h6qWDyUuFNn~53UpRT@q{i7Q=+v|CD~L7l%wbPBDUQ4WbC~<6 znKu1w{b0e`k^C{;k4amZo#Af4oeWWjZUbo((;JXlpDv?5Z62G{K34%1-469AE(L9L zcZwQfZaoBZghO7heq;Uo^3U~f@rAZlf2RUXcD zs$;mD1Qz%hMc+v*bp;o_Lwxs9&(&Pf7*g>o8S_r;+Fh3m<`$_@ARu zMk{3Xa|+AiWK zfMb<28L`lxeNeT&hqWQGtP$Pbj^q<4o*uQiu2QNQ*#G0L$6#08QzBBb$0g{}rgqjL zpW%}WUm=$5`L$pJSFrkK4s)Q9ah!c}>GR$ynSC?Exivq=#Cftal~fmnpih2pVI{yZbuxNmxQ+8-9(_}50&9;@hL^gddift=#qW5`G zu$^=YUB*ZB`JFt<3c8k{t7i z=mdN^VK-z0dXQ7F00CG~?zjTZPpl+fH0kBVoSETc0Ib0d*0pkf_Ffe#adr3kCssz- zLP3p)!76;c;lE>TyqLg4(oN_{jrTM_9o1qIn2-MC^~vqqia5*^$L)dfixtO!yi6>h zki4BQmQ=kN%S|Y8Ax`UcEW3|9P*m~w|M;|w2}GsWWM2g9Hez@}X1ZJ;s^a&zFAn$nC@`@-;W4UUf*a!LFRHI;oYbnIo?k|OtQ(@>PeB{2l3UVO$c zpV2T1wPq7!@}(kO$3mh)IPrkjQ&8vV3b$6OD~&otb{Xd`10f;;Y@j;5At~w#Q*m zGQtlN#N^Hojapt~t(l7x3DPPdl>hQoQI;JCT=*7?ZiP2Q6n$7i=;=)mJqYSxC=^wG z4>S<)uNCgu5**%lO4nU)LLPvP$%Iq)OaIQ~Jbp3Y_^8-P6Wv2V#JBE3KcLmkR{K}`Nr9z?v} z`c$F|QIK2*Lb3YIf0^`-J%`6`P0NmGTaQ>8KH2jM0t!MVg&PK&tq@z#J#P#sLvi@C zr-9Adtmn4P8zE_qWn32YBm-9lK7lkjYV%w$;0TP2JUhKvG{eOEqki5{HOHl$p#$@wt7vfRa@ef!_kZ8noEc+V7e`rQC zCrIO4h4cGw+&mT7&iAb_xt$6hz+p>+D{*+YPO)(98kW7EI`Q`PtvUu79(XkEMN$2u z$k7aE-<~| zZ`7`b&9(eIM#&4jhG;0guvHS-Wv;#LrnuXi)0cmUQGPW|otX~TF4jb3R7RK@>$uAb zF(x|IZm+}K&^vV>mAA=|u1N?TcYFkzao^~$$qgYbCx>YyNak)=o(1>v){+*a{J*o_ zDrYZF7rvh0vU8$Pbk?xf)pf5Stx!+h7{gc}!Pon_WC#P4>Y>z%5qz;=HxdtE_7jxI z${F3|W<6BM>!n{+O{zVn`NvhODb|N=`?Lybm=>Dji-L<$o<6CmzQ(0}eP04BrAOY> z*EOvbovQv#e5IhWv~DA{R-+(occl5aLJ$`JouBGBSn#nM8o 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.fontPaths = { }; +fabric.iMatrix = [1, 0, 0, 1, 0, 0]; + +/** + * 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 {Array[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 {Array[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 {Array[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 {Array[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(//g, '>'); + } + + /** + * Divide a string in the user perceived single units + * @memberOf fabric.util.string + * @param {String} textstring String to escape + * @return {Array} array containing the graphemes + */ + function graphemeSplit(textstring) { + var i = 0, chr, graphemes = []; + for (i = 0, chr; i < textstring.length; i++) { + if ((chr = getWholeChar(textstring, i)) === false) { + continue; + } + graphemes.push(chr); + } + return graphemes; + } + + // taken from mdn in the charAt doc page. + function getWholeChar(str, i) { + var code = str.charCodeAt(i); + + if (isNaN(code)) { + return ''; // Position not found + } + if (code < 0xD800 || code > 0xDFFF) { + return str.charAt(i); + } + + // High surrogate (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 <= code && code <= 0xDBFF) { + if (str.length <= (i + 1)) { + throw 'High surrogate without following low surrogate'; + } + var next = str.charCodeAt(i + 1); + if (0xDC00 > next || next > 0xDFFF) { + throw 'High surrogate without following low surrogate'; + } + return str.charAt(i) + str.charAt(i + 1); + } + // Low surrogate (0xDC00 <= code && code <= 0xDFFF) + if (i === 0) { + throw 'Low surrogate without preceding high surrogate'; + } + var prev = str.charCodeAt(i - 1); + + // (could change last hex to 0xDB7F to treat high private + // surrogates as single characters) + if (0xD800 > prev || prev > 0xDBFF) { + throw 'Low surrogate without preceding high surrogate'; + } + // We can pass over low surrogates now as the second component + // in a pair which we have already processed + return false; + } + + + /** + * String utilities + * @namespace fabric.util.string + */ + fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml, + graphemeSplit: graphemeSplit + }; +})(); +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }, + + IS_DONTENUM_BUGGY = (function() { + for (var p in { toString: 1 }) { + if (p === 'toString') { + return false; + } + } + return true; + })(), + + /** @ignore */ + addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && + typeof klass.prototype[property] === 'function' && + (source[property] + '').indexOf('callSuper') > -1) { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + function callSuper(methodName) { + var parentMethod = null, + _this = this; + + // climb prototype chain to find method not equal to callee's method + while (_this.constructor.superclass) { + var superClassMethod = _this.constructor.superclass.prototype[methodName]; + if (_this[methodName] !== superClassMethod) { + parentMethod = superClassMethod; + break; + } + // eslint-disable-next-line + _this = _this.constructor.superclass.prototype; + } + + if (!parentMethod) { + return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); + } + + return (arguments.length > 1) + ? parentMethod.apply(this, slice.call(arguments, 1)) + : parentMethod.call(this); + } + + /** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param {Function} [parent] optional "Class" to inherit from + * @param {Object} [properties] Properties shared by all instances of this class + * (be careful modifying objects defined here as this would affect all instances) + */ + function createClass() { + var parent = null, + properties = slice.call(arguments, 0); + + if (typeof properties[0] === 'function') { + parent = properties.shift(); + } + function klass() { + this.initialize.apply(this, arguments); + } + + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + Subclass.prototype = parent.prototype; + klass.prototype = new Subclass(); + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + klass.prototype.callSuper = callSuper; + return klass; + } + + fabric.util.createClass = createClass; +})(); +(function () { + // since ie10 or ie9 can use addEventListener but they do not support options, i need to check + var couldUseAttachEvent = !!fabric.document.createElement('div').attachEvent; + /** + * Adds an event listener to an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = function(element, eventName, handler, options) { + element && element.addEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + /** + * Removes an event listener from an element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = function(element, eventName, handler, options) { + element && element.removeEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; + + function getTouchInfo(event) { + var touchProp = event.changedTouches; + if (touchProp && touchProp[0]) { + return touchProp[0]; + } + return event; + } + + fabric.util.getPointer = function(event) { + var element = event.target, + scroll = fabric.util.getScrollLeftTop(element), + _evt = getTouchInfo(event); + return { + x: _evt.clientX + scroll.left, + y: _evt.clientY + scroll.top + }; + }; +})(); +(function () { + + /** + * Cross-browser wrapper for setting element's style + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {Object} styles + * @return {HTMLElement} Element that was passed as a first argument + */ + function setStyle(element, styles) { + var elementStyle = element.style; + if (!elementStyle) { + return element; + } + if (typeof styles === 'string') { + element.style.cssText += ';' + styles; + return styles.indexOf('opacity') > -1 + ? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) + : element; + } + for (var property in styles) { + if (property === 'opacity') { + setOpacity(element, styles[property]); + } + else { + var normalizedProperty = (property === 'float' || property === 'cssFloat') + ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') + : property; + elementStyle[normalizedProperty] = styles[property]; + } + } + return element; + } + + var parseEl = fabric.document.createElement('div'), + supportsOpacity = typeof parseEl.style.opacity === 'string', + supportsFilters = typeof parseEl.style.filter === 'string', + reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, + + /** @ignore */ + setOpacity = function (element) { return element; }; + + if (supportsOpacity) { + /** @ignore */ + setOpacity = function(element, value) { + element.style.opacity = value; + return element; + }; + } + else if (supportsFilters) { + /** @ignore */ + setOpacity = function(element, value) { + var es = element.style; + if (element.currentStyle && !element.currentStyle.hasLayout) { + es.zoom = 1; + } + if (reOpacity.test(es.filter)) { + value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')'); + es.filter = es.filter.replace(reOpacity, value); + } + else { + es.filter += ' alpha(opacity=' + (value * 100) + ')'; + } + return element; + }; + } + + fabric.util.setStyle = setStyle; + +})(); +(function() { + + var _slice = Array.prototype.slice; + + /** + * Takes id and returns an element with that id (if one exists in a document) + * @memberOf fabric.util + * @param {String|HTMLElement} id + * @return {HTMLElement|null} + */ + function getById(id) { + return typeof id === 'string' ? fabric.document.getElementById(id) : id; + } + + var sliceCanConvertNodelists, + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + try { + sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch (err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.document.createElement(tagName); + for (var prop in attributes) { + if (prop === 'class') { + el.className = attributes[prop]; + } + else if (prop === 'for') { + el.htmlFor = attributes[prop]; + } + else { + el.setAttribute(prop, attributes[prop]); + } + } + return el; + } + + /** + * Adds class to an element + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if (element && (' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + function wrapElement(element, wrapper, attributes) { + if (typeof wrapper === 'string') { + wrapper = makeElement(wrapper, attributes); + } + if (element.parentNode) { + element.parentNode.replaceChild(wrapper, element); + } + wrapper.appendChild(element); + return wrapper; + } + + /** + * Returns element scroll offsets + * @memberOf fabric.util + * @param {HTMLElement} element Element to operate on + * @return {Object} Object with left/top values + */ + function getScrollLeftTop(element) { + + var left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + // While loop checks (and then sets element to) .parentNode OR .host + // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, + // but the .parentNode of a root ShadowDOM node will always be null, instead + // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 + while (element && (element.parentNode || element.host)) { + + // Set element to element parent, or 'host' in case of ShadowDOM + element = element.parentNode || element.host; + + if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; + } + + if (element.nodeType === 1 && element.style.position === 'fixed') { + break; + } + } + + return { left: left, top: top }; + } + + /** + * Returns offset for a given element + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ + function getElementOffset(element) { + var docElem, + doc = element && element.ownerDocument, + box = { left: 0, top: 0 }, + offset = { left: 0, top: 0 }, + scrollLeftTop, + offsetAttributes = { + borderLeftWidth: 'left', + borderTopWidth: 'top', + paddingLeft: 'left', + paddingTop: 'top' + }; + + if (!doc) { + return offset; + } + + for (var attr in offsetAttributes) { + offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; + } + + docElem = doc.documentElement; + if ( typeof element.getBoundingClientRect !== 'undefined' ) { + box = element.getBoundingClientRect(); + } + + scrollLeftTop = getScrollLeftTop(element); + + return { + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top + }; + } + + /** + * Returns style attribute value of a given element + * @memberOf fabric.util + * @param {HTMLElement} element Element to get style attribute for + * @param {String} attr Style attribute to get for element + * @return {String} Style attribute value of the given element. + */ + var getElementStyle; + if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { + getElementStyle = function(element, attr) { + var style = fabric.document.defaultView.getComputedStyle(element, null); + return style ? style[attr] : undefined; + }; + } + else { + getElementStyle = function(element, attr) { + var value = element.style[attr]; + if (!value && element.currentStyle) { + value = element.currentStyle[attr]; + } + return value; + }; + } + + (function () { + var style = fabric.document.documentElement.style, + selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementUnselectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = fabric.util.falseFunction; + } + if (selectProp) { + element.style[selectProp] = 'none'; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = 'on'; + } + return element; + } + + /** + * Makes element selectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + (function() { + + /** + * Inserts a script element with a given url into a document; invokes callback, when that script is finished loading + * @memberOf fabric.util + * @param {String} url URL of a script to load + * @param {Function} callback Callback to execute when script is finished loading + */ + function getScript(url, callback) { + var headEl = fabric.document.getElementsByTagName('head')[0], + scriptEl = fabric.document.createElement('script'), + loading = true; + + /** @ignore */ + scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) { + if (loading) { + if (typeof this.readyState === 'string' && + this.readyState !== 'loaded' && + this.readyState !== 'complete') { + return; + } + loading = false; + callback(e || fabric.window.event); + scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null; + } + }; + scriptEl.src = url; + headEl.appendChild(scriptEl); + // causes issue in Opera + // headEl.removeChild(scriptEl); + } + + fabric.util.getScript = getScript; + })(); + + function getNodeCanvas(element) { + var impl = fabric.jsdomImplForWrapper(element); + return impl._canvas || impl._image; + }; + + function cleanUpJsdomNode(element) { + if (!fabric.isLikelyNode) { + return; + } + var impl = fabric.jsdomImplForWrapper(element); + if (impl) { + impl._image = null; + impl._canvas = null; + // unsure if necessary + impl._currentSrc = null; + impl._attributes = null; + impl._classList = null; + } + } + + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.makeElement = makeElement; + fabric.util.addClass = addClass; + fabric.util.wrapElement = wrapElement; + fabric.util.getScrollLeftTop = getScrollLeftTop; + fabric.util.getElementOffset = getElementOffset; + fabric.util.getElementStyle = getElementStyle; + fabric.util.getNodeCanvas = getNodeCanvas; + fabric.util.cleanUpJsdomNode = cleanUpJsdomNode; + +})(); +(function() { + + function addParamToUrl(url, param) { + return url + (/\?/.test(url) ? '&' : '?') + param; + } + + function emptyFn() { } + + /** + * Cross-browser abstraction for sending XMLHttpRequest + * @memberOf fabric.util + * @param {String} url URL to send XMLHttpRequest to + * @param {Object} [options] Options object + * @param {String} [options.method="GET"] + * @param {String} [options.parameters] parameters to append to url in GET or in body + * @param {String} [options.body] body to send with POST or PUT request + * @param {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + xhr = new fabric.window.XMLHttpRequest(), + body = options.body || options.parameters; + + /** @ignore */ + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + onComplete(xhr); + xhr.onreadystatechange = emptyFn; + } + }; + + if (method === 'GET') { + body = null; + if (typeof options.parameters === 'string') { + url = addParamToUrl(url, options.parameters); + } + } + + xhr.open(method, url, true); + + if (method === 'POST' || method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + xhr.send(body); + return xhr; + } + + fabric.util.request = request; +})(); +/** + * Wrapper around `console.log` (when available) + * @param {*} [values] Values to log + */ +fabric.log = function() { }; + +/** + * Wrapper around `console.warn` (when available) + * @param {*} [values] Values to log as a warning + */ +fabric.warn = function() { }; + +/* eslint-disable */ +if (typeof console !== 'undefined') { + + ['log', 'warn'].forEach(function(methodName) { + + if (typeof console[methodName] !== 'undefined' && + typeof console[methodName].apply === 'function') { + + fabric[methodName] = function() { + return console[methodName].apply(console, arguments); + }; + } + }); +} +/* eslint-enable */ +(function() { + + function noop() { + return false; + } + + function defaultEasing(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change (in ms) + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + */ + function animate(options) { + + requestAnimFrame(function(timestamp) { + options || (options = { }); + + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || noop, + abort = options.abort || noop, + onComplete = options.onComplete || noop, + easing = options.easing || defaultEasing, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; + + options.onStart && options.onStart(); + + (function tick(ticktime) { + // TODO: move abort call after calculation + // and pass (current,valuePerc, timePerc) as arguments + time = ticktime || +new Date(); + var currentTime = time > finish ? duration : (time - start), + timePerc = currentTime / duration, + current = easing(currentTime, startValue, byValue, duration), + valuePerc = Math.abs((current - startValue) / byValue); + if (abort()) { + onComplete(endValue, 1, 1); + return; + } + if (time > finish) { + onChange(endValue, 1, 1); + onComplete(endValue, 1, 1); + return; + } + else { + onChange(current, valuePerc, timePerc); + requestAnimFrame(tick); + } + })(start); + }); + } + + var _requestAnimFrame = fabric.window.requestAnimationFrame || + fabric.window.webkitRequestAnimationFrame || + fabric.window.mozRequestAnimationFrame || + fabric.window.oRequestAnimationFrame || + fabric.window.msRequestAnimationFrame || + function(callback) { + return fabric.window.setTimeout(callback, 1000 / 60); + }; + + var _cancelAnimFrame = fabric.window.cancelAnimationFrame || fabric.window.clearTimeout; + + /** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + function requestAnimFrame() { + return _requestAnimFrame.apply(fabric.window, arguments); + } + + function cancelAnimFrame() { + return _cancelAnimFrame.apply(fabric.window, arguments); + } + + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + fabric.util.cancelAnimFrame = cancelAnimFrame; +})(); +(function() { + // Calculate an in-between color. Returns a "rgba()" string. + // Credit: Edwin Martin + // http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js + function calculateColor(begin, end, pos) { + var color = 'rgba(' + + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' + + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' + + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); + + color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); + color += ')'; + return color; + } + + /** + * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {String} fromColor The starting color in hex or rgb(a) format. + * @param {String} toColor The starting color in hex or rgb(a) format. + * @param {Number} [duration] Duration of change (in ms). + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. + * @param {Function} [options.abort] Additional function with logic. If returns true, onComplete is called. + */ + function animateColor(fromColor, toColor, duration, options) { + var startColor = new fabric.Color(fromColor).getSource(), + endColor = new fabric.Color(toColor).getSource(); + + options = options || {}; + + fabric.util.animate(fabric.util.object.extend(options, { + duration: duration || 500, + startValue: startColor, + endValue: endColor, + byValue: endColor, + easing: function (currentTime, startValue, byValue, duration) { + var posValue = options.colorEasing + ? options.colorEasing(currentTime, duration) + : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); + return calculateColor(startValue, byValue, posValue); + } + })); + } + + fabric.util.animateColor = animateColor; + +})(); +(function() { + + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + //handle the 0/0 case: + if (c === 0 && a === 0) { + s = p / (2 * Math.PI) * Math.asin(1); + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + } + return { a: a, c: c, p: p, s: s }; + } + + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); + } + + /** + * Cubic easing out + * @memberOf fabric.util.ease + */ + function easeOutCubic(t, b, c, d) { + return c * ((t = t / d - 1) * t * t + 1) + b; + } + + /** + * Cubic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCubic(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t + 2) + b; + } + + /** + * Quartic easing in + * @memberOf fabric.util.ease + */ + function easeInQuart(t, b, c, d) { + return c * (t /= d) * t * t * t + b; + } + + /** + * Quartic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuart(t, b, c, d) { + return -c * ((t = t / d - 1) * t * t * t - 1) + b; + } + + /** + * Quartic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuart(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t + b; + } + return -c / 2 * ((t -= 2) * t * t * t - 2) + b; + } + + /** + * Quintic easing in + * @memberOf fabric.util.ease + */ + function easeInQuint(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + } + + /** + * Quintic easing out + * @memberOf fabric.util.ease + */ + function easeOutQuint(t, b, c, d) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + } + + /** + * Quintic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutQuint(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return c / 2 * t * t * t * t * t + b; + } + return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + } + + /** + * Sinusoidal easing in + * @memberOf fabric.util.ease + */ + function easeInSine(t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + } + + /** + * Sinusoidal easing out + * @memberOf fabric.util.ease + */ + function easeOutSine(t, b, c, d) { + return c * Math.sin(t / d * (Math.PI / 2)) + b; + } + + /** + * Sinusoidal easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutSine(t, b, c, d) { + return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + } + + /** + * Exponential easing in + * @memberOf fabric.util.ease + */ + function easeInExpo(t, b, c, d) { + return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; + } + + /** + * Exponential easing out + * @memberOf fabric.util.ease + */ + function easeOutExpo(t, b, c, d) { + return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; + } + + /** + * Exponential easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutExpo(t, b, c, d) { + if (t === 0) { + return b; + } + if (t === d) { + return b + c; + } + t /= d / 2; + if (t < 1) { + return c / 2 * Math.pow(2, 10 * (t - 1)) + b; + } + return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; + } + + /** + * Circular easing in + * @memberOf fabric.util.ease + */ + function easeInCirc(t, b, c, d) { + return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + } + + /** + * Circular easing out + * @memberOf fabric.util.ease + */ + function easeOutCirc(t, b, c, d) { + return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + } + + /** + * Circular easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutCirc(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; + } + return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; + } + + /** + * Elastic easing in + * @memberOf fabric.util.ease + */ + function easeInElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; + } + + /** + * Elastic easing out + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) + opts.c + b; + } + + /** + * Elastic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d / 2; + if (t === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + var opts = normalize(a, c, p, s); + if (t < 1) { + return -0.5 * elastic(opts, t, d) + b; + } + return opts.a * Math.pow(2, -10 * (t -= 1)) * + Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) * 0.5 + opts.c + b; + } + + /** + * Backwards easing in + * @memberOf fabric.util.ease + */ + function easeInBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * (t /= d) * t * ((s + 1) * t - s) + b; + } + + /** + * Backwards easing out + * @memberOf fabric.util.ease + */ + function easeOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + } + + /** + * Backwards easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBack(t, b, c, d, s) { + if (s === undefined) { + s = 1.70158; + } + t /= d / 2; + if (t < 1) { + return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; + } + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + } + + /** + * Bouncing easing in + * @memberOf fabric.util.ease + */ + function easeInBounce(t, b, c, d) { + return c - easeOutBounce (d - t, 0, c, d) + b; + } + + /** + * Bouncing easing out + * @memberOf fabric.util.ease + */ + function easeOutBounce(t, b, c, d) { + if ((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; + } + else if (t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; + } + else if (t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; + } + else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; + } + } + + /** + * Bouncing easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutBounce(t, b, c, d) { + if (t < d / 2) { + return easeInBounce (t * 2, 0, c, d) * 0.5 + b; + } + return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + } + + /** + * Easing functions + * See Easing Equations by Robert Penner + * @namespace fabric.util.ease + */ + fabric.util.ease = { + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c * (t /= d) * t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(t, b, c, d) { + t /= (d / 2); + if (t < 1) { + return c / 2 * t * t + b; + } + return -c / 2 * ((--t) * (t - 2) - 1) + b; + }, + + /** + * Cubic easing in + * @memberOf fabric.util.ease + */ + easeInCubic: function(t, b, c, d) { + return c * (t /= d) * t * t + b; + }, + + easeOutCubic: easeOutCubic, + easeInOutCubic: easeInOutCubic, + easeInQuart: easeInQuart, + easeOutQuart: easeOutQuart, + easeInOutQuart: easeInOutQuart, + easeInQuint: easeInQuint, + easeOutQuint: easeOutQuint, + easeInOutQuint: easeInOutQuint, + easeInSine: easeInSine, + easeOutSine: easeOutSine, + easeInOutSine: easeInOutSine, + easeInExpo: easeInExpo, + easeOutExpo: easeOutExpo, + easeInOutExpo: easeInOutExpo, + easeInCirc: easeInCirc, + easeOutCirc: easeOutCirc, + easeInOutCirc: easeInOutCirc, + easeInElastic: easeInElastic, + easeOutElastic: easeOutElastic, + easeInOutElastic: easeInOutElastic, + easeInBack: easeInBack, + easeOutBack: easeOutBack, + easeInOutBack: easeInOutBack, + easeInBounce: easeInBounce, + easeOutBounce: easeOutBounce, + easeInOutBounce: easeInOutBounce + }; + +})(); +(function(global) { + + 'use strict'; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + parseUnit = fabric.util.parseUnit, + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, + + svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'rect', 'line', + 'image', 'text'], + svgViewBoxElements = ['symbol', 'image', 'marker', 'pattern', 'view', 'svg'], + svgInvalidAncestors = ['pattern', 'defs', 'symbol', 'metadata', 'clipPath', 'mask', 'desc'], + svgValidParents = ['symbol', 'g', 'a', 'svg', 'clipPath', 'defs'], + + attributesMap = { + cx: 'left', + x: 'left', + r: 'radius', + cy: 'top', + y: 'top', + display: 'visible', + visibility: 'visible', + transform: 'transformMatrix', + 'fill-opacity': 'fillOpacity', + 'fill-rule': 'fillRule', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'font-style': 'fontStyle', + 'font-weight': 'fontWeight', + 'letter-spacing': 'charSpacing', + 'paint-order': 'paintFirst', + 'stroke-dasharray': 'strokeDashArray', + 'stroke-dashoffset': 'strokeDashOffset', + 'stroke-linecap': 'strokeLineCap', + 'stroke-linejoin': 'strokeLineJoin', + 'stroke-miterlimit': 'strokeMiterLimit', + 'stroke-opacity': 'strokeOpacity', + 'stroke-width': 'strokeWidth', + 'text-decoration': 'textDecoration', + 'text-anchor': 'textAnchor', + opacity: 'opacity', + 'clip-path': 'clipPath', + 'clip-rule': 'clipRule', + 'vector-effect': 'strokeUniform' + }, + + colorAttributes = { + stroke: 'strokeOpacity', + fill: 'fillOpacity' + }; + + fabric.svgValidTagNamesRegEx = getSvgRegex(svgValidTagNames); + fabric.svgViewBoxElementsRegEx = getSvgRegex(svgViewBoxElements); + fabric.svgInvalidAncestorsRegEx = getSvgRegex(svgInvalidAncestors); + fabric.svgValidParentsRegEx = getSvgRegex(svgValidParents); + + fabric.cssRules = { }; + fabric.gradientDefs = { }; + fabric.clipPaths = { }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + function normalizeValue(attr, value, parentAttributes, fontSize) { + var isArray = Object.prototype.toString.call(value) === '[object Array]', + parsed; + + if ((attr === 'fill' || attr === 'stroke') && value === 'none') { + value = ''; + } + else if (attr === 'vector-effect') { + value = value === 'non-scaling-stroke'; + } + else if (attr === 'strokeDashArray') { + if (value === 'none') { + value = null; + } + else { + value = value.replace(/,/g, ' ').split(/\s+/).map(parseFloat); + } + } + else if (attr === 'transformMatrix') { + if (parentAttributes && parentAttributes.transformMatrix) { + value = multiplyTransformMatrices( + parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); + } + else { + value = fabric.parseTransformAttribute(value); + } + } + else if (attr === 'visible') { + value = value !== 'none' && value !== 'hidden'; + // display=none on parent element always takes precedence over child element + if (parentAttributes && parentAttributes.visible === false) { + value = false; + } + } + else if (attr === 'opacity') { + value = parseFloat(value); + if (parentAttributes && typeof parentAttributes.opacity !== 'undefined') { + value *= parentAttributes.opacity; + } + } + else if (attr === 'textAnchor' /* text-anchor */) { + value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; + } + else if (attr === 'charSpacing') { + // parseUnit returns px and we convert it to em + parsed = parseUnit(value, fontSize) / fontSize * 1000; + } + else if (attr === 'paintFirst') { + var fillIndex = value.indexOf('fill'); + var strokeIndex = value.indexOf('stroke'); + var value = 'fill'; + if (fillIndex > -1 && strokeIndex > -1 && strokeIndex < fillIndex) { + value = 'stroke'; + } + else if (fillIndex === -1 && strokeIndex > -1) { + value = 'stroke'; + } + } + else if (attr === 'href' || attr === 'xlink:href') { + return value; + } + else { + parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); + } + + return (!isArray && isNaN(parsed) ? value : parsed); + } + + /** + * @private + */ + function getSvgRegex(arr) { + return new RegExp('^(' + arr.join('|') + ')\\b', 'i'); + } + + /** + * @private + * @param {Object} attributes Array of attributes to parse + */ + function _setStrokeFillOpacity(attributes) { + for (var attr in colorAttributes) { + + if (typeof attributes[colorAttributes[attr]] === 'undefined' || attributes[attr] === '') { + continue; + } + + if (typeof attributes[attr] === 'undefined') { + if (!fabric.Object.prototype[attr]) { + continue; + } + attributes[attr] = fabric.Object.prototype[attr]; + } + + if (attributes[attr].indexOf('url(') === 0) { + continue; + } + + var color = new fabric.Color(attributes[attr]); + attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); + } + return attributes; + } + + /** + * @private + */ + function _getMultipleNodes(doc, nodeNames) { + var nodeName, nodeArray = [], nodeList, i, len; + for (i = 0, len = nodeNames.length; i < len; i++) { + nodeName = nodeNames[i]; + nodeList = doc.getElementsByTagName(nodeName); + nodeArray = nodeArray.concat(Array.prototype.slice.call(nodeList)); + } + return nodeArray; + } + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix + */ + fabric.parseTransformAttribute = (function() { + function rotateMatrix(matrix, args) { + var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]), + x = 0, y = 0; + if (args.length === 3) { + x = args[1]; + y = args[2]; + } + + matrix[0] = cos; + matrix[1] = sin; + matrix[2] = -sin; + matrix[3] = cos; + matrix[4] = x - (cos * x - sin * y); + matrix[5] = y - (sin * x + cos * y); + } + + function scaleMatrix(matrix, args) { + var multiplierX = args[0], + multiplierY = (args.length === 2) ? args[1] : args[0]; + + matrix[0] = multiplierX; + matrix[3] = multiplierY; + } + + function skewMatrix(matrix, args, pos) { + matrix[pos] = Math.tan(fabric.util.degreesToRadians(args[0])); + } + + function translateMatrix(matrix, args) { + matrix[4] = args[0]; + if (args.length === 2) { + matrix[5] = args[1]; + } + } + + // identity matrix + var iMatrix = fabric.iMatrix, + + // == begin transform regexp + number = fabric.reNum, + + commaWsp = '(?:\\s+,?\\s*|,\\s*)', + + skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + ')' + + commaWsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + matrix = '(?:(matrix)\\s*\\(\\s*' + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + + '\\s*\\))', + + transform = '(?:' + + matrix + '|' + + translate + '|' + + scale + '|' + + rotate + '|' + + skewX + '|' + + skewY + + ')', + + transforms = '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', + + transformList = '^\\s*(?:' + transforms + '?)\\s*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transformList), + // == end transform regexp + + reTransform = new RegExp(transform, 'g'); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(), + matrices = []; + + // return if no argument was given or + // an argument does not match transform attribute regexp + if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { + return matrix; + } + + attributeValue.replace(reTransform, function(match) { + + var m = new RegExp(transform).exec(match).filter(function (match) { + // match !== '' && match != null + return (!!match); + }), + operation = m[1], + args = m.slice(2).map(parseFloat); + + switch (operation) { + case 'translate': + translateMatrix(matrix, args); + break; + case 'rotate': + args[0] = fabric.util.degreesToRadians(args[0]); + rotateMatrix(matrix, args); + break; + case 'scale': + scaleMatrix(matrix, args); + break; + case 'skewX': + skewMatrix(matrix, args, 2); + break; + case 'skewY': + skewMatrix(matrix, args, 1); + break; + case 'matrix': + matrix = args; + break; + } + + // snapshot current matrix into matrices array + matrices.push(matrix.concat()); + // reset + matrix = iMatrix.concat(); + }); + + var combinedMatrix = matrices[0]; + while (matrices.length > 1) { + matrices.shift(); + combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); + } + return combinedMatrix; + }; + })(); + + /** + * @private + */ + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;\s*$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); + + attr = pair[0].trim().toLowerCase(); + value = pair[1].trim(); + + oStyle[attr] = value; + }); + } + + /** + * @private + */ + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') { + continue; + } + + attr = prop.toLowerCase(); + value = style[prop]; + + oStyle[attr] = value; + } + } + + /** + * @private + */ + function getGlobalStylesForElement(element, svgUid) { + var styles = { }; + for (var rule in fabric.cssRules[svgUid]) { + if (elementMatchesRule(element, rule.split(' '))) { + for (var property in fabric.cssRules[svgUid][rule]) { + styles[property] = fabric.cssRules[svgUid][rule][property]; + } + } + } + return styles; + } + + /** + * @private + */ + function elementMatchesRule(element, selectors) { + var firstMatching, parentMatching = true; + //start from rightmost selector. + firstMatching = selectorMatches(element, selectors.pop()); + if (firstMatching && selectors.length) { + parentMatching = doesSomeParentMatch(element, selectors); + } + return firstMatching && parentMatching && (selectors.length === 0); + } + + function doesSomeParentMatch(element, selectors) { + var selector, parentMatching = true; + while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { + if (parentMatching) { + selector = selectors.pop(); + } + element = element.parentNode; + parentMatching = selectorMatches(element, selector); + } + return selectors.length === 0; + } + + /** + * @private + */ + function selectorMatches(element, selector) { + var nodeName = element.nodeName, + classNames = element.getAttribute('class'), + id = element.getAttribute('id'), matcher, i; + // i check if a selector matches slicing away part from it. + // if i get empty string i should match + matcher = new RegExp('^' + nodeName, 'i'); + selector = selector.replace(matcher, ''); + if (id && selector.length) { + matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + if (classNames && selector.length) { + classNames = classNames.split(' '); + for (i = classNames.length; i--;) { + matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + } + return selector.length === 0; + } + + /** + * @private + * to support IE8 missing getElementById on SVGdocument and on node xmlDOM + */ + function elementById(doc, id) { + var el; + doc.getElementById && (el = doc.getElementById(id)); + if (el) { + return el; + } + var node, i, len, nodelist = doc.getElementsByTagName('*'); + for (i = 0, len = nodelist.length; i < len; i++) { + node = nodelist[i]; + if (id === node.getAttribute('id')) { + return node; + } + } + } + + /** + * @private + */ + function parseUseDirectives(doc) { + var nodelist = _getMultipleNodes(doc, ['use', 'svg:use']), i = 0; + while (nodelist.length && i < nodelist.length) { + var el = nodelist[i], + xlink = (el.getAttribute('xlink:href') || el.getAttribute('href')).substr(1), + x = el.getAttribute('x') || 0, + y = el.getAttribute('y') || 0, + el2 = elementById(doc, xlink).cloneNode(true), + currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', + parentNode, oldLength = nodelist.length, attr, j, attrs, len; + + applyViewboxTransform(el2); + if (/^svg$/i.test(el2.nodeName)) { + var el3 = el2.ownerDocument.createElement('g'); + for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + el3.setAttribute(attr.nodeName, attr.nodeValue); + } + // el2.firstChild != null + while (el2.firstChild) { + el3.appendChild(el2.firstChild); + } + el2 = el3; + } + + for (j = 0, attrs = el.attributes, len = attrs.length; j < len; j++) { + attr = attrs.item(j); + if (attr.nodeName === 'x' || attr.nodeName === 'y' || + attr.nodeName === 'xlink:href' || attr.nodeName === 'href') { + continue; + } + + if (attr.nodeName === 'transform') { + currentTrans = attr.nodeValue + ' ' + currentTrans; + } + else { + el2.setAttribute(attr.nodeName, attr.nodeValue); + } + } + + el2.setAttribute('transform', currentTrans); + el2.setAttribute('instantiated_by_use', '1'); + el2.removeAttribute('id'); + parentNode = el.parentNode; + parentNode.replaceChild(el2, el); + // some browsers do not shorten nodelist after replaceChild (IE8) + if (nodelist.length === oldLength) { + i++; + } + } + } + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // matches, e.g.: +14.56e-12, etc. + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*' + + '$' + ); + + /** + * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements + */ + function applyViewboxTransform(element) { + + var viewBoxAttr = element.getAttribute('viewBox'), + scaleX = 1, + scaleY = 1, + minX = 0, + minY = 0, + viewBoxWidth, viewBoxHeight, matrix, el, + widthAttr = element.getAttribute('width'), + heightAttr = element.getAttribute('height'), + x = element.getAttribute('x') || 0, + y = element.getAttribute('y') || 0, + preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', + missingViewBox = (!viewBoxAttr || !fabric.svgViewBoxElementsRegEx.test(element.nodeName) + || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), + missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), + toBeParsed = missingViewBox && missingDimAttr, + parsedDim = { }, translateMatrix = '', widthDiff = 0, heightDiff = 0; + + parsedDim.width = 0; + parsedDim.height = 0; + parsedDim.toBeParsed = toBeParsed; + + if (toBeParsed) { + return parsedDim; + } + + if (missingViewBox) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + return parsedDim; + } + minX = -parseFloat(viewBoxAttr[1]); + minY = -parseFloat(viewBoxAttr[2]); + viewBoxWidth = parseFloat(viewBoxAttr[3]); + viewBoxHeight = parseFloat(viewBoxAttr[4]); + parsedDim.minX = minX; + parsedDim.minY = minY; + parsedDim.viewBoxWidth = viewBoxWidth; + parsedDim.viewBoxHeight = viewBoxHeight; + if (!missingDimAttr) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + scaleX = parsedDim.width / viewBoxWidth; + scaleY = parsedDim.height / viewBoxHeight; + } + else { + parsedDim.width = viewBoxWidth; + parsedDim.height = viewBoxHeight; + } + + // default is to preserve aspect ratio + preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); + if (preserveAspectRatio.alignX !== 'none') { + //translate all container for the effect of Mid, Min, Max + if (preserveAspectRatio.meetOrSlice === 'meet') { + scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); + // calculate additional translation to move the viewbox + } + if (preserveAspectRatio.meetOrSlice === 'slice') { + scaleY = scaleX = (scaleX > scaleY ? scaleX : scaleY); + // calculate additional translation to move the viewbox + } + widthDiff = parsedDim.width - viewBoxWidth * scaleX; + heightDiff = parsedDim.height - viewBoxHeight * scaleX; + if (preserveAspectRatio.alignX === 'Mid') { + widthDiff /= 2; + } + if (preserveAspectRatio.alignY === 'Mid') { + heightDiff /= 2; + } + if (preserveAspectRatio.alignX === 'Min') { + widthDiff = 0; + } + if (preserveAspectRatio.alignY === 'Min') { + heightDiff = 0; + } + } + + if (scaleX === 1 && scaleY === 1 && minX === 0 && minY === 0 && x === 0 && y === 0) { + return parsedDim; + } + + if (x || y) { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + } + + matrix = translateMatrix + ' matrix(' + scaleX + + ' 0' + + ' 0 ' + + scaleY + ' ' + + (minX * scaleX + widthDiff) + ' ' + + (minY * scaleY + heightDiff) + ') '; + parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix); + if (element.nodeName === 'svg') { + el = element.ownerDocument.createElement('g'); + // element.firstChild != null + while (element.firstChild) { + el.appendChild(element.firstChild); + } + element.appendChild(el); + } + else { + el = element; + matrix = el.getAttribute('transform') + matrix; + } + el.setAttribute('transform', matrix); + return parsedDim; + } + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) + && !element.getAttribute('instantiated_by_use')) { + return true; + } + } + return false; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; + * It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [parsingOptions] options for parsing document + * @param {String} [parsingOptions.crossOrigin] crossOrigin settings + */ + fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) { + if (!doc) { + return; + } + + parseUseDirectives(doc); + + var svgUid = fabric.Object.__uid++, i, len, + options = applyViewboxTransform(doc), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; + options.svgUid = svgUid; + + if (descendants.length === 0 && fabric.isLikelyNode) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes('//*[name(.)!="svg"]'); + var arr = []; + for (i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + applyViewboxTransform(el); + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')) && + !hasAncestorWithNodeName(el, fabric.svgInvalidAncestorsRegEx); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + + if (!elements || (elements && !elements.length)) { + callback && callback([], {}); + return; + } + var clipPaths = { }; + descendants.filter(function(el) { + return el.nodeName.replace('svg:', '') === 'clipPath'; + }).forEach(function(el) { + var id = el.getAttribute('id'); + clipPaths[id] = fabric.util.toArray(el.getElementsByTagName('*')).filter(function(el) { + return fabric.svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', '')); + }); + }); + fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); + fabric.cssRules[svgUid] = fabric.getCSSRules(doc); + fabric.clipPaths[svgUid] = clipPaths; + // Precedence of rules: style > class > attribute + fabric.parseElements(elements, function(instances, elements) { + if (callback) { + callback(instances, options, elements, descendants); + delete fabric.gradientDefs[svgUid]; + delete fabric.cssRules[svgUid]; + delete fabric.clipPaths[svgUid]; + } + }, clone(options), reviver, parsingOptions); + }; + + function recursivelyParseGradientsXlink(doc, gradient) { + var gradientsAttrs = ['gradientTransform', 'x1', 'x2', 'y1', 'y2', 'gradientUnits', 'cx', 'cy', 'r', 'fx', 'fy'], + xlinkAttr = 'xlink:href', + xLink = gradient.getAttribute(xlinkAttr).substr(1), + referencedGradient = elementById(doc, xLink); + if (referencedGradient && referencedGradient.getAttribute(xlinkAttr)) { + recursivelyParseGradientsXlink(doc, referencedGradient); + } + gradientsAttrs.forEach(function(attr) { + if (referencedGradient && !gradient.hasAttribute(attr) && referencedGradient.hasAttribute(attr)) { + gradient.setAttribute(attr, referencedGradient.getAttribute(attr)); + } + }); + if (!gradient.children.length) { + var referenceClone = referencedGradient.cloneNode(true); + while (referenceClone.firstChild) { + gradient.appendChild(referenceClone.firstChild); + } + } + gradient.removeAttribute(xlinkAttr); + } + + var reFontDeclaration = new RegExp( + '(normal|italic)?\\s*(normal|small-caps)?\\s*' + + '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + + fabric.reNum + + '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)'); + + extend(fabric, { + /** + * Parses a short font declaration, building adding its properties to a style object + * @static + * @function + * @memberOf fabric + * @param {String} value font declaration + * @param {Object} oStyle definition + */ + parseFontDeclaration: function(value, oStyle) { + var match = value.match(reFontDeclaration); + + if (!match) { + return; + } + var fontStyle = match[1], + // font variant is not used + // fontVariant = match[2], + fontWeight = match[3], + fontSize = match[4], + lineHeight = match[5], + fontFamily = match[6]; + + if (fontStyle) { + oStyle.fontStyle = fontStyle; + } + if (fontWeight) { + oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); + } + if (fontSize) { + oStyle.fontSize = parseUnit(fontSize); + } + if (fontFamily) { + oStyle.fontFamily = fontFamily; + } + if (lineHeight) { + oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; + } + }, + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + getGradientDefs: function(doc) { + var tagArray = [ + 'linearGradient', + 'radialGradient', + 'svg:linearGradient', + 'svg:radialGradient'], + elList = _getMultipleNodes(doc, tagArray), + el, j = 0, gradientDefs = { }; + j = elList.length; + while (j--) { + el = elList[j]; + if (el.getAttribute('xlink:href')) { + recursivelyParseGradientsXlink(doc, el); + } + gradientDefs[el.getAttribute('id')] = el; + } + return gradientDefs; + }, + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + parseAttributes: function(element, attributes, svgUid) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }, + fontSize, parentFontSize; + + if (typeof svgUid === 'undefined') { + svgUid = element.getAttribute('svgUid'); + } + // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards + if (element.parentNode && fabric.svgValidParentsRegEx.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { // eslint-disable-line + memo[attr] = value; + } + return memo; + }, { }); + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + ownAttributes = extend(ownAttributes, + extend(getGlobalStylesForElement(element, svgUid), fabric.parseStyleAttribute(element))); + + fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE; + if (ownAttributes['font-size']) { + // looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers. + ownAttributes['font-size'] = fontSize = parseUnit(ownAttributes['font-size'], parentFontSize); + } + + var normalizedAttr, normalizedValue, normalizedStyle = {}; + for (var attr in ownAttributes) { + normalizedAttr = normalizeAttr(attr); + normalizedValue = normalizeValue(normalizedAttr, ownAttributes[attr], parentAttributes, fontSize); + normalizedStyle[normalizedAttr] = normalizedValue; + } + if (normalizedStyle && normalizedStyle.font) { + fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); + } + var mergedAttrs = extend(parentAttributes, normalizedStyle); + return fabric.svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); + }, + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} [options] Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + parseElements: function(elements, callback, options, reviver, parsingOptions) { + new fabric.ElementsParser(elements, callback, options, reviver, parsingOptions).parse(); + }, + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) { + return oStyle; + } + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param {String} points points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) { + return null; + } + + // replace commas with whitespace and remove bookending whitespace + points = points.replace(/,/g, ' ').trim(); + + points = points.split(/\s+/); + var parsedPoints = [], i, len; + + for (i = 0, len = points.length; i < len; i += 2) { + parsedPoints.push({ + x: parseFloat(points[i]), + y: parseFloat(points[i + 1]) + }); + } + + // odd number of points is an error + // if (parsedPoints.length % 2 !== 0) { + // return null; + // } + + return parsedPoints; + }, + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), i, len, + allRules = { }, rules; + + // very crude parsing of style contents + for (i = 0, len = styles.length; i < len; i++) { + // IE9 doesn't support textContent, but provides text instead. + var styleContents = styles[i].textContent || styles[i].text; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + if (styleContents.trim() === '') { + continue; + } + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + // eslint-disable-next-line no-loop-func + rules.forEach(function(rule) { + + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), + ruleObj = { }, declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + for (i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + ruleObj[property] = value; + } + rule = match[1]; + rule.split(',').forEach(function(_rule) { + _rule = _rule.replace(/^svg/i, '').trim(); + if (_rule === '') { + return; + } + if (allRules[_rule]) { + fabric.util.object.extend(allRules[_rule], ruleObj); + } + else { + allRules[_rule] = fabric.util.object.clone(ruleObj); + } + }); + }); + } + return allRules; + }, + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. + * Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) + * @memberOf fabric + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromURL: function(url, callback, reviver, options) { + + url = url.replace(/^\n\s*/, '').trim(); + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (xml && !xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(//i, '')); + } + if (!xml || !xml.documentElement) { + callback && callback(null); + return false; + } + + fabric.parseSVGDocument(xml.documentElement, function (results, _options, elements, allElements) { + callback && callback(results, _options, elements, allElements); + }, reviver, options); + } + }, + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @memberOf fabric + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromString: function(string, callback, reviver, options) { + string = string.trim(); + var doc; + if (typeof fabric.window.DOMParser !== 'undefined') { + var parser = new fabric.window.DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + // IE chokes on DOCTYPE + doc.loadXML(string.replace(//i, '')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, _options, elements, allElements) { + callback(results, _options, elements, allElements); + }, reviver, options); + } + }); + +})(typeof exports !== 'undefined' ? exports : this); +fabric.ElementsParser = function(elements, callback, options, reviver, parsingOptions) { + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + this.svgUid = (options && options.svgUid) || 0; + this.parsingOptions = parsingOptions; + this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g; +}; + +(function(proto) { + proto.parse = function() { + this.instances = new Array(this.elements.length); + this.numElements = this.elements.length; + this.createObjects(); + }; + + proto.createObjects = function() { + var _this = this; + this.elements.forEach(function(element, i) { + element.setAttribute('svgUid', _this.svgUid); + _this.createObject(element, i); + }); + }; + + proto.findTag = function(el) { + return fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))]; + }; + + proto.createObject = function(el, index) { + var klass = this.findTag(el); + if (klass && klass.fromElement) { + try { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + catch (err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } + }; + + proto.createCallback = function(index, el) { + var _this = this; + return function(obj) { + var _options; + _this.resolveGradient(obj, el, 'fill'); + _this.resolveGradient(obj, el, 'stroke'); + if (obj instanceof fabric.Image && obj._originalElement) { + _options = obj.parsePreserveAspectRatioAttribute(el); + } + obj._removeTransformMatrix(_options); + _this.resolveClipPath(obj); + _this.reviver && _this.reviver(el, obj); + _this.instances[index] = obj; + _this.checkIfDone(); + }; + }; + + proto.extractPropertyDefinition = function(obj, property, storage) { + var value = obj[property]; + if (!(/^url\(/).test(value)) { + return; + } + var id = this.regexUrl.exec(value)[1]; + this.regexUrl.lastIndex = 0; + return fabric[storage][this.svgUid][id]; + }; + + proto.resolveGradient = function(obj, el, property) { + var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs'); + if (gradientDef) { + var opacityAttr = el.getAttribute(property + '-opacity'); + var gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr, this.options); + obj.set(property, gradient); + } + }; + + proto.createClipPathCallback = function(obj, container) { + return function(_newObj) { + _newObj._removeTransformMatrix(); + _newObj.fillRule = _newObj.clipRule; + container.push(_newObj); + }; + }; + + proto.resolveClipPath = function(obj) { + var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'), + element, klass, objTransformInv, container, gTransform, options; + if (clipPath) { + container = []; + objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix()); + for (var i = 0; i < clipPath.length; i++) { + element = clipPath[i]; + klass = this.findTag(element); + klass.fromElement( + element, + this.createClipPathCallback(obj, container), + this.options + ); + } + if (container.length === 1) { + clipPath = container[0]; + } + else { + clipPath = new fabric.Group(container); + } + gTransform = fabric.util.multiplyTransformMatrices( + objTransformInv, + clipPath.calcTransformMatrix() + ); + var options = fabric.util.qrDecompose(gTransform); + clipPath.flipX = false; + clipPath.flipY = false; + clipPath.set('scaleX', options.scaleX); + clipPath.set('scaleY', options.scaleY); + clipPath.angle = options.angle; + clipPath.skewX = options.skewX; + clipPath.skewY = 0; + clipPath.setPositionByOrigin({ x: options.translateX, y: options.translateY }, 'center', 'center'); + obj.clipPath = clipPath; + } + }; + + proto.checkIfDone = function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + // eslint-disable-next-line no-eq-null, eqeqeq + return el != null; + }); + this.callback(this.instances, this.elements); + } + }; +})(fabric.ElementsParser.prototype); +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Point) { + fabric.warn('fabric.Point is already defined'); + return; + } + + fabric.Point = Point; + + /** + * Point class + * @class fabric.Point + * @memberOf fabric + * @constructor + * @param {Number} x + * @param {Number} y + * @return {fabric.Point} thisArg + */ + function Point(x, y) { + this.x = x; + this.y = y; + } + + Point.prototype = /** @lends fabric.Point.prototype */ { + + type: 'point', + + constructor: Point, + + /** + * Adds another point to this one and returns another one + * @param {fabric.Point} that + * @return {fabric.Point} new Point instance with added values + */ + add: function (that) { + return new Point(this.x + that.x, this.y + that.y); + }, + + /** + * Adds another point to this one + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + addEquals: function (that) { + this.x += that.x; + this.y += that.y; + return this; + }, + + /** + * Adds value to this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} new Point with added value + */ + scalarAdd: function (scalar) { + return new Point(this.x + scalar, this.y + scalar); + }, + + /** + * Adds value to this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarAddEquals: function (scalar) { + this.x += scalar; + this.y += scalar; + return this; + }, + + /** + * Subtracts another point from this point and returns a new one + * @param {fabric.Point} that + * @return {fabric.Point} new Point object with subtracted values + */ + subtract: function (that) { + return new Point(this.x - that.x, this.y - that.y); + }, + + /** + * Subtracts another point from this point + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + * @chainable + */ + subtractEquals: function (that) { + this.x -= that.x; + this.y -= that.y; + return this; + }, + + /** + * Subtracts value from this point and returns a new one + * @param {Number} scalar + * @return {fabric.Point} + */ + scalarSubtract: function (scalar) { + return new Point(this.x - scalar, this.y - scalar); + }, + + /** + * Subtracts value from this point + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + scalarSubtractEquals: function (scalar) { + this.x -= scalar; + this.y -= scalar; + return this; + }, + + /** + * Multiplies this point by a value and returns a new one + * TODO: rename in scalarMultiply in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + multiply: function (scalar) { + return new Point(this.x * scalar, this.y * scalar); + }, + + /** + * Multiplies this point by a value + * TODO: rename in scalarMultiplyEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + multiplyEquals: function (scalar) { + this.x *= scalar; + this.y *= scalar; + return this; + }, + + /** + * Divides this point by a value and returns a new one + * TODO: rename in scalarDivide in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + divide: function (scalar) { + return new Point(this.x / scalar, this.y / scalar); + }, + + /** + * Divides this point by a value + * TODO: rename in scalarDivideEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + divideEquals: function (scalar) { + this.x /= scalar; + this.y /= scalar; + return this; + }, + + /** + * Returns true if this point is equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + eq: function (that) { + return (this.x === that.x && this.y === that.y); + }, + + /** + * Returns true if this point is less than another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lt: function (that) { + return (this.x < that.x && this.y < that.y); + }, + + /** + * Returns true if this point is less than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + lte: function (that) { + return (this.x <= that.x && this.y <= that.y); + }, + + /** + + * Returns true if this point is greater another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gt: function (that) { + return (this.x > that.x && this.y > that.y); + }, + + /** + * Returns true if this point is greater than or equal to another one + * @param {fabric.Point} that + * @return {Boolean} + */ + gte: function (that) { + return (this.x >= that.x && this.y >= that.y); + }, + + /** + * Returns new point which is the result of linear interpolation with this one and another one + * @param {fabric.Point} that + * @param {Number} t , position of interpolation, between 0 and 1 default 0.5 + * @return {fabric.Point} + */ + lerp: function (that, t) { + if (typeof t === 'undefined') { + t = 0.5; + } + t = Math.max(Math.min(1, t), 0); + return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t); + }, + + /** + * Returns distance from this point and another one + * @param {fabric.Point} that + * @return {Number} + */ + distanceFrom: function (that) { + var dx = this.x - that.x, + dy = this.y - that.y; + return Math.sqrt(dx * dx + dy * dy); + }, + + /** + * Returns the point between this point and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + midPointFrom: function (that) { + return this.lerp(that); + }, + + /** + * Returns a new point which is the min of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + min: function (that) { + return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y)); + }, + + /** + * Returns a new point which is the max of this and another one + * @param {fabric.Point} that + * @return {fabric.Point} + */ + max: function (that) { + return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y)); + }, + + /** + * Returns string representation of this point + * @return {String} + */ + toString: function () { + return this.x + ',' + this.y; + }, + + /** + * Sets x/y of this point + * @param {Number} x + * @param {Number} y + * @chainable + */ + setXY: function (x, y) { + this.x = x; + this.y = y; + return this; + }, + + /** + * Sets x of this point + * @param {Number} x + * @chainable + */ + setX: function (x) { + this.x = x; + return this; + }, + + /** + * Sets y of this point + * @param {Number} y + * @chainable + */ + setY: function (y) { + this.y = y; + return this; + }, + + /** + * Sets x/y of this point from another point + * @param {fabric.Point} that + * @chainable + */ + setFromPoint: function (that) { + this.x = that.x; + this.y = that.y; + return this; + }, + + /** + * Swaps x/y of this point and another point + * @param {fabric.Point} that + */ + swap: function (that) { + var x = this.x, + y = this.y; + this.x = that.x; + this.y = that.y; + that.x = x; + that.y = y; + }, + + /** + * return a cloned instance of the point + * @return {fabric.Point} + */ + clone: function () { + return new Point(this.x, this.y); + } + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * Intersection class + * @class fabric.Intersection + * @memberOf fabric + * @constructor + */ + function Intersection(status) { + this.status = status; + this.points = []; + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { + + constructor: Intersection, + + /** + * Appends a point to intersection + * @param {fabric.Point} point + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoint: function (point) { + this.points.push(point); + return this; + }, + + /** + * Appends points to intersection + * @param {Array} points + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + return this; + } + }; + + /** + * Checks if one line intersects another + * TODO: rename in intersectSegmentSegment + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {fabric.Point} b1 + * @param {fabric.Point} b2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (uB !== 0) { + var ua = uaT / uB, + ub = ubT / uB; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection('Intersection'); + result.appendPoint(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (uaT === 0 || ubT === 0) { + result = new Intersection('Coincident'); + } + else { + result = new Intersection('Parallel'); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * TODO: rename in intersectSegmentPolygon + * fix detection of coincident + * @static + * @param {fabric.Point} a1 + * @param {fabric.Point} a2 + * @param {Array} points + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectLinePolygon = function(a1, a2, points) { + var result = new Intersection(), + length = points.length, + b1, b2, inter, i; + + for (i = 0; i < length; i++) { + b1 = points[i]; + b2 = points[(i + 1) % length]; + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects another polygon + * @static + * @param {Array} points1 + * @param {Array} points2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection(), + length = points1.length, i; + + for (i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i + 1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + + /** + * Checks if polygon intersects rectangle + * @static + * @param {Array} points + * @param {fabric.Point} r1 + * @param {fabric.Point} r2 + * @return {fabric.Intersection} + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection(); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + + if (result.points.length > 0) { + result.status = 'Intersection'; + } + return result; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Color) { + fabric.warn('fabric.Color is already defined.'); + return; + } + + /** + * Color class + * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; + * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. + * + * @class fabric.Color + * @param {String} color optional in hex or rgb(a) or hsl format or from known color list + * @return {fabric.Color} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} + */ + function Color(color) { + if (!color) { + this.setSource([0, 0, 0, 1]); + } + else { + this._tryParsingColor(color); + } + } + + fabric.Color = Color; + + fabric.Color.prototype = /** @lends fabric.Color.prototype */ { + + /** + * @private + * @param {String|Array} color Color value to parse + */ + _tryParsingColor: function(color) { + var source; + + if (color in Color.colorNameMap) { + color = Color.colorNameMap[color]; + } + + if (color === 'transparent') { + source = [255, 255, 255, 0]; + } + + if (!source) { + source = Color.sourceFromHex(color); + } + if (!source) { + source = Color.sourceFromRgb(color); + } + if (!source) { + source = Color.sourceFromHsl(color); + } + if (!source) { + //if color is not recognize let's make black as canvas does + source = [0, 0, 0, 1]; + } + if (source) { + this.setSource(source); + } + }, + + /** + * Adapted from https://github.com/mjijackson + * @private + * @param {Number} r Red color value + * @param {Number} g Green color value + * @param {Number} b Blue color value + * @return {Array} Hsl color + */ + _rgbToHsl: function(r, g, b) { + r /= 255; g /= 255; b /= 255; + + var h, s, l, + max = fabric.util.array.max([r, g, b]), + min = fabric.util.array.min([r, g, b]); + + l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return [ + Math.round(h * 360), + Math.round(s * 100), + Math.round(l * 100) + ]; + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color representation in RGB format + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color representation in RGBA format + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color representation in HSL format + * @return {String} ex: hsl(0-360,0%-100%,0%-100%) + */ + toHsl: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; + }, + + /** + * Returns color representation in HSLA format + * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) + */ + toHsla: function() { + var source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; + }, + + /** + * Returns color representation in HEX format + * @return {String} ex: FF5555 + */ + toHex: function() { + var source = this.getSource(), r, g, b; + + r = source[0].toString(16); + r = (r.length === 1) ? ('0' + r) : r; + + g = source[1].toString(16); + g = (g.length === 1) ? ('0' + g) : g; + + b = source[2].toString(16); + b = (b.length === 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + }, + + /** + * Returns color representation in HEXA format + * @return {String} ex: FF5555CC + */ + toHexa: function() { + var source = this.getSource(), a; + + a = Math.round(source[3] * 255); + a = a.toString(16); + a = (a.length === 1) ? ('0' + a) : a; + + return this.toHex() + a.toUpperCase(); + }, + + /** + * Gets value of alpha channel for this color + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @param {Number} alpha Alpha value 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @return {fabric.Color} thisArg + */ + toGrayscale: function() { + var source = this.getSource(), + average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), + currentAlpha = source[3]; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Transforms color to its black and white representation + * @param {Number} threshold + * @return {fabric.Color} thisArg + */ + toBlackWhite: function(threshold) { + var source = this.getSource(), + average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), + currentAlpha = source[3]; + + threshold = threshold || 127; + + average = (Number(average) < Number(threshold)) ? 0 : 255; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Overlays color with another color + * @param {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + otherColor = new Color(otherColor); + } + + var result = [], + alpha = this.getAlpha(), + otherAlpha = 0.5, + source = this.getSource(), + otherSource = otherColor.getSource(), i; + + for (i = 0; i < 3; i++) { + result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); + } + + result[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) + * @static + * @field + * @memberOf fabric.Color + */ + // eslint-disable-next-line max-len + fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i; + + /** + * Regex matching color in HEX format (ex: #FF5544CC, #FF5555, 010155, aff) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; + + /** + * Map of the 148 color names with HEX code + * @static + * @field + * @memberOf fabric.Color + * @see: https://www.w3.org/TR/css3-color/#svg-color + */ + fabric.Color.colorNameMap = { + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aqua: '#00FFFF', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + black: '#000000', + blanchedalmond: '#FFEBCD', + blue: '#0000FF', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + cyan: '#00FFFF', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgrey: '#A9A9A9', + darkgreen: '#006400', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + fuchsia: '#FF00FF', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + gray: '#808080', + grey: '#808080', + green: '#008000', + greenyellow: '#ADFF2F', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgray: '#D3D3D3', + lightgrey: '#D3D3D3', + lightgreen: '#90EE90', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + lime: '#00FF00', + limegreen: '#32CD32', + linen: '#FAF0E6', + magenta: '#FF00FF', + maroon: '#800000', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + navy: '#000080', + oldlace: '#FDF5E6', + olive: '#808000', + olivedrab: '#6B8E23', + orange: '#FFA500', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + purple: '#800080', + rebeccapurple: '#663399', + red: '#FF0000', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + silver: '#C0C0C0', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + teal: '#008080', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + white: '#FFFFFF', + whitesmoke: '#F5F5F5', + yellow: '#FFFF00', + yellowgreen: '#9ACD32' + }; + + /** + * @private + * @param {Number} p + * @param {Number} q + * @param {Number} t + * @return {Number} + */ + function hue2rgb(p, q, t) { + if (t < 0) { + t += 1; + } + if (t > 1) { + t -= 1; + } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } + if (t < 1 / 2) { + return q; + } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } + return p; + } + + /** + * Returns new color object, when given a color in RGB format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @memberOf fabric.Color + * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) + * @return {Array} source + */ + fabric.Color.sourceFromRgb = function(color) { + var match = color.match(Color.reRGBa); + if (match) { + var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), + g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), + b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); + + return [ + parseInt(r, 10), + parseInt(g, 10), + parseInt(b, 10), + match[4] ? parseFloat(match[4]) : 1 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HSL format + * @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%) + * @memberOf fabric.Color + * @return {fabric.Color} + */ + fabric.Color.fromHsl = function(color) { + return Color.fromSource(Color.sourceFromHsl(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. + * Adapted from https://github.com/mjijackson + * @memberOf fabric.Color + * @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1) + * @return {Array} source + * @see http://http://www.w3.org/TR/css3-color/#hsl-color + */ + fabric.Color.sourceFromHsl = function(color) { + var match = color.match(Color.reHSLa); + if (!match) { + return; + } + + var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, + s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), + l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), + r, g, b; + + if (s === 0) { + r = g = b = l; + } + else { + var q = l <= 0.5 ? l * (s + 1) : l + s - l * s, + p = l * 2 - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255), + match[4] ? parseFloat(match[4]) : 1 + ]; + }; + + /** + * Returns new color object, when given a color in HSLA format + * @static + * @function + * @memberOf fabric.Color + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromHsla = Color.fromHsl; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color Color value ex: FF5555 + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color ex: FF5555 or FF5544CC (RGBa) + * @return {Array} source + */ + fabric.Color.sourceFromHex = function(color) { + if (color.match(Color.reHex)) { + var value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3 || value.length === 4), + isRGBa = (value.length === 8 || value.length === 4), + r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), + g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), + b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), + a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; + + return [ + parseInt(r, 16), + parseInt(g, 16), + parseInt(b, 16), + parseFloat((parseInt(a, 16) / 255).toFixed(2)) + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @memberOf fabric.Color + * @param {Array} source + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function() { + + /* _FROM_SVG_START_ */ + function getColorStop(el, multiplier) { + var style = el.getAttribute('style'), + offset = el.getAttribute('offset') || 0, + color, colorAlpha, opacity, i; + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length - 1] === '') { + keyValuePairs.pop(); + } + + for (i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + color = value; + } + else if (key === 'stop-opacity') { + opacity = value; + } + } + } + + if (!color) { + color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; + } + if (!opacity) { + opacity = el.getAttribute('stop-opacity'); + } + + color = new fabric.Color(color); + colorAlpha = color.getAlpha(); + opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); + opacity *= colorAlpha * multiplier; + + return { + offset: offset, + color: color.toRgb(), + opacity: opacity + }; + } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', + y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', + r1: 0, + x2: el.getAttribute('cx') || '50%', + y2: el.getAttribute('cy') || '50%', + r2: el.getAttribute('r') || '50%' + }; + } + /* _FROM_SVG_END_ */ + + var clone = fabric.util.object.clone; + + /** + * Gradient class + * @class fabric.Gradient + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition + */ + fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { + + /** + * Horizontal offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetY: 0, + + /** + * A transform matrix to apply to the gradient before painting. + * Imported from svg gradients, is not applied with the current transform in the center. + * Before this transform is applied, the origin point is at the top left corner of the object + * plus the addition of offsetY and offsetX. + * @type Array[Number] + * @default null + */ + gradientTransform: null, + + /** + * coordinates units for coords. + * If `pixels`, the number of coords are in the same unit of width / height. + * If set as `percentage` the coords are still a number, but 1 means 100% of width + * for the X and 100% of the height for the y. It can be bigger than 1 and negative. + * @type String pixels || percentage + * @default 'pixels' + */ + gradientUnits: 'pixels', + + /** + * Gradient type + * @type String linear || radial + * @default 'pixels' + */ + type: 'linear', + + /** + * Constructor + * @param {Object} options Options object with type, coords, gradientUnits and colorStops + * @param {Object} [options.type] gradient type linear or radial + * @param {Object} [options.gradientUnits] gradient units + * @param {Object} [options.offsetX] SVG import compatibility + * @param {Object} [options.offsetY] SVG import compatibility + * @param {Array[Object]} options.colorStops contains the colorstops. + * @param {Object} options.coords contains the coords of the gradient + * @param {Number} [options.coords.x1] X coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.y1] Y coordiante of the first point for linear or of the focal point for radial + * @param {Number} [options.coords.x2] X coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.y2] Y coordiante of the second point for linear or of the center point for radial + * @param {Number} [options.coords.r1] only for radial gradient, radius of the inner circle + * @param {Number} [options.coords.r2] only for radial gradient, radius of the external circle + * @return {fabric.Gradient} thisArg + */ + initialize: function(options) { + options || (options = { }); + options.coords || (options.coords = { }); + + var coords, _this = this; + + // sets everything, then coords and colorstops get sets again + Object.keys(options).forEach(function(option) { + _this[option] = options[option]; + }); + + if (this.id) { + this.id += '_' + fabric.Object.__uid++; + } + else { + this.id = fabric.Object.__uid++; + } + + coords = { + x1: options.coords.x1 || 0, + y1: options.coords.y1 || 0, + x2: options.coords.x2 || 0, + y2: options.coords.y2 || 0 + }; + + if (this.type === 'radial') { + coords.r1 = options.coords.r1 || 0; + coords.r2 = options.coords.r2 || 0; + } + + this.coords = coords; + this.colorStops = options.colorStops.slice(); + }, + + /** + * Adds another colorStop + * @param {Object} colorStop Object with offset and color + * @return {fabric.Gradient} thisArg + */ + addColorStop: function(colorStops) { + for (var position in colorStops) { + var color = new fabric.Color(colorStops[position]); + this.colorStops.push({ + offset: parseFloat(position), + color: color.toRgb(), + opacity: color.getAlpha() + }); + } + return this; + }, + + /** + * Returns object representation of a gradient + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} + */ + toObject: function(propertiesToInclude) { + var object = { + type: this.type, + coords: this.coords, + colorStops: this.colorStops, + offsetX: this.offsetX, + offsetY: this.offsetY, + gradientUnits: this.gradientUnits, + gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an gradient + * @param {Object} object Object to create a gradient for + * @return {String} SVG representation of an gradient (linear/radial) + */ + toSVG: function(object, options) { + var coords = clone(this.coords, true), i, len, options = options || {}, + markup, commonAttributes, colorStops = clone(this.colorStops, true), + needsSwap = coords.r1 > coords.r2, + transform = this.gradientTransform ? this.gradientTransform.concat() : fabric.iMatrix.concat(), + offsetX = -this.offsetX, offsetY = -this.offsetY, + withViewport = !!options.additionalTransform, + gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; + // colorStops must be sorted ascending + colorStops.sort(function(a, b) { + return a.offset - b.offset; + }); + + if (gradientUnits === 'objectBoundingBox') { + offsetX /= object.width; + offsetY /= object.height; + } + else { + offsetX += object.width / 2; + offsetY += object.height / 2; + } + if (object.type === 'path') { + offsetX -= object.pathOffset.x; + offsetY -= object.pathOffset.y; + } + + + transform[4] -= offsetX; + transform[5] -= offsetY; + + commonAttributes = 'id="SVGID_' + this.id + + '" gradientUnits="' + gradientUnits + '"'; + commonAttributes += ' gradientTransform="' + (withViewport ? + options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; + + if (this.type === 'linear') { + markup = [ + '\n' + ]; + } + else if (this.type === 'radial') { + // svg radial gradient has just 1 radius. the biggest. + markup = [ + '\n' + ]; + } + + if (this.type === 'radial') { + if (needsSwap) { + // svg goes from internal to external radius. if radius are inverted, swap color stops. + colorStops = colorStops.concat(); + colorStops.reverse(); + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset = 1 - colorStops[i].offset; + } + } + var minRadius = Math.min(coords.r1, coords.r2); + if (minRadius > 0) { + // i have to shift all colorStops and add new one in 0. + var maxRadius = Math.max(coords.r1, coords.r2), + percentageShift = minRadius / maxRadius; + for (i = 0, len = colorStops.length; i < len; i++) { + colorStops[i].offset += percentageShift * (1 - colorStops[i].offset); + } + } + } + + for (i = 0, len = colorStops.length; i < len; i++) { + var colorStop = colorStops[i]; + markup.push( + '\n' + ); + } + + markup.push((this.type === 'linear' ? '\n' : '\n')); + + return markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns an instance of CanvasGradient + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {CanvasGradient} + */ + toLive: function(ctx) { + var gradient, coords = fabric.util.object.clone(this.coords), i, len; + + if (!this.type) { + return; + } + + if (this.type === 'linear') { + gradient = ctx.createLinearGradient( + coords.x1, coords.y1, coords.x2, coords.y2); + } + else if (this.type === 'radial') { + gradient = ctx.createRadialGradient( + coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); + } + + for (i = 0, len = this.colorStops.length; i < len; i++) { + var color = this.colorStops[i].color, + opacity = this.colorStops[i].opacity, + offset = this.colorStops[i].offset; + + if (typeof opacity !== 'undefined') { + color = new fabric.Color(color).setAlpha(opacity).toRgba(); + } + gradient.addColorStop(offset, color); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /* _FROM_SVG_START_ */ + /** + * Returns {@link fabric.Gradient} instance from an SVG element + * @static + * @memberOf fabric.Gradient + * @param {SVGGradientElement} el SVG gradient element + * @param {fabric.Object} instance + * @param {String} opacityAttr A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity. + * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly graidents + * that uses gradientUnits as 'userSpaceOnUse' and percentages. + * @param {Object.number} viewBoxWidth width part of the viewBox attribute on svg + * @param {Object.number} viewBoxHeight height part of the viewBox attribute on svg + * @param {Object.number} width width part of the svg tag if viewBox is not specified + * @param {Object.number} height height part of the svg tag if viewBox is not specified + * @return {fabric.Gradient} Gradient instance + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement + */ + fromElement: function(el, instance, opacityAttr, svgOptions) { + /** + * @example: + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + */ + + var multiplier = parseFloat(opacityAttr) / (/%$/.test(opacityAttr) ? 100 : 1); + multiplier = multiplier < 0 ? 0 : multiplier > 1 ? 1 : multiplier; + if (isNaN(multiplier)) { + multiplier = 1; + } + + var colorStopEls = el.getElementsByTagName('stop'), + type, + gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? + 'pixels' : 'percentage', + gradientTransform = el.getAttribute('gradientTransform') || '', + colorStops = [], + coords, i, offsetX = 0, offsetY = 0, + transformMatrix; + if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { + type = 'linear'; + coords = getLinearCoords(el); + } + else { + type = 'radial'; + coords = getRadialCoords(el); + } + + for (i = colorStopEls.length; i--; ) { + colorStops.push(getColorStop(colorStopEls[i], multiplier)); + } + + transformMatrix = fabric.parseTransformAttribute(gradientTransform); + + __convertPercentUnitsToValues(instance, coords, svgOptions, gradientUnits); + + if (gradientUnits === 'pixels') { + offsetX = -instance.left; + offsetY = -instance.top; + } + + var gradient = new fabric.Gradient({ + id: el.getAttribute('id'), + type: type, + coords: coords, + colorStops: colorStops, + gradientUnits: gradientUnits, + gradientTransform: transformMatrix, + offsetX: offsetX, + offsetY: offsetY, + }); + + return gradient; + }, + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Gradient} instance from its object representation + * this function is uniquely used by Object.setGradient and is deprecated with it. + * @static + * @deprecated since 3.4.0 + * @memberOf fabric.Gradient + * @param {Object} obj + * @param {Object} [options] Options object + */ + forObject: function(obj, options) { + options || (options = { }); + __convertPercentUnitsToValues(obj, options.coords, options.gradientUnits, { + // those values are to avoid errors. this function is uniquely used by + viewBoxWidth: 100, + viewBoxHeight: 100, + }); + return new fabric.Gradient(options); + } + }); + + /** + * @private + */ + function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { + var propValue, finalValue; + Object.keys(options).forEach(function(prop) { + propValue = options[prop]; + if (propValue === 'Infinity') { + finalValue = 1; + } + else if (propValue === '-Infinity') { + finalValue = 0; + } + else { + finalValue = parseFloat(options[prop], 10); + if (typeof propValue === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(propValue)) { + finalValue *= 0.01; + if (gradientUnits === 'pixels') { + // then we need to fix those percentages here in svg parsing + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + finalValue *= svgOptions.viewBoxWidth || svgOptions.width; + } + if (prop === 'y1' || prop === 'y2') { + finalValue *= svgOptions.viewBoxHeight || svgOptions.height; + } + } + } + } + options[prop] = finalValue; + }); + } +})(); +(function() { + + 'use strict'; + + var toFixed = fabric.util.toFixed; + + /** + * Pattern class + * @class fabric.Pattern + * @see {@link http://fabricjs.com/patterns|Pattern demo} + * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition + */ + + + fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { + + /** + * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @type String + * @default + */ + repeat: 'repeat', + + /** + * Pattern horizontal offset from object's left/top corner + * @type Number + * @default + */ + offsetX: 0, + + /** + * Pattern vertical offset from object's left/top corner + * @type Number + * @default + */ + offsetY: 0, + + /** + * crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + + /** + * transform matrix to change the pattern, imported from svgs. + * @type Array + * @default + */ + patternTransform: null, + + /** + * Constructor + * @param {Object} [options] Options object + * @param {Function} [callback] function to invoke after callback init. + * @return {fabric.Pattern} thisArg + */ + initialize: function(options, callback) { + options || (options = { }); + + this.id = fabric.Object.__uid++; + this.setOptions(options); + if (!options.source || (options.source && typeof options.source !== 'string')) { + callback && callback(this); + return; + } + // function string + if (typeof fabric.util.getFunctionBody(options.source) !== 'undefined') { + this.source = new Function(fabric.util.getFunctionBody(options.source)); + callback && callback(this); + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img) { + _this.source = img; + callback && callback(_this); + }, null, this.crossOrigin); + } + }, + + /** + * Returns object representation of a pattern + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of a pattern instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + source, object; + + // callback + if (typeof this.source === 'function') { + source = String(this.source); + } + // element + else if (typeof this.source.src === 'string') { + source = this.source.src; + } + // element + else if (typeof this.source === 'object' && this.source.toDataURL) { + source = this.source.toDataURL(); + } + + object = { + type: 'pattern', + source: source, + repeat: this.repeat, + crossOrigin: this.crossOrigin, + offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), + patternTransform: this.patternTransform ? this.patternTransform.concat() : null + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a pattern + * @param {fabric.Object} object + * @return {String} SVG representation of a pattern + */ + toSVG: function(object) { + var patternSource = typeof this.source === 'function' ? this.source() : this.source, + patternWidth = patternSource.width / object.width, + patternHeight = patternSource.height / object.height, + patternOffsetX = this.offsetX / object.width, + patternOffsetY = this.offsetY / object.height, + patternImgSrc = ''; + if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { + patternHeight = 1; + if (patternOffsetY) { + patternHeight += Math.abs(patternOffsetY); + } + } + if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { + patternWidth = 1; + if (patternOffsetX) { + patternWidth += Math.abs(patternOffsetX); + } + + } + if (patternSource.src) { + patternImgSrc = patternSource.src; + } + else if (patternSource.toDataURL) { + patternImgSrc = patternSource.toDataURL(); + } + + return '\n' + + '\n' + + '\n'; + }, + /* _TO_SVG_END_ */ + + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Returns an instance of CanvasPattern + * @param {CanvasRenderingContext2D} ctx Context to create pattern + * @return {CanvasPattern} + */ + toLive: function(ctx) { + var source = typeof this.source === 'function' ? this.source() : this.source; + + // if the image failed to load, return, and allow rest to continue loading + if (!source) { + return ''; + } + + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) { + return ''; + } + if (source.naturalWidth === 0 || source.naturalHeight === 0) { + return ''; + } + } + return ctx.createPattern(source, this.repeat); + } + }); +})(); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Shadow) { + fabric.warn('fabric.Shadow is already defined.'); + return; + } + + /** + * Shadow class + * @class fabric.Shadow + * @see {@link http://fabricjs.com/shadows|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition + */ + fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { + + /** + * Shadow color + * @type String + * @default + */ + color: 'rgb(0,0,0)', + + /** + * Shadow blur + * @type Number + */ + blur: 0, + + /** + * Shadow horizontal offset + * @type Number + * @default + */ + offsetX: 0, + + /** + * Shadow vertical offset + * @type Number + * @default + */ + offsetY: 0, + + /** + * Whether the shadow should affect stroke operations + * @type Boolean + * @default + */ + affectStroke: false, + + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * When `false`, the shadow will scale with the object. + * When `true`, the shadow's offsetX, offsetY, and blur will not be affected by the object's scale. + * default to false + * @type Boolean + * @default + */ + nonScaling: false, + + /** + * Constructor + * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetY properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px") + * @return {fabric.Shadow} thisArg + */ + initialize: function(options) { + + if (typeof options === 'string') { + options = this._parseShadow(options); + } + + for (var prop in options) { + this[prop] = options[prop]; + } + + this.id = fabric.Object.__uid++; + }, + + /** + * @private + * @param {String} shadow Shadow value to parse + * @return {Object} Shadow object with color, offsetX, offsetY and blur + */ + _parseShadow: function(shadow) { + var shadowStr = shadow.trim(), + offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [], + color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)'; + + return { + color: color.trim(), + offsetX: parseInt(offsetsAndBlur[1], 10) || 0, + offsetY: parseInt(offsetsAndBlur[2], 10) || 0, + blur: parseInt(offsetsAndBlur[3], 10) || 0 + }; + }, + + /** + * Returns a string representation of an instance + * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow + * @return {String} Returns CSS3 text-shadow declaration + */ + toString: function() { + return [this.offsetX, this.offsetY, this.blur, this.color].join('px '); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a shadow + * @param {fabric.Object} object + * @return {String} SVG representation of a shadow + */ + toSVG: function(object) { + var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + offset = fabric.util.rotateVector( + { x: this.offsetX, y: this.offsetY }, + fabric.util.degreesToRadians(-object.angle)), + BLUR_BOX = 20, color = new fabric.Color(this.color); + + if (object.width && object.height) { + //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion + // we add some extra space to filter box to contain the blur ( 20 ) + fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + } + if (object.flipX) { + offset.x *= -1; + } + if (object.flipY) { + offset.y *= -1; + } + + return ( + '\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\n' + + '\n'); + }, + /* _TO_SVG_END_ */ + + /** + * Returns object representation of a shadow + * @return {Object} Object representation of a shadow instance + */ + toObject: function() { + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY, + affectStroke: this.affectStroke, + nonScaling: this.nonScaling + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + + ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke', 'nonScaling'].forEach(function(prop) { + if (this[prop] !== proto[prop]) { + obj[prop] = this[prop]; + } + }, this); + + return obj; + } + }); + + /** + * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") + * @static + * @field + * @memberOf fabric.Shadow + */ + // eslint-disable-next-line max-len + fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/; + +})(typeof exports !== 'undefined' ? exports : this); +(function () { + + 'use strict'; + + if (fabric.StaticCanvas) { + fabric.warn('fabric.StaticCanvas is already defined.'); + return; + } + + // aliases for faster resolution + var extend = fabric.util.object.extend, + getElementOffset = fabric.util.getElementOffset, + removeFromArray = fabric.util.removeFromArray, + toFixed = fabric.util.toFixed, + transformPoint = fabric.util.transformPoint, + invertTransform = fabric.util.invertTransform, + getNodeCanvas = fabric.util.getNodeCanvas, + createCanvasElement = fabric.util.createCanvasElement, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * Static canvas class + * @class fabric.StaticCanvas + * @mixes fabric.Collection + * @mixes fabric.Observable + * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition + * @fires before:render + * @fires after:render + * @fires canvas:cleared + * @fires object:added + * @fires object:removed + */ + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + }, + + /** + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} + * @default + */ + backgroundColor: '', + + /** + * Background image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundImage}. + * Backwards incompatibility note: The "backgroundImageOpacity" + * and "backgroundImageStretch" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#opacity}, {@link fabric.Image#width} and {@link fabric.Image#height}. + * since 2.4.0 image caching is active, please when putting an image as background, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + backgroundImage: null, + + /** + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} + * @default + */ + overlayColor: '', + + /** + * Overlay image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayImage}. + * Backwards incompatibility note: The "overlayImageLeft" + * and "overlayImageTop" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#left} and {@link fabric.Image#top}. + * since 2.4.0 image caching is active, please when putting an image as overlay, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type fabric.Image + * @default + */ + overlayImage: null, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * if set to false, takes precedence over the object value. + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @type Boolean + * @default + */ + stateful: false, + + /** + * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove}, + * {@link fabric.StaticCanvas.moveTo}, {@link fabric.StaticCanvas.clear} and many more, should also re-render canvas. + * Disabling this option will not give a performance boost when adding/removing a lot of objects to/from canvas at once + * since the renders are quequed and executed one per frame. + * Disabling is suggested anyway and managing the renders of the app manually is not a big effort ( canvas.requestRenderAll() ) + * Left default to true to do not break documentation and old app, fiddles. + * @type Boolean + * @default + */ + renderOnAddRemove: true, + + /** + * Function that determines clipping of entire canvas area + * Being passed context as first argument. + * If you are using code minification, ctx argument can be minified/manglied you should use + * as a workaround `var ctx = arguments[0];` in the function; + * See clipping canvas area in {@link https://github.com/kangax/fabric.js/wiki/FAQ} + * @deprecated since 2.0.0 + * @type Function + * @default + */ + clipTo: null, + + /** + * Indicates whether object controls (borders/controls) are rendered above overlay image + * @type Boolean + * @default + */ + controlsAboveOverlay: false, + + /** + * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas + * @type Boolean + * @default + */ + allowTouchScrolling: false, + + /** + * Indicates whether this canvas will use image smoothing, this is on by default in browsers + * @type Boolean + * @default + */ + imageSmoothingEnabled: true, + + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: fabric.iMatrix.concat(), + + /** + * if set to false background image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + backgroundVpt: true, + + /** + * if set to false overlya image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + overlayVpt: true, + + /** + * Callback; invoked right before object is about to be scaled/rotated + * @deprecated since 2.3.0 + * Use before:transform event + */ + onBeforeScaleRotate: function () { + /* NOOP */ + }, + + /** + * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens + * @type Boolean + * @default + */ + enableRetinaScaling: true, + + /** + * Describe canvas element extension over design + * properties are tl,tr,bl,br. + * if canvas is not zoomed/panned those points are the four corner of canvas + * if canvas is viewportTransformed you those points indicate the extension + * of canvas element in plain untrasformed coordinates + * The coordinates get updated with @method calcViewportBoundaries. + * @memberOf fabric.StaticCanvas.prototype + */ + vptCoords: { }, + + /** + * Based on vptCoords and object.aCoords, skip rendering of objects that + * are not included in current viewport. + * May greatly help in applications with crowded canvas and use of zoom/pan + * If One of the corner of the bounding box of the object is on the canvas + * the objects get rendered. + * @memberOf fabric.StaticCanvas.prototype + * @type Boolean + * @default + */ + skipOffscreen: true, + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the canvas has rendered, and the context is placed in the + * top left corner of the canvas. + * clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true + * @type fabric.Object + */ + clipPath: undefined, + + /** + * @private + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + _initStatic: function(el, options) { + var cb = this.requestRenderAllBound; + this._objects = []; + this._createLowerCanvas(el); + this._initOptions(options); + this._setImageSmoothing(); + // only initialize retina scaling once + if (!this.interactive) { + this._initRetinaScaling(); + } + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, cb); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, cb); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, cb); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, cb); + } + this.calcOffset(); + }, + + /** + * @private + */ + _isRetinaScaling: function() { + return (fabric.devicePixelRatio !== 1 && this.enableRetinaScaling); + }, + + /** + * @private + * @return {Number} retinaScaling if applied, otherwise 1; + */ + getRetinaScaling: function() { + return this._isRetinaScaling() ? fabric.devicePixelRatio : 1; + }, + + /** + * @private + */ + _initRetinaScaling: function() { + if (!this._isRetinaScaling()) { + return; + } + this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio); + this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio); + + this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio); + }, + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} + * @example Normal overlayImage with left/top = 0 + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage loaded from cross-origin + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set background to + * @param {Function} callback Callback to invoke when image is loaded and set as background + * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/djnr8o7a/28/|jsFiddle demo} + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage loaded from cross-origin + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + // TODO: fix stretched examples + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayColor|foreground color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set foreground color to + * @param {Function} callback Callback to invoke when foreground color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} + * @example Normal backgroundColor - color value + * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setBackgroundColor: function(backgroundColor, callback) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-imagesmoothingenabled|WhatWG Canvas Standard} + */ + _setImageSmoothing: function() { + var ctx = this.getContext(); + + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled + || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; + ctx.imageSmoothingEnabled = this.imageSmoothingEnabled; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img) { + if (img) { + var instance = new fabric.Image(img, options); + this[property] = instance; + instance.canvas = this; + } + callback && callback(img); + }, this, options && options.crossOrigin); + } + else { + options && image.setOptions(options); + this[property] = image; + image && (image.canvas = this); + callback && callback(image); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String|null)} color Object with pattern information, color value or null + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + this[property] = color; + this._initGradient(color, property); + this._initPattern(color, property, callback); + return this; + }, + + /** + * @private + */ + _createCanvasElement: function() { + var element = createCanvasElement(); + if (!element) { + throw CANVAS_INIT_ERROR; + } + if (!element.style) { + element.style = { }; + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + return element; + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initOptions: function (options) { + var lowerCanvasEl = this.lowerCanvasEl; + this._setOptions(options); + + this.width = this.width || parseInt(lowerCanvasEl.width, 10) || 0; + this.height = this.height || parseInt(lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) { + return; + } + + lowerCanvasEl.width = this.width; + lowerCanvasEl.height = this.height; + + lowerCanvasEl.style.width = this.width + 'px'; + lowerCanvasEl.style.height = this.height + 'px'; + + this.viewportTransform = this.viewportTransform.slice(); + }, + + /** + * Creates a bottom canvas + * @private + * @param {HTMLElement} [canvasEl] + */ + _createLowerCanvas: function (canvasEl) { + // canvasEl === 'HTMLCanvasElement' does not work on jsdom/node + if (canvasEl && canvasEl.getContext) { + this.lowerCanvasEl = canvasEl; + } + else { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + } + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width (in px) + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height (in px) + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @param {Number|String} value Value to set width to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value, options) { + return this.setDimensions({ width: value }, options); + }, + + /** + * Sets height of this canvas instance + * @param {Number|String} value Value to set height to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value, options) { + return this.setDimensions({ height: value }, options); + }, + + /** + * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) + * @param {Object} dimensions Object with width/height properties + * @param {Number|String} [dimensions.width] Width of canvas element + * @param {Number|String} [dimensions.height] Height of canvas element + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function (dimensions, options) { + var cssValue; + + options = options || {}; + + for (var prop in dimensions) { + cssValue = dimensions[prop]; + + if (!options.cssOnly) { + this._setBackstoreDimension(prop, dimensions[prop]); + cssValue += 'px'; + this.hasLostContext = true; + } + + if (!options.backstoreOnly) { + this._setCssDimension(prop, cssValue); + } + } + if (this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(); + } + this._initRetinaScaling(); + this._setImageSmoothing(); + this.calcOffset(); + + if (!options.cssOnly) { + this.requestRenderAll(); + } + + return this; + }, + + /** + * Helper for setting width/height + * @private + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setBackstoreDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + } + + if (this.cacheCanvasEl) { + this.cacheCanvasEl[prop] = value; + } + + this[prop] = value; + + return this; + }, + + /** + * Helper for setting css width/height + * @private + * @param {String} prop property (width|height) + * @param {String} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setCssDimension: function (prop, value) { + this.lowerCanvasEl.style[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl.style[prop] = value; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value; + } + + return this; + }, + + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return this.viewportTransform[0]; + }, + + /** + * Sets viewport transform of this canvas instance + * @param {Array} vpt the transform in the form of context.transform + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportTransform: function (vpt) { + var activeObject = this._activeObject, object, ignoreVpt = false, skipAbsolute = true, i, len; + this.viewportTransform = vpt; + for (i = 0, len = this._objects.length; i < len; i++) { + object = this._objects[i]; + object.group || object.setCoords(ignoreVpt, skipAbsolute); + } + if (activeObject && activeObject.type === 'activeSelection') { + activeObject.setCoords(ignoreVpt, skipAbsolute); + } + this.calcViewportBoundaries(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Sets zoom level of this canvas instance, zoom centered around point + * @param {fabric.Point} point to zoom with respect to + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + zoomToPoint: function (point, value) { + // TODO: just change the scale, preserve other transformations + var before = point, vpt = this.viewportTransform.slice(0); + point = transformPoint(point, invertTransform(this.viewportTransform)); + vpt[0] = value; + vpt[3] = value; + var after = transformPoint(point, vpt); + vpt[4] += before.x - after.x; + vpt[5] += before.y - after.y; + return this.setViewportTransform(vpt); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + this.zoomToPoint(new fabric.Point(0, 0), value); + return this; + }, + + /** + * Pan viewport so as to place point at top left corner of canvas + * @param {fabric.Point} point to move to + * @return {fabric.Canvas} instance + * @chainable true + */ + absolutePan: function (point) { + var vpt = this.viewportTransform.slice(0); + vpt[4] = -point.x; + vpt[5] = -point.y; + return this.setViewportTransform(vpt); + }, + + /** + * Pans viewpoint relatively + * @param {fabric.Point} point (position vector) to move by + * @return {fabric.Canvas} instance + * @chainable true + */ + relativePan: function (point) { + return this.absolutePan(new fabric.Point( + -point.x - this.viewportTransform[4], + -point.y - this.viewportTransform[5] + )); + }, + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj._set('canvas', this); + obj.setCoords(); + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + this.fire('object:removed', { target: obj }); + obj.fire('removed'); + delete obj.canvas; + }, + + /** + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this._objects.length = 0; + this.backgroundImage = null; + this.overlayImage = null; + this.backgroundColor = ''; + this.overlayColor = ''; + if (this._hasITextHandlers) { + this.off('mouse:up', this._mouseUpITextHandler); + this._iTextInstances = null; + this._hasITextHandlers = false; + } + this.clearContext(this.contextContainer); + this.fire('canvas:cleared'); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Renders the canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._objects); + return this; + }, + + /** + * Function created to be instance bound at initialization + * used in requestAnimationFrame rendering + * Let the fabricJS call it. If you call it manually you could have more + * animationFrame stacking on to of each other + * for an imperative rendering, use canvas.renderAll + * @private + * @return {fabric.Canvas} instance + * @chainable + */ + renderAndReset: function() { + this.isRendering = 0; + this.renderAll(); + }, + + /** + * Append a renderAll request to next animation frame. + * unless one is already in progress, in that case nothing is done + * a boolean flag will avoid appending more. + * @return {fabric.Canvas} instance + * @chainable + */ + requestRenderAll: function () { + if (!this.isRendering) { + this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound); + } + return this; + }, + + /** + * Calculate the position of the 4 corner of canvas with current viewportTransform. + * helps to determinate when an object is in the current rendering viewport using + * object absolute coordinates ( aCoords ) + * @return {Object} points.tl + * @chainable + */ + calcViewportBoundaries: function() { + var points = { }, width = this.width, height = this.height, + iVpt = invertTransform(this.viewportTransform); + points.tl = transformPoint({ x: 0, y: 0 }, iVpt); + points.br = transformPoint({ x: width, y: height }, iVpt); + points.tr = new fabric.Point(points.br.x, points.tl.y); + points.bl = new fabric.Point(points.tl.x, points.br.y); + this.vptCoords = points; + return points; + }, + + cancelRequestedRender: function() { + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + }, + + /** + * Renders background, objects, overlay and controls. + * @param {CanvasRenderingContext2D} ctx + * @param {Array} objects to render + * @return {fabric.Canvas} instance + * @chainable + */ + renderCanvas: function(ctx, objects) { + var v = this.viewportTransform, path = this.clipPath; + this.cancelRequestedRender(); + this.calcViewportBoundaries(); + this.clearContext(ctx); + this.fire('before:render', { ctx: ctx, }); + if (this.clipTo) { + fabric.util.clipContext(this, ctx); + } + this._renderBackground(ctx); + + ctx.save(); + //apply viewport transform once for all rendering process + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this._renderObjects(ctx, objects); + ctx.restore(); + if (!this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + if (this.clipTo) { + ctx.restore(); + } + if (path) { + path.canvas = this; + // needed to setup a couple of variables + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCanvas(ctx); + } + this._renderOverlay(ctx); + if (this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + this.fire('after:render', { ctx: ctx, }); + }, + + /** + * Paint the cached clipPath on the lowerCanvasEl + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCanvas: function(ctx) { + var v = this.viewportTransform, path = this.clipPath; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4; + ctx.globalCompositeOperation = 'destination-in'; + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} objects to render + */ + _renderObjects: function(ctx, objects) { + var i, len; + for (i = 0, len = objects.length; i < len; ++i) { + objects[i] && objects[i].render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {string} property 'background' or 'overlay' + */ + _renderBackgroundOrOverlay: function(ctx, property) { + var fill = this[property + 'Color'], object = this[property + 'Image'], + v = this.viewportTransform, needsVpt = this[property + 'Vpt']; + if (!fill && !object) { + return; + } + if (fill) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(this.width, 0); + ctx.lineTo(this.width, this.height); + ctx.lineTo(0, this.height); + ctx.closePath(); + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx, this) + : fill; + if (needsVpt) { + ctx.transform( + v[0], v[1], v[2], v[3], + v[4] + (fill.offsetX || 0), + v[5] + (fill.offsetY || 0) + ); + } + var m = fill.gradientTransform || fill.patternTransform; + m && ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + ctx.fill(); + ctx.restore(); + } + if (object) { + ctx.save(); + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + } + object.render(ctx); + ctx.restore(); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'background'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'overlay'); + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.height / 2, + left: this.width / 2 + }; + }, + + /** + * Centers object horizontally in the canvas + * @param {fabric.Object} object Object to center horizontally + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + }, + + /** + * Centers object vertically in the canvas + * @param {fabric.Object} object Object to center vertically + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + }, + + /** + * Centers object vertically and horizontally in the canvas + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function(object) { + var center = this.getCenter(); + + return this._centerObject(object, new fabric.Point(center.left, center.top)); + }, + + /** + * Centers object vertically and horizontally in the viewport + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObject: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, vpCenter); + }, + + /** + * Centers object horizontally in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectH: function(object) { + var vpCenter = this.getVpCenter(); + this._centerObject(object, new fabric.Point(vpCenter.x, object.getCenterPoint().y)); + return this; + }, + + /** + * Centers object Vertically in the viewport, object.top is unchanged + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectV: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, vpCenter.y)); + }, + + /** + * Calculate the point in canvas that correspond to the center of actual viewport. + * @return {fabric.Point} vpCenter, viewport center + * @chainable + */ + getVpCenter: function() { + var center = this.getCenter(), + iVpt = invertTransform(this.viewportTransform); + return transformPoint({ x: center.left, y: center.top }, iVpt); + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + object.setCoords(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Returs dataless JSON representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {String} json string + */ + toDatalessJSON: function (propertiesToInclude) { + return this.toDatalessObject(propertiesToInclude); + }, + + /** + * Returns object representation of canvas + * @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._toObjectMethod('toObject', propertiesToInclude); + }, + + /** + * Returns dataless object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function (propertiesToInclude) { + return this._toObjectMethod('toDatalessObject', propertiesToInclude); + }, + + /** + * @private + */ + _toObjectMethod: function (methodName, propertiesToInclude) { + + var clipPath = this.clipPath, data = { + version: fabric.version, + objects: this._toObjects(methodName, propertiesToInclude), + }; + if (clipPath) { + data.clipPath = this._toObject(this.clipPath, methodName, propertiesToInclude); + } + extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude)); + + fabric.util.populateWithProperties(this, data, propertiesToInclude); + + return data; + }, + + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this._objects.filter(function(object) { + return !object.excludeFromExport; + }).map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function(methodName, propertiesToInclude) { + var data = { }, bgImage = this.backgroundImage, overlay = this.overlayImage; + + if (this.backgroundColor) { + data.background = this.backgroundColor.toObject + ? this.backgroundColor.toObject(propertiesToInclude) + : this.backgroundColor; + } + + if (this.overlayColor) { + data.overlay = this.overlayColor.toObject + ? this.overlayColor.toObject(propertiesToInclude) + : this.overlayColor; + } + if (bgImage && !bgImage.excludeFromExport) { + data.backgroundImage = this._toObject(bgImage, methodName, propertiesToInclude); + } + if (overlay && !overlay.excludeFromExport) { + data.overlayImage = this._toObject(overlay, methodName, propertiesToInclude); + } + + return data; + }, + + /* _TO_SVG_START_ */ + /** + * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, + * a zoomed canvas will then produce zoomed SVG output. + * @type Boolean + * @default + */ + svgViewportTransformation: true, + + /** + * Returns SVG representation of canvas + * @function + * @param {Object} [options] Options object for SVG output + * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included + * @param {Object} [options.viewBox] SVG viewbox object + * @param {Number} [options.viewBox.x] x-cooridnate of viewbox + * @param {Number} [options.viewBox.y] y-coordinate of viewbox + * @param {Number} [options.viewBox.width] Width of viewbox + * @param {Number} [options.viewBox.height] Height of viewbox + * @param {String} [options.encoding=UTF-8] Encoding of SVG output + * @param {String} [options.width] desired width of svg with or without units + * @param {String} [options.height] desired height of svg with or without units + * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. + * @return {String} SVG string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} + * @example Normal SVG output + * var svg = canvas.toSVG(); + * @example SVG output without preamble (without <?xml ../>) + * var svg = canvas.toSVG({suppressPreamble: true}); + * @example SVG output with viewBox attribute + * var svg = canvas.toSVG({ + * viewBox: { + * x: 100, + * y: 100, + * width: 200, + * height: 300 + * } + * }); + * @example SVG output with different encoding (default: UTF-8) + * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); + * @example Modify SVG output with reviver function + * var svg = canvas.toSVG(null, function(svg) { + * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); + * }); + */ + toSVG: function(options, reviver) { + options || (options = { }); + options.reviver = reviver; + var markup = []; + + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'background'); + this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); + this._setSVGObjects(markup, reviver); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'overlay'); + this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { + if (options.suppressPreamble) { + return; + } + markup.push( + '\n', + '\n' + ); + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { + var width = options.width || this.width, + height = options.height || this.height, + vpt, viewBox = 'viewBox="0 0 ' + this.width + ' ' + this.height + '" ', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + if (options.viewBox) { + viewBox = 'viewBox="' + + options.viewBox.x + ' ' + + options.viewBox.y + ' ' + + options.viewBox.width + ' ' + + options.viewBox.height + '" '; + } + else { + if (this.svgViewportTransformation) { + vpt = this.viewportTransform; + viewBox = 'viewBox="' + + toFixed(-vpt[4] / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.width / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS) + '" '; + } + } + + markup.push( + '\n', + 'Created with Fabric.js ', fabric.version, '\n', + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), + this.createSVGClipPathMarkup(options), + '\n' + ); + }, + + createSVGClipPathMarkup: function(options) { + var clipPath = this.clipPath; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + return '\n' + + this.clipPath.toClipPathSVG(options.reviver) + + '\n'; + } + return ''; + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup: function() { + var _this = this, + markup = ['background', 'overlay'].map(function(prop) { + var fill = _this[prop + 'Color']; + if (fill && fill.toLive) { + var shouldTransform = _this[prop + 'Vpt'], vpt = _this.viewportTransform, + object = { + width: _this.width / (shouldTransform ? vpt[0] : 1), + height: _this.height / (shouldTransform ? vpt[3] : 1) + }; + return fill.toSVG( + object, + { additionalTransform: shouldTransform ? fabric.util.matrixToSVG(vpt) : '' } + ); + } + }); + return markup.join(''); + }, + + /** + * Creates markup containing SVG font faces, + * font URLs for font faces must be collected by developers + * and are not extracted from the DOM by fabricjs + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function() { + var markup = '', fontList = { }, obj, fontFamily, + style, row, rowIndex, _char, charIndex, i, len, + fontPaths = fabric.fontPaths, objects = this._objects; + + for (i = 0, len = objects.length; i < len; i++) { + obj = objects[i]; + fontFamily = obj.fontFamily; + if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { + continue; + } + fontList[fontFamily] = true; + if (!obj.styles) { + continue; + } + style = obj.styles; + for (rowIndex in style) { + row = style[rowIndex]; + for (charIndex in row) { + _char = row[charIndex]; + fontFamily = _char.fontFamily; + if (!fontList[fontFamily] && fontPaths[fontFamily]) { + fontList[fontFamily] = true; + } + } + } + } + + for (var j in fontList) { + markup += [ + '\t\t@font-face {\n', + '\t\t\tfont-family: \'', j, '\';\n', + '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', + '\t\t}\n' + ].join(''); + } + + if (markup) { + markup = [ + '\t\n' + ].join(''); + } + + return markup; + }, + + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { + var instance, i, len, objects = this._objects; + for (i = 0, len = objects.length; i < len; i++) { + instance = objects[i]; + if (instance.excludeFromExport) { + continue; + } + this._setSVGObject(markup, instance, reviver); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + markup.push(instance.toSVG(reviver)); + }, + + /** + * @private + */ + _setSVGBgOverlayImage: function(markup, property, reviver) { + if (this[property] && !this[property].excludeFromExport && this[property].toSVG) { + markup.push(this[property].toSVG(reviver)); + } + }, + + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + var filler = this[property + 'Color'], vpt = this.viewportTransform, finalWidth = this.width, + finalHeight = this.height; + if (!filler) { + return; + } + if (filler.toLive) { + var repeat = filler.repeat, iVpt = fabric.util.invertTransform(vpt), shouldInvert = this[property + 'Vpt'], + additionalTransform = shouldInvert ? fabric.util.matrixToSVG(iVpt) : ''; + markup.push( + '\n' + ); + } + else { + markup.push( + '\n' + ); + } + }, + /* _TO_SVG_END_ */ + + /** + * Moves an object or the objects of a multiple selection + * to the bottom of the stack of drawn objects + * @param {fabric.Object} object Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.unshift(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.unshift(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or the objects of a multiple selection + * to the top of the stack of drawn objects + * @param {fabric.Object} object Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, objs; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.push(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.push(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * Moves an object or a selection down in stack of drawn objects + * An optional paramter, intersecting allowes to move the object in behind + * the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx > 0 + objsMoved) { + newIdx = idx - 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== 0) { + // if object is not on the bottom of stack + newIdx = this._findNewLowerIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + var newIdx, i; + + if (intersecting) { + newIdx = idx; + + // traverse down the stack looking for the nearest intersecting object + for (i = idx - 1; i >= 0; --i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx - 1; + } + + return newIdx; + }, + + /** + * Moves an object or a selection up in stack of drawn objects + * An optional paramter, intersecting allowes to move the object in front + * of the first intersecting object. Where intersection is calculated with + * bounding box. If no intersection is found, there will not be change in the + * stack. + * @param {fabric.Object} object Object to send + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object, intersecting) { + if (!object) { + return this; + } + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; + + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx < this._objects.length - 1 - objsMoved) { + newIdx = idx + 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + objsMoved++; + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== this._objects.length - 1) { + // if object is not on top of stack (last item in an array) + newIdx = this._findNewUpperIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + }, + + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + var newIdx, i, len; + + if (intersecting) { + newIdx = idx; + + // traverse up the stack looking for the nearest intersecting object + for (i = idx + 1, len = this._objects.length; i < len; ++i) { + + var isIntersecting = object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; + } + } + } + else { + newIdx = idx + 1; + } + + return newIdx; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {fabric.Object} object Object to send + * @param {Number} index Position to move to + * @return {fabric.Canvas} thisArg + * @chainable + */ + moveTo: function (object, index) { + removeFromArray(this._objects, object); + this._objects.splice(index, 0, object); + return this.renderOnAddRemove && this.requestRenderAll(); + }, + + /** + * Clears a canvas element and dispose objects + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + // cancel eventually ongoing renders + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + this.forEachObject(function(object) { + object.dispose && object.dispose(); + }); + this._objects = []; + if (this.backgroundImage && this.backgroundImage.dispose) { + this.backgroundImage.dispose(); + } + this.backgroundImage = null; + if (this.overlayImage && this.overlayImage.dispose) { + this.overlayImage.dispose(); + } + this.overlayImage = null; + this._iTextInstances = null; + this.contextContainer = null; + fabric.util.cleanUpJsdomNode(this.lowerCanvasEl); + this.lowerCanvasEl = undefined; + return this; + }, + + /** + * Returns a string representation of an instance + * @return {String} string representation of an instance + */ + toString: function () { + return '#'; + } + }); + + extend(fabric.StaticCanvas.prototype, fabric.Observable); + extend(fabric.StaticCanvas.prototype, fabric.Collection); + extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); + + extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { + + /** + * @static + * @type String + * @default + */ + EMPTY_JSON: '{"objects": [], "background": "white"}', + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @param {String} methodName Method to check support for; + * Could be one of "setLineDash" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = createCanvasElement(); + + if (!el || !el.getContext) { + return null; + } + + var ctx = el.getContext('2d'); + if (!ctx) { + return null; + } + + switch (methodName) { + + case 'setLineDash': + return typeof ctx.setLineDash !== 'undefined'; + + default: + return null; + } + } + }); + + /** + * Returns JSON representation of canvas + * @function + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {String} JSON string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} + * @example JSON without additional properties + * var json = canvas.toJSON(); + * @example JSON with additional properties included + * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY', 'lockUniScaling']); + * @example JSON without default values + * canvas.includeDefaultValues = false; + * var json = canvas.toJSON(); + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + + if (fabric.isLikelyNode) { + fabric.StaticCanvas.prototype.createPNGStream = function() { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createPNGStream(); + }; + fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { + var impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createJPEGStream(opts); + }; + } +})(); +/** + * BaseBrush class + * @class fabric.BaseBrush + * @see {@link http://fabricjs.com/freedrawing|Freedrawing demo} + */ +fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ { + + /** + * Color of a brush + * @type String + * @default + */ + color: 'rgb(0, 0, 0)', + + /** + * Width of a brush, has to be a Number, no string literals + * @type Number + * @default + */ + width: 1, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property replaces "shadowColor" (String), "shadowOffsetX" (Number), + * "shadowOffsetY" (Number) and "shadowBlur" (Number) since v1.2.12 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Line endings style of a brush (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'round', + + /** + * Corner style of a brush (one of "bevel", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'round', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of a brush's + * @type Number + * @default + */ + strokeMiterLimit: 10, + + /** + * Stroke Dash Array. + * @type Array + * @default + */ + strokeDashArray: null, + + /** + * Sets shadow of an object + * @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)") + * @return {fabric.Object} thisArg + * @chainable + */ + setShadow: function(options) { + this.shadow = new fabric.Shadow(options); + return this; + }, + + /** + * Sets brush styles + * @private + */ + _setBrushStyles: function() { + var ctx = this.canvas.contextTop; + ctx.strokeStyle = this.color; + ctx.lineWidth = this.width; + ctx.lineCap = this.strokeLineCap; + ctx.miterLimit = this.strokeMiterLimit; + ctx.lineJoin = this.strokeLineJoin; + if (fabric.StaticCanvas.supports('setLineDash')) { + ctx.setLineDash(this.strokeDashArray || []); + } + }, + + /** + * Sets the transformation on given context + * @param {RenderingContext2d} ctx context to render on + * @private + */ + _saveAndTransform: function(ctx) { + var v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + }, + + /** + * Sets brush shadow styles + * @private + */ + _setShadow: function() { + if (!this.shadow) { + return; + } + + var ctx = this.canvas.contextTop, + zoom = this.canvas.getZoom(); + + ctx.shadowColor = this.shadow.color; + ctx.shadowBlur = this.shadow.blur * zoom; + ctx.shadowOffsetX = this.shadow.offsetX * zoom; + ctx.shadowOffsetY = this.shadow.offsetY * zoom; + }, + + needsFullRender: function() { + var color = new fabric.Color(this.color); + return color.getAlpha() < 1 || !!this.shadow; + }, + + /** + * Removes brush shadow styles + * @private + */ + _resetShadow: function() { + var ctx = this.canvas.contextTop; + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + } +}); +(function() { + /** + * PencilBrush class + * @class fabric.PencilBrush + * @extends fabric.BaseBrush + */ + fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ { + + /** + * Discard points that are less than `decimate` pixel distant from each other + * @type Number + * @default 0.4 + */ + decimate: 0.4, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.PencilBrush} Instance of a pencil brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this._points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + _drawSegment: function (ctx, p1, p2) { + var midPoint = p1.midPointFrom(p2); + ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); + return midPoint; + }, + + /** + * Inovoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + this._prepareForDrawing(pointer); + // capture coordinates immediately + // this allows to draw dots (when movement never occurs) + this._captureDrawingPath(pointer); + this._render(); + }, + + /** + * Inovoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + if (this._captureDrawingPath(pointer) && this._points.length > 1) { + if (this.needsFullRender()) { + // redraw curve + // clear top canvas + this.canvas.clearContext(this.canvas.contextTop); + this._render(); + } + else { + var points = this._points, length = points.length, ctx = this.canvas.contextTop; + // draw the curve update + this._saveAndTransform(ctx); + if (this.oldEnd) { + ctx.beginPath(); + ctx.moveTo(this.oldEnd.x, this.oldEnd.y); + } + this.oldEnd = this._drawSegment(ctx, points[length - 2], points[length - 1], true); + ctx.stroke(); + ctx.restore(); + } + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function(options) { + if (!this.canvas._isMainEvent(options.e)) { + return true; + } + this.oldEnd = undefined; + this._finalizeAndAddPath(); + return false; + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _prepareForDrawing: function(pointer) { + + var p = new fabric.Point(pointer.x, pointer.y); + + this._reset(); + this._addPoint(p); + this.canvas.contextTop.moveTo(p.x, p.y); + }, + + /** + * @private + * @param {fabric.Point} point Point to be added to points array + */ + _addPoint: function(point) { + if (this._points.length > 1 && point.eq(this._points[this._points.length - 1])) { + return false; + } + this._points.push(point); + return true; + }, + + /** + * Clear points array and set contextTop canvas style. + * @private + */ + _reset: function() { + this._points = []; + this._setBrushStyles(); + this._setShadow(); + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _captureDrawingPath: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y); + return this._addPoint(pointerPoint); + }, + + /** + * Draw a smooth path on the topCanvas using quadraticCurveTo + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + p1 = this._points[0], + p2 = this._points[1]; + + this._saveAndTransform(ctx); + ctx.beginPath(); + //if we only have 2 points in the path and they are the same + //it means that the user only clicked the canvas without moving the mouse + //then we should be drawing a dot. A path isn't drawn between two identical dots + //that's why we set them apart a bit + if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { + var width = this.width / 1000; + p1 = new fabric.Point(p1.x, p1.y); + p2 = new fabric.Point(p2.x, p2.y); + p1.x -= width; + p2.x += width; + } + ctx.moveTo(p1.x, p1.y); + + for (i = 1, len = this._points.length; i < len; i++) { + // we pick the point between pi + 1 & pi + 2 as the + // end point and p1 as our control point. + this._drawSegment(ctx, p1, p2); + p1 = this._points[i]; + p2 = this._points[i + 1]; + } + // Draw last line as a straight line while + // we wait for the next point to be able to calculate + // the bezier control point + ctx.lineTo(p1.x, p1.y); + ctx.stroke(); + ctx.restore(); + }, + + /** + * Converts points to SVG path + * @param {Array} points Array of points + * @return {String} SVG path + */ + convertPointsToSVGPath: function(points) { + var path = [], i, width = this.width / 1000, + p1 = new fabric.Point(points[0].x, points[0].y), + p2 = new fabric.Point(points[1].x, points[1].y), + len = points.length, multSignX = 1, multSignY = 0, manyPoints = len > 2; + + if (manyPoints) { + multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1; + multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1; + } + path.push('M ', p1.x - multSignX * width, ' ', p1.y - multSignY * width, ' '); + for (i = 1; i < len; i++) { + if (!p1.eq(p2)) { + var midPoint = p1.midPointFrom(p2); + // p1 is our bezier control point + // midpoint is our endpoint + // start point is p(i-1) value. + path.push('Q ', p1.x, ' ', p1.y, ' ', midPoint.x, ' ', midPoint.y, ' '); + } + p1 = points[i]; + if ((i + 1) < points.length) { + p2 = points[i + 1]; + } + } + if (manyPoints) { + multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1; + multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1; + } + path.push('L ', p1.x + multSignX * width, ' ', p1.y + multSignY * width); + return path; + }, + + /** + * Creates fabric.Path object to add on canvas + * @param {String} pathData Path data + * @return {fabric.Path} Path to add on canvas + */ + createPath: function(pathData) { + var path = new fabric.Path(pathData, { + fill: null, + stroke: this.color, + strokeWidth: this.width, + strokeLineCap: this.strokeLineCap, + strokeMiterLimit: this.strokeMiterLimit, + strokeLineJoin: this.strokeLineJoin, + strokeDashArray: this.strokeDashArray, + }); + if (this.shadow) { + this.shadow.affectStroke = true; + path.setShadow(this.shadow); + } + + return path; + }, + + /** + * Decimate poins array with the decimate value + */ + decimatePoints: function(points, distance) { + if (points.length <= 2) { + return points; + } + var zoom = this.canvas.getZoom(), adjustedDistance = Math.pow(distance / zoom, 2), + i, l = points.length - 1, lastPoint = points[0], newPoints = [lastPoint], + cDistance; + for (i = 1; i < l; i++) { + cDistance = Math.pow(lastPoint.x - points[i].x, 2) + Math.pow(lastPoint.y - points[i].y, 2); + if (cDistance >= adjustedDistance) { + lastPoint = points[i]; + newPoints.push(lastPoint); + } + } + if (newPoints.length === 1) { + newPoints.push(new fabric.Point(newPoints[0].x, newPoints[0].y)); + } + return newPoints; + }, + + /** + * On mouseup after drawing the path on contextTop canvas + * we use the points captured to create an new fabric path object + * and add it to the fabric canvas. + */ + _finalizeAndAddPath: function() { + var ctx = this.canvas.contextTop; + ctx.closePath(); + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + var pathData = this.convertPointsToSVGPath(this._points).join(''); + if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { + // do not create 0 width/height paths, as they are + // rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, + // whereas Chrome 10 renders nothing + this.canvas.requestRenderAll(); + return; + } + + var path = this.createPath(pathData); + this.canvas.clearContext(this.canvas.contextTop); + this.canvas.add(path); + this.canvas.renderAll(); + path.setCoords(); + this._resetShadow(); + + + // fire event 'path' created + this.canvas.fire('path:created', { path: path }); + } + }); +})(); +/** + * CircleBrush class + * @class fabric.CircleBrush + */ +fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { + + /** + * Width of a brush + * @type Number + * @default + */ + width: 10, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.CircleBrush} Instance of a circle brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.points = []; + }, + + /** + * Invoked inside on mouse down and mouse move + * @param {Object} pointer + */ + drawDot: function(pointer) { + var point = this.addPoint(pointer), + ctx = this.canvas.contextTop; + this._saveAndTransform(ctx); + this.dot(ctx, point); + ctx.restore(); + }, + + dot: function(ctx, point) { + ctx.fillStyle = point.fill; + ctx.beginPath(); + ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fill(); + }, + + /** + * Invoked on mouse down + */ + onMouseDown: function(pointer) { + this.points.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + this.drawDot(pointer); + }, + + /** + * Render the full state of the brush + * @private + */ + _render: function() { + var ctx = this.canvas.contextTop, i, len, + points = this.points; + this._saveAndTransform(ctx); + for (i = 0, len = points.length; i < len; i++) { + this.dot(ctx, points[i]); + } + ctx.restore(); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + if (this.needsFullRender()) { + this.canvas.clearContext(this.canvas.contextTop); + this.addPoint(pointer); + this._render(); + } + else { + this.drawDot(pointer); + } + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove, i, len; + this.canvas.renderOnAddRemove = false; + + var circles = []; + + for (i = 0, len = this.points.length; i < len; i++) { + var point = this.points[i], + circle = new fabric.Circle({ + radius: point.radius, + left: point.x, + top: point.y, + originX: 'center', + originY: 'center', + fill: point.fill + }); + + this.shadow && circle.setShadow(this.shadow); + + circles.push(circle); + } + var group = new fabric.Group(circles); + group.canvas = this.canvas; + + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @param {Object} pointer + * @return {fabric.Point} Just added pointer point + */ + addPoint: function(pointer) { + var pointerPoint = new fabric.Point(pointer.x, pointer.y), + + circleRadius = fabric.util.getRandomInt( + Math.max(0, this.width - 20), this.width + 20) / 2, + + circleColor = new fabric.Color(this.color) + .setAlpha(fabric.util.getRandomInt(0, 100) / 100) + .toRgba(); + + pointerPoint.radius = circleRadius; + pointerPoint.fill = circleColor; + + this.points.push(pointerPoint); + + return pointerPoint; + } +}); +/** + * SprayBrush class + * @class fabric.SprayBrush + */ +fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ { + + /** + * Width of a spray + * @type Number + * @default + */ + width: 10, + + /** + * Density of a spray (number of dots per chunk) + * @type Number + * @default + */ + density: 20, + + /** + * Width of spray dots + * @type Number + * @default + */ + dotWidth: 1, + + /** + * Width variance of spray dots + * @type Number + * @default + */ + dotWidthVariance: 1, + + /** + * Whether opacity of a dot should be random + * @type Boolean + * @default + */ + randomOpacity: false, + + /** + * Whether overlapping dots (rectangles) should be removed (for performance reasons) + * @type Boolean + * @default + */ + optimizeOverlapping: true, + + /** + * Constructor + * @param {fabric.Canvas} canvas + * @return {fabric.SprayBrush} Instance of a spray brush + */ + initialize: function(canvas) { + this.canvas = canvas; + this.sprayChunks = []; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer) { + this.sprayChunks.length = 0; + this.canvas.clearContext(this.canvas.contextTop); + this._setShadow(); + + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse move + * @param {Object} pointer + */ + onMouseMove: function(pointer) { + this.addSprayChunk(pointer); + this.render(this.sprayChunkPoints); + }, + + /** + * Invoked on mouse up + */ + onMouseUp: function() { + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; + this.canvas.renderOnAddRemove = false; + + var rects = []; + + for (var i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + var sprayChunk = this.sprayChunks[i]; + + for (var j = 0, jlen = sprayChunk.length; j < jlen; j++) { + + var rect = new fabric.Rect({ + width: sprayChunk[j].width, + height: sprayChunk[j].width, + left: sprayChunk[j].x + 1, + top: sprayChunk[j].y + 1, + originX: 'center', + originY: 'center', + fill: this.color + }); + rects.push(rect); + } + } + + if (this.optimizeOverlapping) { + rects = this._getOptimizedRects(rects); + } + + var group = new fabric.Group(rects); + this.shadow && group.setShadow(this.shadow); + this.canvas.add(group); + this.canvas.fire('path:created', { path: group }); + + this.canvas.clearContext(this.canvas.contextTop); + this._resetShadow(); + this.canvas.renderOnAddRemove = originalRenderOnAddRemove; + this.canvas.requestRenderAll(); + }, + + /** + * @private + * @param {Array} rects + */ + _getOptimizedRects: function(rects) { + + // avoid creating duplicate rects at the same coordinates + var uniqueRects = { }, key, i, len; + + for (i = 0, len = rects.length; i < len; i++) { + key = rects[i].left + '' + rects[i].top; + if (!uniqueRects[key]) { + uniqueRects[key] = rects[i]; + } + } + var uniqueRectsArray = []; + for (key in uniqueRects) { + uniqueRectsArray.push(uniqueRects[key]); + } + + return uniqueRectsArray; + }, + + /** + * Render new chunk of spray brush + */ + render: function(sprayChunk) { + var ctx = this.canvas.contextTop, i, len; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, len = sprayChunk.length; i < len; i++) { + var point = sprayChunk[i]; + if (typeof point.opacity !== 'undefined') { + ctx.globalAlpha = point.opacity; + } + ctx.fillRect(point.x, point.y, point.width, point.width); + } + ctx.restore(); + }, + + /** + * Render all spray chunks + */ + _render: function() { + var ctx = this.canvas.contextTop, i, ilen; + ctx.fillStyle = this.color; + + this._saveAndTransform(ctx); + + for (i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { + this.render(this.sprayChunks[i]); + } + ctx.restore(); + }, + + /** + * @param {Object} pointer + */ + addSprayChunk: function(pointer) { + this.sprayChunkPoints = []; + + var x, y, width, radius = this.width / 2, i; + + for (i = 0; i < this.density; i++) { + + x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius); + y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius); + + if (this.dotWidthVariance) { + width = fabric.util.getRandomInt( + // bottom clamp width to 1 + Math.max(1, this.dotWidth - this.dotWidthVariance), + this.dotWidth + this.dotWidthVariance); + } + else { + width = this.dotWidth; + } + + var point = new fabric.Point(x, y); + point.width = width; + + if (this.randomOpacity) { + point.opacity = fabric.util.getRandomInt(0, 100) / 100; + } + + this.sprayChunkPoints.push(point); + } + + this.sprayChunks.push(this.sprayChunkPoints); + } +}); +/** + * PatternBrush class + * @class fabric.PatternBrush + * @extends fabric.BaseBrush + */ +fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ { + + getPatternSrc: function() { + + var dotWidth = 20, + dotDistance = 5, + patternCanvas = fabric.util.createCanvasElement(), + patternCtx = patternCanvas.getContext('2d'); + + patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; + + patternCtx.fillStyle = this.color; + patternCtx.beginPath(); + patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false); + patternCtx.closePath(); + patternCtx.fill(); + + return patternCanvas; + }, + + getPatternSrcFunction: function() { + return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"'); + }, + + /** + * Creates "pattern" instance property + */ + getPattern: function() { + return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat'); + }, + + /** + * Sets brush styles + */ + _setBrushStyles: function() { + this.callSuper('_setBrushStyles'); + this.canvas.contextTop.strokeStyle = this.getPattern(); + }, + + /** + * Creates path + */ + createPath: function(pathData) { + var path = this.callSuper('createPath', pathData), + topLeft = path._getLeftTopCoords().scalarAdd(path.strokeWidth / 2); + + path.stroke = new fabric.Pattern({ + source: this.source || this.getPatternSrcFunction(), + offsetX: -topLeft.x, + offsetY: -topLeft.y + }); + return path; + } +}); +(function() { + + var getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + radiansToDegrees = fabric.util.radiansToDegrees, + atan2 = Math.atan2, + abs = Math.abs, + supportLineDash = fabric.StaticCanvas.supports('setLineDash'), + + STROKE_OFFSET = 0.5; + + /** + * Canvas class + * @class fabric.Canvas + * @extends fabric.StaticCanvas + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition + * + * @fires object:modified + * @fires object:rotated + * @fires object:scaled + * @fires object:moved + * @fires object:skewed + * @fires object:rotating + * @fires object:scaling + * @fires object:moving + * @fires object:skewing + * @fires object:selected this event is deprecated. use selection:created + * + * @fires before:transform + * @fires before:selection:cleared + * @fires selection:cleared + * @fires selection:updated + * @fires selection:created + * + * @fires path:created + * @fires mouse:down + * @fires mouse:move + * @fires mouse:up + * @fires mouse:down:before + * @fires mouse:move:before + * @fires mouse:up:before + * @fires mouse:over + * @fires mouse:out + * @fires mouse:dblclick + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + * + */ + fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { + + /** + * Constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(el, options) { + options || (options = { }); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + this._initInteractive(); + this._createCacheCanvas(); + }, + + /** + * When true, objects can be transformed by one side (unproportionally) + * @type Boolean + * @default + */ + uniScaleTransform: false, + + /** + * Indicates which key enable unproportional scaling + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + uniScaleKey: 'shiftKey', + + /** + * When true, objects use center point as the origin of scale transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, objects use center point as the origin of rotate transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: false, + + /** + * Indicates which key enable centered Transform + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + centeredKey: 'altKey', + + /** + * Indicates which key enable alternate action on corner + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + altActionKey: 'shiftKey', + + /** + * Indicates that canvas is interactive. This property should not be changed. + * @type Boolean + * @default + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @type Boolean + * @default + */ + selection: true, + + /** + * Indicates which key or keys enable multiple click selection + * Pass value as a string or array of strings + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or empty or containing any other string that is not a modifier key + * feature is disabled. + * @since 1.6.2 + * @type String|Array + * @default + */ + selectionKey: 'shiftKey', + + /** + * Indicates which key enable alternative selection + * in case of target overlapping with active object + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * For a series of reason that come from the general expectations on how + * things should work, this feature works only for preserveObjectStacking true. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled. + * @since 1.6.5 + * @type null|String + * @default + */ + altSelectionKey: null, + + /** + * Color of selection + * @type String + * @default + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Default dash array pattern + * If not empty the selection border is dashed + * @type Array + */ + selectionDashArray: [], + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @type String + * @default + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @type Number + * @default + */ + selectionLineWidth: 1, + + /** + * Select only shapes that are fully contained in the dragged selection rectangle. + * @type Boolean + * @default + */ + selectionFullyContained: false, + + /** + * Default cursor value used when hovering over an object on canvas + * @type String + * @default + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @type String + * @default + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @type String + * @default + */ + defaultCursor: 'default', + + /** + * Cursor value used during free drawing + * @type String + * @default + */ + freeDrawingCursor: 'crosshair', + + /** + * Cursor value used for rotation point + * @type String + * @default + */ + rotationCursor: 'crosshair', + + /** + * Cursor value used for disabled elements ( corners with disabled action ) + * @type String + * @since 2.0.0 + * @default + */ + notAllowedCursor: 'not-allowed', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @type String + * @default + */ + containerClass: 'canvas-container', + + /** + * When true, object detection happens on per-pixel basis rather than on per-bounding-box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * Number of pixels around target pixel to tolerate (consider active) during object detection + * @type Number + * @default + */ + targetFindTolerance: 0, + + /** + * When true, target detection is skipped when hovering over canvas. This can be used to improve performance. + * @type Boolean + * @default + */ + skipTargetFind: false, + + /** + * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. + * After mousedown, mousemove creates a shape, + * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. + * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} + * @type Boolean + * @default + */ + isDrawingMode: false, + + /** + * Indicates whether objects should remain in current stack position when selected. + * When false objects are brought to top and rendered as part of the selection group + * @type Boolean + * @default + */ + preserveObjectStacking: false, + + /** + * Indicates the angle that an object will lock to while rotating. + * @type Number + * @since 1.6.7 + * @default + */ + snapAngle: 0, + + /** + * Indicates the distance from the snapAngle the rotation will lock to the snapAngle. + * When `null`, the snapThreshold will default to the snapAngle. + * @type null|Number + * @since 1.6.7 + * @default + */ + snapThreshold: null, + + /** + * Indicates if the right click on canvas can output the context menu or not + * @type Boolean + * @since 1.6.5 + * @default + */ + stopContextMenu: false, + + /** + * Indicates if the canvas can fire right click events + * @type Boolean + * @since 1.6.5 + * @default + */ + fireRightClick: false, + + /** + * Indicates if the canvas can fire middle click events + * @type Boolean + * @since 1.7.8 + * @default + */ + fireMiddleClick: false, + + /** + * @private + */ + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEventListeners(); + + this._initRetinaScaling(); + + this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); + + this.calcOffset(); + }, + + /** + * Divides objects in two groups, one to render immediately + * and one to render as activeGroup. + * @return {Array} objects to render immediately and pushes the other in the activeGroup. + */ + _chooseObjectsToRender: function() { + var activeObjects = this.getActiveObjects(), + object, objsToRender, activeGroupObjects; + + if (activeObjects.length > 0 && !this.preserveObjectStacking) { + objsToRender = []; + activeGroupObjects = []; + for (var i = 0, length = this._objects.length; i < length; i++) { + object = this._objects[i]; + if (activeObjects.indexOf(object) === -1 ) { + objsToRender.push(object); + } + else { + activeGroupObjects.push(object); + } + } + if (activeObjects.length > 1) { + this._activeObject._objects = activeGroupObjects; + } + objsToRender.push.apply(objsToRender, activeGroupObjects); + } + else { + objsToRender = this._objects; + } + return objsToRender; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { + this.clearContext(this.contextTop); + this.contextTopDirty = false; + } + if (this.hasLostContext) { + this.renderTopLayer(this.contextTop); + } + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + return this; + }, + + renderTopLayer: function(ctx) { + ctx.save(); + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this.freeDrawingBrush && this.freeDrawingBrush._render(); + this.contextTopDirty = true; + } + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(ctx); + this.contextTopDirty = true; + } + ctx.restore(); + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + var ctx = this.contextTop; + this.clearContext(ctx); + this.renderTopLayer(ctx); + this.fire('after:render'); + return this; + }, + + /** + * Resets the current transform to its original values and chooses the type of resizing based on the event + * @private + */ + _resetCurrentTransform: function() { + var t = this._currentTransform; + + t.target.set({ + scaleX: t.original.scaleX, + scaleY: t.original.scaleY, + skewX: t.original.skewX, + skewY: t.original.skewY, + left: t.original.left, + top: t.original.top + }); + + if (this._shouldCenterTransform(t.target)) { + if (t.originX !== 'center') { + if (t.originX === 'right') { + t.mouseXSign = -1; + } + else { + t.mouseXSign = 1; + } + } + if (t.originY !== 'center') { + if (t.originY === 'bottom') { + t.mouseYSign = -1; + } + else { + t.mouseYSign = 1; + } + } + + t.originX = 'center'; + t.originY = 'center'; + } + else { + t.originX = t.original.originX; + t.originY = t.original.originY; + } + }, + + /** + * Checks if point is contained within an area of given object + * @param {Event} e Event object + * @param {fabric.Object} target Object to test against + * @param {Object} [point] x,y object of point coordinates we want to check. + * @return {Boolean} true if point is contained within an area of given object + */ + containsPoint: function (e, target, point) { + var ignoreZoom = true, + pointer = point || this.getPointer(e, ignoreZoom), + xy; + + if (target.group && target.group === this._activeObject && target.group.type === 'activeSelection') { + xy = this._normalizePointer(target.group, pointer); + } + else { + xy = { x: pointer.x, y: pointer.y }; + } + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + return (target.containsPoint(xy) || target._findTargetCorner(pointer)); + }, + + /** + * @private + */ + _normalizePointer: function (object, pointer) { + var m = object.calcTransformMatrix(), + invertedM = fabric.util.invertTransform(m), + vptPointer = this.restorePointerVpt(pointer); + return fabric.util.transformPoint(vptPointer, invertedM); + }, + + /** + * Returns true if object is transparent at a certain location + * @param {fabric.Object} target Object to check + * @param {Number} x Left coordinate + * @param {Number} y Top coordinate + * @return {Boolean} + */ + isTargetTransparent: function (target, x, y) { + // in case the target is the activeObject, we cannot execute this optimization + // because we need to draw controls too. + if (target.shouldCache() && target._cacheCanvas && target !== this._activeObject) { + var normalizedPointer = this._normalizePointer(target, {x: x, y: y}), + targetRelativeX = Math.max(target.cacheTranslationX + (normalizedPointer.x * target.zoomX), 0), + targetRelativeY = Math.max(target.cacheTranslationY + (normalizedPointer.y * target.zoomY), 0); + + var isTransparent = fabric.util.isTransparent( + target._cacheContext, Math.round(targetRelativeX), Math.round(targetRelativeY), this.targetFindTolerance); + + return isTransparent; + } + + var ctx = this.contextCache, + originalColor = target.selectionBackgroundColor, v = this.viewportTransform; + + target.selectionBackgroundColor = ''; + + this.clearContext(ctx); + + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + target.render(ctx); + ctx.restore(); + + target === this._activeObject && target._renderControls(ctx, { + hasBorders: false, + transparentCorners: false + }, { + hasBorders: false, + }); + + target.selectionBackgroundColor = originalColor; + + var isTransparent = fabric.util.isTransparent( + ctx, x, y, this.targetFindTolerance); + + return isTransparent; + }, + + /** + * takes an event and determins if selection key has been pressed + * @private + * @param {Event} e Event object + */ + _isSelectionKeyPressed: function(e) { + var selectionKeyPressed = false; + + if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { + selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); + } + else { + selectionKeyPressed = e[this.selectionKey]; + } + + return selectionKeyPressed; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldClearSelection: function (e, target) { + var activeObjects = this.getActiveObjects(), + activeObject = this._activeObject; + + return ( + !target + || + (target && + activeObject && + activeObjects.length > 1 && + activeObjects.indexOf(target) === -1 && + activeObject !== target && + !this._isSelectionKeyPressed(e)) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) + ); + }, + + /** + * centeredScaling from object can't override centeredScaling from canvas. + * this should be fixed, since object setting should take precedence over canvas. + * @private + * @param {fabric.Object} target + */ + _shouldCenterTransform: function (target) { + if (!target) { + return; + } + + var t = this._currentTransform, + centerTransform; + + if (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY') { + centerTransform = this.centeredScaling || target.centeredScaling; + } + else if (t.action === 'rotate') { + centerTransform = this.centeredRotation || target.centeredRotation; + } + + return centerTransform ? !t.altKey : t.altKey; + }, + + /** + * @private + */ + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; + + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } + + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + + return origin; + }, + + /** + * @private + * @param {Boolean} alreadySelected true if target is already selected + * @param {String} corner a string representing the corner ml, mr, tl ... + * @param {Event} e Event object + * @param {fabric.Object} [target] inserted back to help overriding. Unused + */ + _getActionFromCorner: function(alreadySelected, corner, e /* target */) { + if (!corner || !alreadySelected) { + return 'drag'; + } + + switch (corner) { + case 'mtr': + return 'rotate'; + case 'ml': + case 'mr': + return e[this.altActionKey] ? 'skewY' : 'scaleX'; + case 'mt': + case 'mb': + return e[this.altActionKey] ? 'skewX' : 'scaleY'; + default: + return 'scale'; + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target, alreadySelected) { + if (!target) { + return; + } + + var pointer = this.getPointer(e), + corner = target._findTargetCorner(this.getPointer(e, true)), + action = this._getActionFromCorner(alreadySelected, corner, e, target), + origin = this._getOriginFromCorner(target, corner); + + this._currentTransform = { + target: target, + action: action, + corner: corner, + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + // used by transation + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + originX: origin.x, + originY: origin.y, + ex: pointer.x, + ey: pointer.y, + lastX: pointer.x, + lastY: pointer.y, + // unsure they are usefull anymore. + // left: target.left, + // top: target.top, + theta: degreesToRadians(target.angle), + // end of unsure + width: target.width * target.scaleX, + mouseXSign: 1, + mouseYSign: 1, + shiftKey: e.shiftKey, + altKey: e[this.centeredKey], + original: fabric.util.saveObjectTransform(target), + }; + + this._currentTransform.original.originX = origin.x; + this._currentTransform.original.originY = origin.y; + + this._resetCurrentTransform(); + this._beforeTransform(e); + }, + + /** + * Translates object by "setting" its left/top + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @return {Boolean} true if the translation occurred + */ + _translateObject: function (x, y) { + var transform = this._currentTransform, + target = transform.target, + newLeft = x - transform.offsetX, + newTop = y - transform.offsetY, + moveX = !target.get('lockMovementX') && target.left !== newLeft, + moveY = !target.get('lockMovementY') && target.top !== newTop; + + moveX && target.set('left', newLeft); + moveY && target.set('top', newTop); + return moveX || moveY; + }, + + /** + * Check if we are increasing a positive skew or lower it, + * checking mouse direction and pressed corner. + * @private + */ + _changeSkewTransformOrigin: function(mouseMove, t, by) { + var property = 'originX', origins = { 0: 'center' }, + skew = t.target.skewX, originA = 'left', originB = 'right', + corner = t.corner === 'mt' || t.corner === 'ml' ? 1 : -1, + flipSign = 1; + + mouseMove = mouseMove > 0 ? 1 : -1; + if (by === 'y') { + skew = t.target.skewY; + originA = 'top'; + originB = 'bottom'; + property = 'originY'; + } + origins[-1] = originA; + origins[1] = originB; + + t.target.flipX && (flipSign *= -1); + t.target.flipY && (flipSign *= -1); + + if (skew === 0) { + t.skewSign = -corner * mouseMove * flipSign; + t[property] = origins[-mouseMove]; + } + else { + skew = skew > 0 ? 1 : -1; + t.skewSign = skew; + t[property] = origins[skew * corner * flipSign]; + } + }, + + /** + * Skew object by mouse events + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @param {String} by Either 'x' or 'y' + * @return {Boolean} true if the skewing occurred + */ + _skewObject: function (x, y, by) { + var t = this._currentTransform, + target = t.target, skewed = false, + lockSkewingX = target.get('lockSkewingX'), + lockSkewingY = target.get('lockSkewingY'); + + if ((lockSkewingX && by === 'x') || (lockSkewingY && by === 'y')) { + return false; + } + + // Get the constraint point + var center = target.getCenterPoint(), + actualMouseByCenter = target.toLocalPoint(new fabric.Point(x, y), 'center', 'center')[by], + lastMouseByCenter = target.toLocalPoint(new fabric.Point(t.lastX, t.lastY), 'center', 'center')[by], + actualMouseByOrigin, constraintPosition, dim = target._getTransformedDimensions(); + + this._changeSkewTransformOrigin(actualMouseByCenter - lastMouseByCenter, t, by); + actualMouseByOrigin = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY)[by]; + constraintPosition = target.translateToOriginPoint(center, t.originX, t.originY); + // Actually skew the object + skewed = this._setObjectSkew(actualMouseByOrigin, t, by, dim); + t.lastX = x; + t.lastY = y; + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + return skewed; + }, + + /** + * Set object skew + * @private + * @return {Boolean} true if the skewing occurred + */ + _setObjectSkew: function(localMouse, transform, by, _dim) { + var target = transform.target, newValue, skewed = false, + skewSign = transform.skewSign, newDim, dimNoSkew, + otherBy, _otherBy, _by, newDimMouse, skewX, skewY; + + if (by === 'x') { + otherBy = 'y'; + _otherBy = 'Y'; + _by = 'X'; + skewX = 0; + skewY = target.skewY; + } + else { + otherBy = 'x'; + _otherBy = 'X'; + _by = 'Y'; + skewX = target.skewX; + skewY = 0; + } + + dimNoSkew = target._getTransformedDimensions(skewX, skewY); + newDimMouse = 2 * Math.abs(localMouse) - dimNoSkew[by]; + if (newDimMouse <= 2) { + newValue = 0; + } + else { + newValue = skewSign * Math.atan((newDimMouse / target['scale' + _by]) / + (dimNoSkew[otherBy] / target['scale' + _otherBy])); + newValue = fabric.util.radiansToDegrees(newValue); + } + skewed = target['skew' + _by] !== newValue; + target.set('skew' + _by, newValue); + if (target['skew' + _otherBy] !== 0) { + newDim = target._getTransformedDimensions(); + newValue = (_dim[otherBy] / newDim[otherBy]) * target['scale' + _otherBy]; + target.set('scale' + _otherBy, newValue); + } + return skewed; + }, + + /** + * Scales object by invoking its scaleX/scaleY methods + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @param {String} by Either 'x' or 'y' - specifies dimension constraint by which to scale an object. + * When not provided, an object is scaled by both dimensions equally + * @return {Boolean} true if the scaling occurred + */ + _scaleObject: function (x, y, by) { + var t = this._currentTransform, + target = t.target, + lockScalingX = target.lockScalingX, + lockScalingY = target.lockScalingY, + lockScalingFlip = target.lockScalingFlip; + + if (lockScalingX && lockScalingY) { + return false; + } + + // Get the constraint point + var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY), + localMouse = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY), + dim = target._getTransformedDimensions(), scaled = false; + + this._setLocalMouse(localMouse, t); + + // Actually scale the object + scaled = this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip, dim); + + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + return scaled; + }, + + /** + * @private + * @return {Boolean} true if the scaling occurred + */ + _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { + var target = transform.target, forbidScalingX = false, forbidScalingY = false, scaled = false, + scaleX = localMouse.x * target.scaleX / _dim.x, + scaleY = localMouse.y * target.scaleY / _dim.y, + changeX = target.scaleX !== scaleX, + changeY = target.scaleY !== scaleY; + + transform.newScaleX = scaleX; + transform.newScaleY = scaleY; + if (by === 'x' && target instanceof fabric.Textbox) { + var w = target.width * (localMouse.x / _dim.x); + if (w >= target.getMinWidth()) { + scaled = w !== target.width; + target.set('width', w); + return scaled; + } + return false; + } + + if (lockScalingFlip && scaleX <= 0 && scaleX < target.scaleX) { + forbidScalingX = true; + localMouse.x = 0; + } + + if (lockScalingFlip && scaleY <= 0 && scaleY < target.scaleY) { + forbidScalingY = true; + localMouse.y = 0; + } + + if (by === 'equally' && !lockScalingX && !lockScalingY) { + scaled = this._scaleObjectEqually(localMouse, target, transform, _dim); + } + else if (!by) { + forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = scaled || changeX)); + forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY)); + } + else if (by === 'x' && !target.get('lockUniScaling')) { + forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = changeX)); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = changeY)); + } + forbidScalingX || forbidScalingY || this._flipObject(transform, by); + return scaled; + }, + + /** + * @private + * @return {Boolean} true if the scaling occurred + */ + _scaleObjectEqually: function(localMouse, target, transform, _dim) { + + var dist = localMouse.y + localMouse.x, + lastDist = _dim.y * transform.original.scaleY / target.scaleY + + _dim.x * transform.original.scaleX / target.scaleX, + scaled, signX = localMouse.x < 0 ? -1 : 1, + signY = localMouse.y < 0 ? -1 : 1, newScaleX, newScaleY; + + // We use transform.scaleX/Y instead of target.scaleX/Y + // because the object may have a min scale and we'll loose the proportions + newScaleX = signX * Math.abs(transform.original.scaleX * dist / lastDist); + newScaleY = signY * Math.abs(transform.original.scaleY * dist / lastDist); + scaled = newScaleX !== target.scaleX || newScaleY !== target.scaleY; + target.set('scaleX', newScaleX); + target.set('scaleY', newScaleY); + return scaled; + }, + + /** + * @private + */ + _flipObject: function(transform, by) { + if (transform.newScaleX < 0 && by !== 'y') { + if (transform.originX === 'left') { + transform.originX = 'right'; + } + else if (transform.originX === 'right') { + transform.originX = 'left'; + } + } + + if (transform.newScaleY < 0 && by !== 'x') { + if (transform.originY === 'top') { + transform.originY = 'bottom'; + } + else if (transform.originY === 'bottom') { + transform.originY = 'top'; + } + } + }, + + /** + * @private + */ + _setLocalMouse: function(localMouse, t) { + var target = t.target, zoom = this.getZoom(), + padding = target.padding / zoom; + + if (t.originX === 'right') { + localMouse.x *= -1; + } + else if (t.originX === 'center') { + localMouse.x *= t.mouseXSign * 2; + if (localMouse.x < 0) { + t.mouseXSign = -t.mouseXSign; + } + } + + if (t.originY === 'bottom') { + localMouse.y *= -1; + } + else if (t.originY === 'center') { + localMouse.y *= t.mouseYSign * 2; + if (localMouse.y < 0) { + t.mouseYSign = -t.mouseYSign; + } + } + + // adjust the mouse coordinates when dealing with padding + if (abs(localMouse.x) > padding) { + if (localMouse.x < 0) { + localMouse.x += padding; + } + else { + localMouse.x -= padding; + } + } + else { // mouse is within the padding, set to 0 + localMouse.x = 0; + } + + if (abs(localMouse.y) > padding) { + if (localMouse.y < 0) { + localMouse.y += padding; + } + else { + localMouse.y -= padding; + } + } + else { + localMouse.y = 0; + } + }, + + /** + * Rotates object by invoking its rotate method + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @return {Boolean} true if the rotation occurred + */ + _rotateObject: function (x, y) { + + var t = this._currentTransform, + target = t.target, constraintPosition, + constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + + if (target.lockRotation) { + return false; + } + + var lastAngle = atan2(t.ey - constraintPosition.y, t.ex - constraintPosition.x), + curAngle = atan2(y - constraintPosition.y, x - constraintPosition.x), + angle = radiansToDegrees(curAngle - lastAngle + t.theta), + hasRotated = true; + + if (target.snapAngle > 0) { + var snapAngle = target.snapAngle, + snapThreshold = target.snapThreshold || snapAngle, + rightAngleLocked = Math.ceil(angle / snapAngle) * snapAngle, + leftAngleLocked = Math.floor(angle / snapAngle) * snapAngle; + + if (Math.abs(angle - leftAngleLocked) < snapThreshold) { + angle = leftAngleLocked; + } + else if (Math.abs(angle - rightAngleLocked) < snapThreshold) { + angle = rightAngleLocked; + } + } + + // normalize angle to positive value + if (angle < 0) { + angle = 360 + angle; + } + angle %= 360; + + if (target.angle === angle) { + hasRotated = false; + } + else { + // rotation only happen here + target.angle = angle; + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + } + + return hasRotated; + }, + + /** + * Set the cursor type of the canvas element + * @param {String} value Cursor type of the canvas element. + * @see http://www.w3.org/TR/css3-ui/#cursor + */ + setCursor: function (value) { + this.upperCanvasEl.style.cursor = value; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx to draw the selection on + */ + _drawSelection: function (ctx) { + var groupSelector = this._groupSelector, + left = groupSelector.left, + top = groupSelector.top, + aleft = abs(left), + atop = abs(top); + + if (this.selectionColor) { + ctx.fillStyle = this.selectionColor; + + ctx.fillRect( + groupSelector.ex - ((left > 0) ? 0 : -left), + groupSelector.ey - ((top > 0) ? 0 : -top), + aleft, + atop + ); + } + + if (!this.selectionLineWidth || !this.selectionBorderColor) { + return; + } + ctx.lineWidth = this.selectionLineWidth; + ctx.strokeStyle = this.selectionBorderColor; + + // selection border + if (this.selectionDashArray.length > 1 && !supportLineDash) { + + var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), + py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop); + + ctx.beginPath(); + + fabric.util.drawDashedLine(ctx, px, py, px + aleft, py, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px, py + atop - 1, px + aleft, py + atop - 1, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px, py, px, py + atop, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px + aleft - 1, py, px + aleft - 1, py + atop, this.selectionDashArray); + + ctx.closePath(); + ctx.stroke(); + } + else { + fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); + ctx.strokeRect( + groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), + groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), + aleft, + atop + ); + } + }, + + /** + * Method that determines what object we are clicking on + * the skipGroup parameter is for internal use, is needed for shift+click action + * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target + * or the outside part of the corner. + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through + * @return {fabric.Object} the target found + */ + findTarget: function (e, skipGroup) { + if (this.skipTargetFind) { + return; + } + + var ignoreZoom = true, + pointer = this.getPointer(e, ignoreZoom), + activeObject = this._activeObject, + aObjects = this.getActiveObjects(), + activeTarget, activeTargetSubs; + + // first check current group (if one exists) + // active group does not check sub targets like normal groups. + // if active group just exits. + this.targets = []; + + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + return activeObject; + } + // if we hit the corner of an activeObject, let's return that. + if (aObjects.length === 1 && activeObject._findTargetCorner(pointer)) { + return activeObject; + } + if (aObjects.length === 1 && + activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (!this.preserveObjectStacking) { + return activeObject; + } + else { + activeTarget = activeObject; + activeTargetSubs = this.targets; + this.targets = []; + } + } + var target = this._searchPossibleTargets(this._objects, pointer); + if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { + target = activeTarget; + this.targets = activeTargetSubs; + } + return target; + }, + + /** + * Checks point is inside the object. + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @param {fabric.Object} obj Object to test against + * @param {Object} [globalPointer] x,y object of point coordinates relative to canvas used to search per pixel target. + * @return {Boolean} true if point is contained within an area of given object + * @private + */ + _checkTarget: function(pointer, obj, globalPointer) { + if (obj && + obj.visible && + obj.evented && + this.containsPoint(null, obj, pointer)){ + if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { + var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); + if (!isTransparent) { + return true; + } + } + else { + return true; + } + } + }, + + /** + * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted + * @param {Array} [objects] objects array to look into + * @param {Object} [pointer] x,y object of point coordinates we want to check. + * @return {fabric.Object} object that contains pointer + * @private + */ + _searchPossibleTargets: function(objects, pointer) { + // Cache all targets where their bounding box contains point. + var target, i = objects.length, subTarget; + // Do not check for currently grouped objects, since we check the parent group itself. + // until we call this function specifically to search inside the activeGroup + while (i--) { + var objToCheck = objects[i]; + var pointerToUse = objToCheck.group && objToCheck.group.type !== 'activeSelection' ? + this._normalizePointer(objToCheck.group, pointer) : pointer; + if (this._checkTarget(pointerToUse, objToCheck, pointer)) { + target = objects[i]; + if (target.subTargetCheck && target instanceof fabric.Group) { + subTarget = this._searchPossibleTargets(target._objects, pointer); + subTarget && this.targets.push(subTarget); + } + break; + } + } + return target; + }, + + /** + * Returns pointer coordinates without the effect of the viewport + * @param {Object} pointer with "x" and "y" number values + * @return {Object} object with "x" and "y" number values + */ + restorePointerVpt: function(pointer) { + return fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + }, + + /** + * Returns pointer coordinates relative to canvas. + * Can return coordinates with or without viewportTransform. + * ignoreZoom false gives back coordinates that represent + * the point clicked on canvas element. + * ignoreZoom true gives back coordinates after being processed + * by the viewportTransform ( sort of coordinates of what is displayed + * on the canvas where you are clicking. + * ignoreZoom true = HTMLElement coordinates relative to top,left + * ignoreZoom false, default = fabric space coordinates, the same used for shape position + * To interact with your shapes top and left you want to use ignoreZoom true + * most of the time, while ignoreZoom false will give you coordinates + * compatible with the object.oCoords system. + * of the time. + * @param {Event} e + * @param {Boolean} ignoreZoom + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e, ignoreZoom) { + // return cached values if we are in the event processing chain + if (this._absolutePointer && !ignoreZoom) { + return this._absolutePointer; + } + if (this._pointer && ignoreZoom) { + return this._pointer; + } + + var pointer = getPointer(e), + upperCanvasEl = this.upperCanvasEl, + bounds = upperCanvasEl.getBoundingClientRect(), + boundsWidth = bounds.width || 0, + boundsHeight = bounds.height || 0, + cssScale; + + if (!boundsWidth || !boundsHeight ) { + if ('top' in bounds && 'bottom' in bounds) { + boundsHeight = Math.abs( bounds.top - bounds.bottom ); + } + if ('right' in bounds && 'left' in bounds) { + boundsWidth = Math.abs( bounds.right - bounds.left ); + } + } + + this.calcOffset(); + pointer.x = pointer.x - this._offset.left; + pointer.y = pointer.y - this._offset.top; + if (!ignoreZoom) { + pointer = this.restorePointerVpt(pointer); + } + + if (boundsWidth === 0 || boundsHeight === 0) { + // If bounds are not available (i.e. not visible), do not apply scale. + cssScale = { width: 1, height: 1 }; + } + else { + cssScale = { + width: upperCanvasEl.width / boundsWidth, + height: upperCanvasEl.height / boundsHeight + }; + } + + return { + x: pointer.x * cssScale.width, + y: pointer.y * cssScale.height + }; + }, + + /** + * @private + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''); + + // there is no need to create a new upperCanvas element if we have already one. + if (this.upperCanvasEl) { + this.upperCanvasEl.className = ''; + } + else { + this.upperCanvasEl = this._createCanvasElement(); + } + fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); + + this.wrapperEl.appendChild(this.upperCanvasEl); + + this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl); + this._applyCanvasStyle(this.upperCanvasEl); + this.contextTop = this.upperCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.width + 'px', + height: this.height + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @param {HTMLElement} element canvas element to apply styles on + */ + _applyCanvasStyle: function (element) { + var width = this.width || element.width, + height = this.height || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0, + 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', + '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Copy the entire inline style from one element (fromEl) to another (toEl) + * @private + * @param {Element} fromEl Element style is copied from + * @param {Element} toEl Element copied style is applied to + */ + _copyCanvasStyle: function (fromEl, toEl) { + toEl.style.cssText = fromEl.style.cssText; + }, + + /** + * Returns context of canvas where object selection is drawn + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * Returns currently active object + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * Returns an array with the current selected objects + * @return {fabric.Object} active object + */ + getActiveObjects: function () { + var active = this._activeObject; + if (active) { + if (active.type === 'activeSelection' && active._objects) { + return active._objects.slice(0); + } + else { + return [active]; + } + } + return []; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (obj === this._activeObject) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared', { target: obj }); + obj.fire('deselected'); + } + if (this._hoveredTarget === obj) { + this._hoveredTarget = null; + } + this.callSuper('_onObjectRemoved', obj); + }, + + /** + * @private + * Compares the old activeObject with the current one and fires correct events + * @param {fabric.Object} obj old activeObject + */ + _fireSelectionEvents: function(oldObjects, e) { + var somethingChanged = false, objects = this.getActiveObjects(), + added = [], removed = [], opt = { e: e }; + oldObjects.forEach(function(oldObject) { + if (objects.indexOf(oldObject) === -1) { + somethingChanged = true; + oldObject.fire('deselected', opt); + removed.push(oldObject); + } + }); + objects.forEach(function(object) { + if (oldObjects.indexOf(object) === -1) { + somethingChanged = true; + object.fire('selected', opt); + added.push(object); + } + }); + if (oldObjects.length > 0 && objects.length > 0) { + opt.selected = added; + opt.deselected = removed; + // added for backward compatibility + opt.updated = added[0] || removed[0]; + opt.target = this._activeObject; + somethingChanged && this.fire('selection:updated', opt); + } + else if (objects.length > 0) { + // deprecated event + if (objects.length === 1) { + opt.target = added[0]; + this.fire('object:selected', opt); + } + opt.selected = added; + // added for backward compatibility + opt.target = this._activeObject; + this.fire('selection:created', opt); + } + else if (oldObjects.length > 0) { + opt.deselected = removed; + this.fire('selection:cleared', opt); + } + }, + + /** + * Sets given object as the only active object on canvas + * @param {fabric.Object} object Object to set as an active one + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + var currentActives = this.getActiveObjects(); + this._setActiveObject(object, e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * @private + * @param {Object} object to set as active + * @param {Event} [e] Event (passed along when firing "object:selected") + * @return {Boolean} true if the selection happened + */ + _setActiveObject: function(object, e) { + if (this._activeObject === object) { + return false; + } + if (!this._discardActiveObject(e, object)) { + return false; + } + if (object.onSelect({ e: e })) { + return false; + } + this._activeObject = object; + return true; + }, + + /** + * @private + */ + _discardActiveObject: function(e, object) { + var obj = this._activeObject; + if (obj) { + // onDeselect return TRUE to cancel selection; + if (obj.onDeselect({ e: e, object: object })) { + return false; + } + this._activeObject = null; + } + return true; + }, + + /** + * Discards currently active object and fire events. If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parameter and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @param {event} e + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + var currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); + if (currentActives.length) { + this.fire('before:selection:cleared', { target: activeObject, e: e }); + } + this._discardActiveObject(e); + this._fireSelectionEvents(currentActives, e); + return this; + }, + + /** + * Clears a canvas element and removes all event listeners + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + var wrapper = this.wrapperEl; + this.removeListeners(); + wrapper.removeChild(this.upperCanvasEl); + wrapper.removeChild(this.lowerCanvasEl); + this.contextCache = null; + this.contextTop = null; + ['upperCanvasEl', 'cacheCanvasEl'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + if (wrapper.parentNode) { + wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); + } + delete this.wrapperEl; + fabric.StaticCanvas.prototype.dispose.call(this); + return this; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + // this.discardActiveGroup(); + this.discardActiveObject(); + this.clearContext(this.contextTop); + return this.callSuper('clear'); + }, + + /** + * Draws objects' controls (borders/controls) + * @param {CanvasRenderingContext2D} ctx Context to render controls on + */ + drawControls: function(ctx) { + var activeObject = this._activeObject; + + if (activeObject) { + activeObject._renderControls(ctx); + } + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + //If the object is part of the current selection group, it should + //be transformed appropriately + //i.e. it should be serialised as it would appear if the selection group + //were to be destroyed. + var originalProperties = this._realizeGroupTransformOnObject(instance), + object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); + //Undo the damage we did by changing all of its properties + this._unwindGroupTransformOnObject(instance, originalProperties); + return object; + }, + + /** + * Realises an object's group transformation on it + * @private + * @param {fabric.Object} [instance] the object to transform (gets mutated) + * @returns the original values of instance which were changed + */ + _realizeGroupTransformOnObject: function(instance) { + if (instance.group && instance.group.type === 'activeSelection' && this._activeObject === instance.group) { + var layoutProps = ['angle', 'flipX', 'flipY', 'left', 'scaleX', 'scaleY', 'skewX', 'skewY', 'top']; + //Copy all the positionally relevant properties across now + var originalValues = {}; + layoutProps.forEach(function(prop) { + originalValues[prop] = instance[prop]; + }); + this._activeObject.realizeTransform(instance); + return originalValues; + } + else { + return null; + } + }, + + /** + * Restores the changed properties of instance + * @private + * @param {fabric.Object} [instance] the object to un-transform (gets mutated) + * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject + */ + _unwindGroupTransformOnObject: function(instance, originalValues) { + if (originalValues) { + instance.set(originalValues); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + //If the object is in a selection group, simulate what would happen to that + //object when the group is deselected + var originalProperties = this._realizeGroupTransformOnObject(instance); + this.callSuper('_setSVGObject', markup, instance, reviver); + this._unwindGroupTransformOnObject(instance, originalProperties); + }, + + setViewportTransform: function (vpt) { + if (this.renderOnAddRemove && this._activeObject && this._activeObject.isEditing) { + this._activeObject.clearContextTop(); + } + fabric.StaticCanvas.prototype.setViewportTransform.call(this, vpt); + } + }); + + // copying static properties manually to work around Opera's bug, + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } +})(); +(function() { + + var cursorOffset = { + mt: 0, // n + tr: 1, // ne + mr: 2, // e + br: 3, // se + mb: 4, // s + bl: 5, // sw + ml: 6, // w + tl: 7 // nw + }, + addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + RIGHT_CLICK = 3, MIDDLE_CLICK = 2, LEFT_CLICK = 1, + addEventOptions = { passive: false }; + + function checkClick(e, value) { + return e.button && (e.button === value - 1); + } + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Map of cursor style values for each of the object controls + * @private + */ + cursorMap: [ + 'n-resize', + 'ne-resize', + 'e-resize', + 'se-resize', + 's-resize', + 'sw-resize', + 'w-resize', + 'nw-resize' + ], + + /** + * Contains the id of the touch event that owns the fabric transform + * @type Number + * @private + */ + mainTouchId: null, + + /** + * Adds mouse listeners to canvas + * @private + */ + _initEventListeners: function () { + // in case we initialized the class twice. This should not happen normally + // but in some kind of applications where the canvas element may be changed + // this is a workaround to having double listeners. + this.removeListeners(); + this._bindEvents(); + this.addOrRemove(addListener, 'add'); + }, + + /** + * return an event prefix pointer or mouse. + * @private + */ + _getEventPrefix: function () { + return this.enablePointerEvents ? 'pointer' : 'mouse'; + }, + + addOrRemove: function(functor, eventjsFunctor) { + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + functor(fabric.window, 'resize', this._onResize); + functor(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + functor(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + functor(canvasElement, eventTypePrefix + 'out', this._onMouseOut); + functor(canvasElement, eventTypePrefix + 'enter', this._onMouseEnter); + functor(canvasElement, 'wheel', this._onMouseWheel); + functor(canvasElement, 'contextmenu', this._onContextMenu); + functor(canvasElement, 'dblclick', this._onDoubleClick); + functor(canvasElement, 'dragover', this._onDragOver); + functor(canvasElement, 'dragenter', this._onDragEnter); + functor(canvasElement, 'dragleave', this._onDragLeave); + functor(canvasElement, 'drop', this._onDrop); + if (!this.enablePointerEvents) { + functor(canvasElement, 'touchstart', this._onTouchStart, addEventOptions); + } + if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) { + eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture); + eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag); + eventjs[eventjsFunctor](canvasElement, 'orientation', this._onOrientationChange); + eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake); + eventjs[eventjsFunctor](canvasElement, 'longpress', this._onLongPress); + } + }, + + /** + * Removes all event listeners + */ + removeListeners: function() { + this.addOrRemove(removeListener, 'remove'); + // if you dispose on a mouseDown, before mouse up, you need to clean document to... + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + }, + + /** + * @private + */ + _bindEvents: function() { + if (this.eventsBound) { + // for any reason we pass here twice we do not want to bind events twice. + return; + } + this._onMouseDown = this._onMouseDown.bind(this); + this._onTouchStart = this._onTouchStart.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onTouchEnd = this._onTouchEnd.bind(this); + this._onResize = this._onResize.bind(this); + this._onGesture = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onLongPress = this._onLongPress.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onMouseEnter = this._onMouseEnter.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onDoubleClick = this._onDoubleClick.bind(this); + this._onDragOver = this._onDragOver.bind(this); + this._onDragEnter = this._simpleEventHandler.bind(this, 'dragenter'); + this._onDragLeave = this._simpleEventHandler.bind(this, 'dragleave'); + this._onDrop = this._simpleEventHandler.bind(this, 'drop'); + this.eventsBound = true; + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, self) { + this.__onTransformGesture && this.__onTransformGesture(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, self) { + this.__onDrag && this.__onDrag(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on wheel event + */ + _onMouseWheel: function(e) { + this.__onMouseWheel(e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseOut: function(e) { + var target = this._hoveredTarget; + this.fire('mouse:out', { target: target, e: e }); + this._hoveredTarget = null; + target && target.fire('mouseout', { e: e }); + if (this._iTextInstances) { + this._iTextInstances.forEach(function(obj) { + if (obj.isEditing) { + obj.hiddenTextarea.focus(); + } + }); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mouseenter + */ + _onMouseEnter: function(e) { + // This find target and consequent 'mouse:over' is used to + // clear old instances on hovered target. + // calling findTarget has the side effect of killing target.__corner. + // as a short term fix we are not firing this if we are currently transforming. + // as a long term fix we need to separate the action of finding a target with the + // side effects we added to it. + if (!this.currentTransform && !this.findTarget(e)) { + this.fire('mouse:over', { target: null, e: e }); + this._hoveredTarget = null; + } + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e, self) { + this.__onOrientationChange && this.__onOrientationChange(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e, self) { + this.__onShake && this.__onShake(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onLongPress: function(e, self) { + this.__onLongPress && this.__onLongPress(e, self); + }, + + /** + * prevent default to allow drop event to be fired + * @private + * @param {Event} [e] Event object fired on Event.js shake + */ + _onDragOver: function(e) { + e.preventDefault(); + var target = this._simpleEventHandler('dragover', e); + this._fireEnterLeaveEvents(target, e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onContextMenu: function (e) { + if (this.stopContextMenu) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onDoubleClick: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'dblclick'); + this._resetTransformEventData(e); + }, + + /** + * Return a the id of an event. + * returns either the pointerId or the identifier or 0 for the mouse event + * @private + * @param {Event} evt Event object + */ + getPointerId: function(evt) { + var changedTouches = evt.changedTouches; + + if (changedTouches) { + return changedTouches[0] && changedTouches[0].identifier; + } + + if (this.enablePointerEvents) { + return evt.pointerId; + } + + return -1; + }, + + /** + * Determines if an event has the id of the event that is considered main + * @private + * @param {evt} event Event object + */ + _isMainEvent: function(evt) { + if (evt.isPrimary === true) { + return true; + } + if (evt.isPrimary === false) { + return false; + } + if (evt.type === 'touchend' && evt.touches.length === 0) { + return true; + } + if (evt.changedTouches) { + return evt.changedTouches[0].identifier === this.mainTouchId; + } + return true; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchStart: function(e) { + e.preventDefault(); + if (this.mainTouchId === null) { + this.mainTouchId = this.getPointerId(e); + } + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + addListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + addListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + // Unbind mousedown to prevent double triggers from touch devices + removeListener(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDown: function (e) { + this.__onMouseDown(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + removeListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchEnd: function(e) { + if (e.touches.length > 0) { + // if there are still touches stop here + return; + } + this.__onMouseUp(e); + this._resetTransformEventData(); + this.mainTouchId = null; + var eventTypePrefix = this._getEventPrefix(); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); + removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); + var _this = this; + if (this._willAddMouseDown) { + clearTimeout(this._willAddMouseDown); + } + this._willAddMouseDown = setTimeout(function() { + // Wait 400ms before rebinding mousedown to prevent double triggers + // from touch devices + addListener(_this.upperCanvasEl, eventTypePrefix + 'down', _this._onMouseDown); + _this._willAddMouseDown = 0; + }, 400); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUp: function (e) { + this.__onMouseUp(e); + this._resetTransformEventData(); + var canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + if (this._isMainEvent(e)) { + removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + addListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMove: function (e) { + !this.allowTouchScrolling && e.preventDefault && e.preventDefault(); + this.__onMouseMove(e); + }, + + /** + * @private + */ + _onResize: function () { + this.calcOffset(); + }, + + /** + * Decides whether the canvas should be redrawn in mouseup and mousedown events. + * @private + * @param {Object} target + */ + _shouldRender: function(target) { + var activeObject = this._activeObject; + + if ( + !!activeObject !== !!target || + (activeObject && target && (activeObject !== target)) + ) { + // this covers: switch of target, from target to no target, selection of target + // multiSelection with key and mouse + return true; + } + else if (activeObject && activeObject.isEditing) { + // if we mouse up/down over a editing textbox a cursor change, + // there is no need to re render + return false; + } + return false; + }, + + /** + * Method that defines the actions when mouse is released on canvas. + * The method resets the currentTransform parameters, store the image corner + * position in the image object and render the canvas on top. + * @private + * @param {Event} e Event object fired on mouseup + */ + __onMouseUp: function (e) { + var target, transform = this._currentTransform, + groupSelector = this._groupSelector, shouldRender = false, + isClick = (!groupSelector || (groupSelector.left === 0 && groupSelector.top === 0)); + this._cacheTransformEventData(e); + target = this._target; + this._handleEvent(e, 'up:before'); + // if right/middle click just fire events and return + // target undefined will make the _handleEvent search the target + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'up', RIGHT_CLICK, isClick); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); + } + this._resetTransformEventData(); + return; + } + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._onMouseUpInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + if (transform) { + this._finalizeCurrentTransform(e); + shouldRender = transform.actionPerformed; + } + + if (!isClick) { + this._maybeGroupObjects(e); + shouldRender || (shouldRender = this._shouldRender(target)); + } + if (target) { + target.isMoving = false; + } + this._setCursorFromEvent(e, target); + this._handleEvent(e, 'up', LEFT_CLICK, isClick); + this._groupSelector = null; + this._currentTransform = null; + // reset the target information about which corner is selected + target && (target.__corner = 0); + if (shouldRender) { + this.requestRenderAll(); + } + else if (!isClick) { + this.renderTop(); + } + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @return {Fabric.Object} target return the the target found, for internal reasons. + */ + _simpleEventHandler: function(eventType, e) { + var target = this.findTarget(e), + targets = this.targets, + options = { + e: e, + target: target, + subTargets: targets, + }; + this.fire(eventType, options); + target && target.fire(eventType, options); + if (!targets) { + return target; + } + for (var i = 0; i < targets.length; i++) { + targets[i].fire(eventType, options); + } + return target; + }, + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @param {fabric.Object} targetObj receiving event + * @param {Number} [button] button used in the event 1 = left, 2 = middle, 3 = right + * @param {Boolean} isClick for left button only, indicates that the mouse up happened without move. + */ + _handleEvent: function(e, eventType, button, isClick) { + var target = this._target, + targets = this.targets || [], + options = { + e: e, + target: target, + subTargets: targets, + button: button || LEFT_CLICK, + isClick: isClick || false, + pointer: this._pointer, + absolutePointer: this._absolutePointer, + transform: this._currentTransform + }; + this.fire('mouse:' + eventType, options); + target && target.fire('mouse' + eventType, options); + for (var i = 0; i < targets.length; i++) { + targets[i].fire('mouse' + eventType, options); + } + }, + + /** + * @private + * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event + */ + _finalizeCurrentTransform: function(e) { + + var transform = this._currentTransform, + target = transform.target, + eventName, + options = { + e: e, + target: target, + transform: transform, + }; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) { + if (transform.actionPerformed) { + eventName = this._addEventOptions(options, transform); + this._fire(eventName, options); + } + this._fire('modified', options); + } + }, + + /** + * Mutate option object in order to add by property and give back the event name. + * @private + * @param {Object} options to mutate + * @param {Object} transform to inspect action from + */ + _addEventOptions: function(options, transform) { + // we can probably add more details at low cost + // scale change, rotation changes, translation changes + var eventName, by; + switch (transform.action) { + case 'scaleX': + eventName = 'scaled'; + by = 'x'; + break; + case 'scaleY': + eventName = 'scaled'; + by = 'y'; + break; + case 'skewX': + eventName = 'skewed'; + by = 'x'; + break; + case 'skewY': + eventName = 'skewed'; + by = 'y'; + break; + case 'scale': + eventName = 'scaled'; + by = 'equally'; + break; + case 'rotate': + eventName = 'rotated'; + break; + case 'drag': + eventName = 'moved'; + break; + } + options.by = by; + return eventName; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDownInDrawingMode: function(e) { + this._isCurrentlyDrawing = true; + if (this.getActiveObject()) { + this.discardActiveObject(e).requestRenderAll(); + } + if (this.clipTo) { + fabric.util.clipContext(this, this.contextTop); + } + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseDown(pointer, { e: e, pointer: pointer }); + this._handleEvent(e, 'down'); + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseMove(pointer, { e: e, pointer: pointer }); + } + this.setCursor(this.freeDrawingCursor); + this._handleEvent(e, 'move'); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + if (this.clipTo) { + this.contextTop.restore(); + } + var pointer = this.getPointer(e); + this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); + this._handleEvent(e, 'up'); + }, + + /** + * Method that defines the actions when mouse is clicked on canvas. + * The method inits the currentTransform parameters and renders all the + * canvas so the current image can be placed on the top canvas and the rest + * in on the container one. + * @private + * @param {Event} e Event object fired on mousedown + */ + __onMouseDown: function (e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'down:before'); + var target = this._target; + // if right click just fire events + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'down', RIGHT_CLICK); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'down', MIDDLE_CLICK); + } + return; + } + + if (this.isDrawingMode) { + this._onMouseDownInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) { + return; + } + + var pointer = this._pointer; + // save pointer for check in __onMouseUp event + this._previousPointer = pointer; + var shouldRender = this._shouldRender(target), + shouldGroup = this._shouldGroup(e, target); + if (this._shouldClearSelection(e, target)) { + this.discardActiveObject(e); + } + else if (shouldGroup) { + this._handleGrouping(e, target); + target = this._activeObject; + } + + if (this.selection && (!target || + (!target.selectable && !target.isEditing && target !== this._activeObject))) { + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + } + + if (target) { + var alreadySelected = target === this._activeObject; + if (target.selectable) { + this.setActiveObject(target, e); + } + if (target === this._activeObject && (target.__corner || !shouldGroup)) { + this._setupCurrentTransform(e, target, alreadySelected); + } + } + this._handleEvent(e, 'down'); + // we must renderAll so that we update the visuals + (shouldRender || shouldGroup) && this.requestRenderAll(); + }, + + /** + * reset cache form common information needed during event processing + * @private + */ + _resetTransformEventData: function() { + this._target = null; + this._pointer = null; + this._absolutePointer = null; + }, + + /** + * Cache common information needed during event processing + * @private + * @param {Event} e Event object fired on event + */ + _cacheTransformEventData: function(e) { + // reset in order to avoid stale caching + this._resetTransformEventData(); + this._pointer = this.getPointer(e, true); + this._absolutePointer = this.restorePointerVpt(this._pointer); + this._target = this._currentTransform ? this._currentTransform.target : this.findTarget(e) || null; + }, + + /** + * @private + */ + _beforeTransform: function(e) { + var t = this._currentTransform; + this.stateful && t.target.saveState(); + this.fire('before:transform', { + e: e, + transform: t, + }); + // determine if it's a drag or rotate case + if (t.corner) { + this.onBeforeScaleRotate(t.target); + } + }, + + /** + * Method that defines the actions when mouse is hovering the canvas. + * The currentTransform parameter will define whether the user is rotating/scaling/translating + * an image or neither of them (only hovering). A group selection is also possible and would cancel + * all any other type of action. + * In case of an image transformation only the top canvas will be rendered. + * @private + * @param {Event} e Event object fired on mousemove + */ + __onMouseMove: function (e) { + this._handleEvent(e, 'move:before'); + this._cacheTransformEventData(e); + var target, pointer; + + if (this.isDrawingMode) { + this._onMouseMoveInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + var groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection + if (groupSelector) { + pointer = this._pointer; + + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; + + this.renderTop(); + } + else if (!this._currentTransform) { + target = this.findTarget(e) || null; + this._setCursorFromEvent(e, target); + this._fireOverOutEvents(target, e); + } + else { + this._transformObject(e); + } + this._handleEvent(e, 'move'); + this._resetTransformEventData(); + }, + + /** + * Manage the mouseout, mouseover events for the fabric object on the canvas + * @param {Fabric.Object} target the target where the target from the mousemove event + * @param {Event} e Event object fired on mousemove + * @private + */ + _fireOverOutEvents: function(target, e) { + this.fireSyntheticInOutEvents(target, e, { + targetName: '_hoveredTarget', + canvasEvtOut: 'mouse:out', + evtOut: 'mouseout', + canvasEvtIn: 'mouse:over', + evtIn: 'mouseover', + }); + }, + + /** + * Manage the dragEnter, dragLeave events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the onDrag event + * @param {Event} e Event object fired on ondrag + * @private + */ + _fireEnterLeaveEvents: function(target, e) { + this.fireSyntheticInOutEvents(target, e, { + targetName: '_draggedoverTarget', + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + }, + + /** + * Manage the synthetic in/out events for the fabric objects on the canvas + * @param {Fabric.Object} target the target where the target from the supported events + * @param {Event} e Event object fired + * @param {Object} config configuration for the function to work + * @param {String} config.targetName property on the canvas where the old target is stored + * @param {String} [config.canvasEvtOut] name of the event to fire at canvas level for out + * @param {String} config.evtOut name of the event to fire for out + * @param {String} [config.canvasEvtIn] name of the event to fire at canvas level for in + * @param {String} config.evtIn name of the event to fire for in + * @private + */ + fireSyntheticInOutEvents: function(target, e, config) { + var inOpt, outOpt, oldTarget = this[config.targetName], outFires, inFires, + targetChanged = oldTarget !== target, canvasEvtIn = config.canvasEvtIn, canvasEvtOut = config.canvasEvtOut; + if (targetChanged) { + inOpt = { e: e, target: target, previousTarget: oldTarget }; + outOpt = { e: e, target: oldTarget, nextTarget: target }; + this[config.targetName] = target; + } + inFires = target && targetChanged; + outFires = oldTarget && targetChanged; + if (outFires) { + canvasEvtOut && this.fire(canvasEvtOut, outOpt); + oldTarget.fire(config.evtOut, outOpt); + } + if (inFires) { + canvasEvtIn && this.fire(canvasEvtIn, inOpt); + target.fire(config.evtIn, inOpt); + } + }, + + /** + * Method that defines actions when an Event Mouse Wheel + * @param {Event} e Event object fired on mouseup + */ + __onMouseWheel: function(e) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'wheel'); + this._resetTransformEventData(); + }, + + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + var pointer = this.getPointer(e), + transform = this._currentTransform; + + transform.reset = false; + transform.target.isMoving = true; + transform.shiftKey = e.shiftKey; + transform.altKey = e[this.centeredKey]; + + this._beforeScaleTransform(e, transform); + this._performTransformAction(e, transform, pointer); + + transform.actionPerformed && this.requestRenderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + action = transform.action, + actionPerformed = false, + options = { + target: transform.target, + e: e, + transform: transform, + pointer: pointer + }; + + if (action === 'rotate') { + (actionPerformed = this._rotateObject(x, y)) && this._fire('rotating', options); + } + else if (action === 'scale') { + (actionPerformed = this._onScale(e, transform, x, y)) && this._fire('scaling', options); + } + else if (action === 'scaleX') { + (actionPerformed = this._scaleObject(x, y, 'x')) && this._fire('scaling', options); + } + else if (action === 'scaleY') { + (actionPerformed = this._scaleObject(x, y, 'y')) && this._fire('scaling', options); + } + else if (action === 'skewX') { + (actionPerformed = this._skewObject(x, y, 'x')) && this._fire('skewing', options); + } + else if (action === 'skewY') { + (actionPerformed = this._skewObject(x, y, 'y')) && this._fire('skewing', options); + } + else { + actionPerformed = this._translateObject(x, y); + if (actionPerformed) { + this._fire('moving', options); + this.setCursor(options.target.moveCursor || this.moveCursor); + } + } + transform.actionPerformed = transform.actionPerformed || actionPerformed; + }, + + /** + * @private + */ + _fire: function(eventName, options) { + this.fire('object:' + eventName, options); + options.target.fire(eventName, options); + }, + + /** + * @private + */ + _beforeScaleTransform: function(e, transform) { + if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { + var centerTransform = this._shouldCenterTransform(transform.target); + + // Switch from a normal resize to center-based + if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || + // Switch from center-based resize to normal one + (!centerTransform && transform.originX === 'center' && transform.originY === 'center') + ) { + this._resetCurrentTransform(); + transform.reset = true; + } + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {Object} transform current transform + * @param {Number} x mouse position x from origin + * @param {Number} y mouse position y from origin + * @return {Boolean} true if the scaling occurred + */ + _onScale: function(e, transform, x, y) { + if (this._isUniscalePossible(e, transform.target)) { + transform.currentAction = 'scale'; + return this._scaleObject(x, y); + } + else { + // Switch from a normal resize to proportional + if (!transform.reset && transform.currentAction === 'scale') { + this._resetCurrentTransform(); + } + + transform.currentAction = 'scaleEqually'; + return this._scaleObject(x, y, 'equally'); + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target current target + * @return {Boolean} true if unproportional scaling is possible + */ + _isUniscalePossible: function(e, target) { + return (e[this.uniScaleKey] || this.uniScaleTransform) && !target.get('lockUniScaling'); + }, + + /** + * Sets the cursor depending on where the canvas is being hovered. + * Note: very buggy in Opera + * @param {Event} e Event object + * @param {Object} target Object that the mouse is hovering, if so. + */ + _setCursorFromEvent: function (e, target) { + if (!target) { + this.setCursor(this.defaultCursor); + return false; + } + var hoverCursor = target.hoverCursor || this.hoverCursor, + activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? + this._activeObject : null, + // only show proper corner when group selection is not active + corner = (!activeSelection || !activeSelection.contains(target)) + && target._findTargetCorner(this.getPointer(e, true)); + + if (!corner) { + this.setCursor(hoverCursor); + } + else { + this.setCursor(this.getCornerCursor(corner, target, e)); + } + }, + + /** + * @private + */ + getCornerCursor: function(corner, target, e) { + if (this.actionIsDisabled(corner, target, e)) { + return this.notAllowedCursor; + } + else if (corner in cursorOffset) { + return this._getRotatedCornerCursor(corner, target, e); + } + else if (corner === 'mtr' && target.hasRotatingPoint) { + return this.rotationCursor; + } + else { + return this.defaultCursor; + } + }, + + actionIsDisabled: function(corner, target, e) { + if (corner === 'mt' || corner === 'mb') { + return e[this.altActionKey] ? target.lockSkewingX : target.lockScalingY; + } + else if (corner === 'ml' || corner === 'mr') { + return e[this.altActionKey] ? target.lockSkewingY : target.lockScalingX; + } + else if (corner === 'mtr') { + return target.lockRotation; + } + else { + return this._isUniscalePossible(e, target) ? + target.lockScalingX && target.lockScalingY : target.lockScalingX || target.lockScalingY; + } + }, + + /** + * @private + */ + _getRotatedCornerCursor: function(corner, target, e) { + var n = Math.round((target.angle % 360) / 45); + + if (n < 0) { + n += 8; // full circle ahead + } + n += cursorOffset[corner]; + if (e[this.altActionKey] && cursorOffset[corner] % 2 === 0) { + //if we are holding shift and we are on a mx corner... + n += 2; + } + // normalize n to be from 0 to 7 + n %= 8; + + return this.cursorMap[n]; + } + }); +})(); +(function() { + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this._activeObject; + return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && + (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + var activeObject = this._activeObject; + // avoid multi select when shift click on a corner + if (activeObject.__corner) { + return; + } + if (target === activeObject) { + // if it's a group, find target again, using activeGroup objects + target = this.findTarget(e, true); + // if even object is not found or we are on activeObjectCorner, bail out + if (!target || !target.selectable) { + return; + } + } + if (activeObject && activeObject.type === 'activeSelection') { + this._updateActiveSelection(target, e); + } + else { + this._createActiveSelection(target, e); + } + }, + + /** + * @private + */ + _updateActiveSelection: function(target, e) { + var activeSelection = this._activeObject, + currentActiveObjects = activeSelection._objects.slice(0); + if (activeSelection.contains(target)) { + activeSelection.removeWithUpdate(target); + this._hoveredTarget = target; + if (activeSelection.size() === 1) { + // activate last remaining object + this._setActiveObject(activeSelection.item(0), e); + } + } + else { + activeSelection.addWithUpdate(target); + this._hoveredTarget = activeSelection; + } + this._fireSelectionEvents(currentActiveObjects, e); + }, + + /** + * @private + */ + _createActiveSelection: function(target, e) { + var currentActives = this.getActiveObjects(), group = this._createGroup(target); + this._hoveredTarget = group; + this._setActiveObject(group, e); + this._fireSelectionEvents(currentActives, e); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + var objects = this._objects, + isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), + groupObjects = isActiveLower + ? [this._activeObject, target] + : [target, this._activeObject]; + this._activeObject.isEditing && this._activeObject.exitEditing(); + return new fabric.ActiveSelection(groupObjects, { + canvas: this + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(e), + aGroup; + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + aGroup = new fabric.ActiveSelection(group.reverse(), { + canvas: this + }); + this.setActiveObject(aGroup, e); + } + }, + + /** + * @private + */ + _collectObjects: function(e) { + var group = [], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + allowIntersect = !this.selectionFullyContained, + isClick = x1 === x2 && y1 === y2; + // we iterate reverse order to collect top first in case of click. + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) { + continue; + } + + if ((allowIntersect && currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2)) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || + (allowIntersect && currentObject.containsPoint(selectionX1Y1)) || + (allowIntersect && currentObject.containsPoint(selectionX2Y2)) + ) { + group.push(currentObject); + // only add one object if it's a click + if (isClick) { + break; + } + } + } + + if (group.length > 1) { + group = group.filter(function(object) { + return !object.onSelect({ e: e }); + }); + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + this.setCursor(this.defaultCursor); + // clear selection and current transformation + this._groupSelector = null; + } + }); + +})(); +(function () { + fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately + * @param {Object} [options] Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} + * @example Generate jpeg dataURL with lower quality + * var dataURL = canvas.toDataURL({ + * format: 'jpeg', + * quality: 0.8 + * }); + * @example Generate cropped png dataURL (clipping of canvas) + * var dataURL = canvas.toDataURL({ + * format: 'png', + * left: 100, + * top: 100, + * width: 200, + * height: 200 + * }); + * @example Generate double scaled png dataURL + * var dataURL = canvas.toDataURL({ + * format: 'png', + * multiplier: 2 + * }); + */ + toDataURL: function (options) { + options || (options = { }); + + var format = options.format || 'png', + quality = options.quality || 1, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? this.getRetinaScaling() : 1), + canvasEl = this.toCanvasElement(multiplier, options); + return fabric.util.toDataURL(canvasEl, format, quality); + }, + + /** + * Create a new HTMLCanvas element painted with the current canvas content. + * No need to resize the actual one or repaint it. + * Will transfer object ownership to a new canvas, paint it, and set everything back. + * This is an intermediary step used to get to a dataUrl but also it is useful to + * create quick image copies of a canvas without passing for the dataUrl string + * @param {Number} [multiplier] a zoom factor. + * @param {Object} [cropping] Cropping informations + * @param {Number} [cropping.left] Cropping left offset. + * @param {Number} [cropping.top] Cropping top offset. + * @param {Number} [cropping.width] Cropping width. + * @param {Number} [cropping.height] Cropping height. + */ + toCanvasElement: function(multiplier, cropping) { + multiplier = multiplier || 1; + cropping = cropping || { }; + var scaledWidth = (cropping.width || this.width) * multiplier, + scaledHeight = (cropping.height || this.height) * multiplier, + zoom = this.getZoom(), + originalWidth = this.width, + originalHeight = this.height, + newZoom = zoom * multiplier, + vp = this.viewportTransform, + translateX = (vp[4] - (cropping.left || 0)) * multiplier, + translateY = (vp[5] - (cropping.top || 0)) * multiplier, + originalInteractive = this.interactive, + newVp = [newZoom, 0, 0, newZoom, translateX, translateY], + originalRetina = this.enableRetinaScaling, + canvasEl = fabric.util.createCanvasElement(), + originalContextTop = this.contextTop; + canvasEl.width = scaledWidth; + canvasEl.height = scaledHeight; + this.contextTop = null; + this.enableRetinaScaling = false; + this.interactive = false; + this.viewportTransform = newVp; + this.width = scaledWidth; + this.height = scaledHeight; + this.calcViewportBoundaries(); + this.renderCanvas(canvasEl.getContext('2d'), this._objects); + this.viewportTransform = vp; + this.width = originalWidth; + this.height = originalHeight; + this.calcViewportBoundaries(); + this.interactive = originalInteractive; + this.enableRetinaScaling = originalRetina; + this.contextTop = originalContextTop; + return canvasEl; + }, + }); + +})(); +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Populates canvas with data from the specified dataless JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toDatalessJSON} + * @deprecated since 1.2.2 + * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @return {fabric.Canvas} instance + * @chainable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + */ + loadFromDatalessJSON: function (json, callback, reviver) { + return this.loadFromJSON(json, callback, reviver); + }, + + /** + * Populates canvas with data from the specified JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toJSON} + * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @return {fabric.Canvas} instance + * @chainable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} + * @example loadFromJSON + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); + * @example loadFromJSON with reviver + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { + * // `o` = json object + * // `object` = fabric.Object instance + * // ... do some stuff ... + * }); + */ + loadFromJSON: function (json, callback, reviver) { + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : fabric.util.object.clone(json); + + var _this = this, + clipPath = serialized.clipPath, + renderOnAddRemove = this.renderOnAddRemove; + + this.renderOnAddRemove = false; + + delete serialized.clipPath; + + this._enlivenObjects(serialized.objects, function (enlivenedObjects) { + _this.clear(); + _this._setBgOverlay(serialized, function () { + if (clipPath) { + _this._enlivenObjects([clipPath], function (enlivenedCanvasClip) { + _this.clipPath = enlivenedCanvasClip[0]; + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + }); + } + else { + _this.__setupCanvas.call(_this, serialized, enlivenedObjects, renderOnAddRemove, callback); + } + }); + }, reviver); + return this; + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Array} restored canvas objects + * @param {Function} cached renderOnAddRemove callback + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + __setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove, callback) { + var _this = this; + enlivenedObjects.forEach(function(obj, index) { + // we splice the array just in case some custom classes restored from JSON + // will add more object to canvas at canvas init. + _this.insertAt(obj, index); + }); + this.renderOnAddRemove = renderOnAddRemove; + // remove parts i cannot set as options + delete serialized.objects; + delete serialized.backgroundImage; + delete serialized.overlayImage; + delete serialized.background; + delete serialized.overlay; + // this._initOptions does too many things to just + // call it. Normally loading an Object from JSON + // create the Object instance. Here the Canvas is + // already an instance and we are just loading things over it + this._setOptions(serialized); + this.renderAll(); + callback && callback(); + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { + var loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } + + var cbIfLoaded = function () { + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + callback && callback(); + } + }; + + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); + }, + + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; + + if (!value) { + loaded[property] = true; + callback && callback(); + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.util.enlivenObjects([value], function(enlivedObject){ + _this[property] = enlivedObject[0]; + loaded[property] = true; + callback && callback(); + }); + } + else { + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); + }); + } + }, + + /** + * @private + * @param {Array} objects + * @param {Function} callback + * @param {Function} [reviver] + */ + _enlivenObjects: function (objects, callback, reviver) { + if (!objects || objects.length === 0) { + callback && callback([]); + return; + } + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, null, reviver); + }, + + /** + * @private + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @param {Object} [callback] Receives cloned instance as a first argument + * @param {Array} [properties] Array of properties to include in the cloned canvas and children + */ + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.util.createCanvasElement(); + + el.width = this.width; + el.height = this.height; + + var clone = new fabric.Canvas(el); + clone.clipTo = this.clipTo; + if (this.backgroundImage) { + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; + } + else { + callback && callback(clone); + } + } +}); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + degreesToRadians = fabric.util.degreesToRadians, + supportsLineDash = fabric.StaticCanvas.supports('setLineDash'), + objectCaching = !fabric.isLikelyNode, + ALIASING_LIMIT = 2; + + if (fabric.Object) { + return; + } + + /** + * Root object class from which all 2d shape classes inherit from + * @class fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects} + * @see {@link fabric.Object#initialize} for constructor definition + * + * @fires added + * @fires removed + * + * @fires selected + * @fires deselected + * @fires modified + * @fires modified + * @fires moved + * @fires scaled + * @fires rotated + * @fires skewed + * + * @fires rotating + * @fires scaling + * @fires moving + * @fires skewing + * + * @fires mousedown + * @fires mouseup + * @fires mouseover + * @fires mouseout + * @fires mousewheel + * @fires mousedblclick + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop + */ + fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc.). + * Note that this property is meant to be read-only and not meant to be modified. + * If you modify, certain parts of Fabric (such as JSON loading) won't work correctly. + * @type String + * @default + */ + type: 'object', + + /** + * Horizontal origin of transformation of an object (one of "left", "right", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originX: 'left', + + /** + * Vertical origin of transformation of an object (one of "top", "bottom", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originY: 'top', + + /** + * Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom} + * @type Number + * @default + */ + top: 0, + + /** + * Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right} + * @type Number + * @default + */ + left: 0, + + /** + * Object width + * @type Number + * @default + */ + width: 0, + + /** + * Object height + * @type Number + * @default + */ + height: 0, + + /** + * Object scale factor (horizontal) + * @type Number + * @default + */ + scaleX: 1, + + /** + * Object scale factor (vertical) + * @type Number + * @default + */ + scaleY: 1, + + /** + * When true, an object is rendered as flipped horizontally + * @type Boolean + * @default + */ + flipX: false, + + /** + * When true, an object is rendered as flipped vertically + * @type Boolean + * @default + */ + flipY: false, + + /** + * Opacity of an object + * @type Number + * @default + */ + opacity: 1, + + /** + * Angle of rotation of an object (in degrees) + * @type Number + * @default + */ + angle: 0, + + /** + * Angle of skew on x axes of an object (in degrees) + * @type Number + * @default + */ + skewX: 0, + + /** + * Angle of skew on y axes of an object (in degrees) + * @type Number + * @default + */ + skewY: 0, + + /** + * Size of object's controlling corners (in pixels) + * @type Number + * @default + */ + cornerSize: 13, + + /** + * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) + * @type Boolean + * @default + */ + transparentCorners: true, + + /** + * Default cursor value used when hovering over this object on canvas + * @type String + * @default + */ + hoverCursor: null, + + /** + * Default cursor value used when moving this object on canvas + * @type String + * @default + */ + moveCursor: null, + + /** + * Padding between object and its controlling borders (in pixels) + * @type Number + * @default + */ + padding: 0, + + /** + * Color of controlling borders of an object (when it's active) + * @type String + * @default + */ + borderColor: 'rgba(102,153,255,0.75)', + + /** + * Array specifying dash pattern of an object's borders (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + borderDashArray: null, + + /** + * Color of controlling corners of an object (when it's active) + * @type String + * @default + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * Color of controlling corners of an object (when it's active and transparentCorners false) + * @since 1.6.2 + * @type String + * @default + */ + cornerStrokeColor: null, + + /** + * Specify style of control, 'rect' or 'circle' + * @since 1.6.2 + * @type String + */ + cornerStyle: 'rect', + + /** + * Array specifying dash pattern of an object's control (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + cornerDashArray: null, + + /** + * When true, this object will use center point as the origin of transformation + * when being scaled via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, this object will use center point as the origin of transformation + * when being rotated via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: true, + + /** + * Color of object's fill + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + fill: 'rgb(0,0,0)', + + /** + * Fill rule used to fill an object + * accepted values are nonzero, evenodd + * Backwards incompatibility note: This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead) + * @type String + * @default + */ + fillRule: 'nonzero', + + /** + * Composite rule used for canvas globalCompositeOperation + * @type String + * @default + */ + globalCompositeOperation: 'source-over', + + /** + * Background color of an object. + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + backgroundColor: '', + + /** + * Selection Background color of an object. colored layer behind the object when it is active. + * does not mix good with globalCompositeOperation methods. + * @type String + * @default + */ + selectionBackgroundColor: '', + + /** + * When defined, an object is rendered via stroke and this property specifies its color + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default + */ + stroke: null, + + /** + * Width of a stroke used to render this object + * @type Number + * @default + */ + strokeWidth: 1, + + /** + * Array specifying dash pattern of an object's stroke (stroke must be defined) + * @type Array + */ + strokeDashArray: null, + + /** + * Line offset of an object's stroke + * @type Number + * @default + */ + strokeDashOffset: 0, + + /** + * Line endings style of an object's stroke (one of "butt", "round", "square") + * @type String + * @default + */ + strokeLineCap: 'butt', + + /** + * Corner style of an object's stroke (one of "bevil", "round", "miter") + * @type String + * @default + */ + strokeLineJoin: 'miter', + + /** + * Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke + * @type Number + * @default + */ + strokeMiterLimit: 4, + + /** + * Shadow object representing shadow of this shape + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * Opacity of object's controlling borders when object is active and moving + * @type Number + * @default + */ + borderOpacityWhenMoving: 0.4, + + /** + * Scale factor of object's controlling borders + * @type Number + * @default + */ + borderScaleFactor: 1, + + /** + * Transform matrix (similar to SVG's transform matrix) + * This property has been depreacted. Since caching and and qrDecompose this + * property can be handled with the standard top,left,scaleX,scaleY,angle and skewX. + * A documentation example on how to parse and merge a transformMatrix will be provided before + * completely removing it in fabric 4.0 + * If you are starting a project now, DO NOT use it. + * @deprecated since 3.2.0 + * @type Array + */ + transformMatrix: null, + + /** + * Minimum allowed scale value of an object + * @type Number + * @default + */ + minScaleLimit: 0, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection). + * But events still fire on it. + * @type Boolean + * @default + */ + selectable: true, + + /** + * When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4 + * @type Boolean + * @default + */ + evented: true, + + /** + * When set to `false`, an object is not rendered on canvas + * @type Boolean + * @default + */ + visible: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @type Boolean + * @default + */ + hasControls: true, + + /** + * When set to `false`, object's controlling borders are not rendered + * @type Boolean + * @default + */ + hasBorders: true, + + /** + * When set to `false`, object's controlling rotating point will not be visible or selectable + * @type Boolean + * @default + */ + hasRotatingPoint: true, + + /** + * Offset for object's controlling rotating point (when enabled via `hasRotatingPoint`) + * @type Number + * @default + */ + rotatingPointOffset: 40, + + /** + * When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box + * @type Boolean + * @default + */ + perPixelTargetFind: false, + + /** + * When `false`, default object's values are not included in its serialization + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * Function that determines clipping of an object (context is passed as a first argument). + * If you are using code minification, ctx argument can be minified/manglied you should use + * as a workaround `var ctx = arguments[0];` in the function; + * Note that context origin is at the object's center point (not left/top corner) + * @deprecated since 2.0.0 + * @type Function + */ + clipTo: null, + + /** + * When `true`, object horizontal movement is locked + * @type Boolean + * @default + */ + lockMovementX: false, + + /** + * When `true`, object vertical movement is locked + * @type Boolean + * @default + */ + lockMovementY: false, + + /** + * When `true`, object rotation is locked + * @type Boolean + * @default + */ + lockRotation: false, + + /** + * When `true`, object horizontal scaling is locked + * @type Boolean + * @default + */ + lockScalingX: false, + + /** + * When `true`, object vertical scaling is locked + * @type Boolean + * @default + */ + lockScalingY: false, + + /** + * When `true`, object non-uniform scaling is locked + * @type Boolean + * @default + */ + lockUniScaling: false, + + /** + * When `true`, object horizontal skewing is locked + * @type Boolean + * @default + */ + lockSkewingX: false, + + /** + * When `true`, object vertical skewing is locked + * @type Boolean + * @default + */ + lockSkewingY: false, + + /** + * When `true`, object cannot be flipped by scaling into negative values + * @type Boolean + * @default + */ + lockScalingFlip: false, + + /** + * When `true`, object is not exported in OBJECT/JSON + * @since 1.6.3 + * @type Boolean + * @default + */ + excludeFromExport: false, + + /** + * When `true`, object is cached on an additional canvas. + * When `false`, object is not cached unless necessary ( clipPath ) + * default to true + * @since 1.7.0 + * @type Boolean + * @default true + */ + objectCaching: objectCaching, + + /** + * When `true`, object properties are checked for cache invalidation. In some particular + * situation you may want this to be disabled ( spray brush, very big, groups) + * or if your application does not allow you to modify properties for groups child you want + * to disable it for groups. + * default to false + * since 1.7.0 + * @type Boolean + * @default false + */ + statefullCache: false, + + /** + * When `true`, cache does not get updated during scaling. The picture will get blocky if scaled + * too much and will be redrawn with correct details at the end of scaling. + * this setting is performance and application dependant. + * default to true + * since 1.7.0 + * @type Boolean + * @default true + */ + noScaleCache: true, + + /** + * When `false`, the stoke width will scale with the object. + * When `true`, the stroke will always match the exact pixel size entered for stroke width. + * default to false + * @since 2.6.0 + * @type Boolean + * @default false + * @type Boolean + * @default false + */ + strokeUniform: false, + + /** + * When set to `true`, object's cache will be rerendered next render call. + * since 1.7.0 + * @type Boolean + * @default true + */ + dirty: true, + + /** + * keeps the value of the last hovered corner during mouse move. + * 0 is no corner, or 'mt', 'ml', 'mtr' etc.. + * It should be private, but there is no harm in using it as + * a read-only property. + * @type number|string|any + * @default 0 + */ + __corner: 0, + + /** + * Determines if the fill or the stroke is drawn first (one of "fill" or "stroke") + * @type String + * @default + */ + paintFirst: 'fill', + + /** + * List of properties to consider when checking if state + * of an object is changed (fabric.Object#hasStateChanged) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + + 'stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit ' + + 'angle opacity fill globalCompositeOperation shadow clipTo visible backgroundColor ' + + 'skewX skewY fillRule paintFirst clipPath strokeUniform' + ).split(' '), + + /** + * List of properties to consider when checking if cache needs refresh + * Those properties are checked by statefullCache ON ( or lazy mode if we want ) or from single + * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty + * and refreshed at the next render + * @type Array + */ + cacheProperties: ( + 'fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform' + + ' strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath' + ).split(' '), + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the object has rendered, and the context is placed in the center + * of the object cacheCanvas. + * If you want 0,0 of a clipPath to align with an object center, use clipPath.originX/Y to 'center' + * @type fabric.Object + */ + clipPath: undefined, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will make the object clip to the outside of the clipPath + * since 2.4.0 + * @type boolean + * @default false + */ + inverted: false, + + /** + * Meaningful ONLY when the object is used as clipPath. + * if true, the clipPath will have its top and left relative to canvas, and will + * not be influenced by the object transform. This will make the clipPath relative + * to the canvas, but clipping just a particular object. + * WARNING this is beta, this feature may change or be renamed. + * since 2.4.0 + * @type boolean + * @default false + */ + absolutePositioned: false, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Create a the canvas used to keep the cached copy of the object + * @private + */ + _createCacheCanvas: function() { + this._cacheProperties = {}; + this._cacheCanvas = fabric.util.createCanvasElement(); + this._cacheContext = this._cacheCanvas.getContext('2d'); + this._updateCacheCanvas(); + // if canvas gets created, is empty, so dirty. + this.dirty = true; + }, + + /** + * Limit the cache dimensions so that X * Y do not cross fabric.perfLimitSizeTotal + * and each side do not cross fabric.cacheSideLimit + * those numbers are configurable so that you can get as much detail as you want + * making bargain with performances. + * @param {Object} dims + * @param {Object} dims.width width of canvas + * @param {Object} dims.height height of canvas + * @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache + * @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 + */ + _limitCacheSize: function(dims) { + var perfLimitSizeTotal = fabric.perfLimitSizeTotal, + width = dims.width, height = dims.height, + max = fabric.maxCacheSideLimit, min = fabric.minCacheSideLimit; + if (width <= max && height <= max && width * height <= perfLimitSizeTotal) { + if (width < min) { + dims.width = min; + } + if (height < min) { + dims.height = min; + } + return dims; + } + var ar = width / height, limitedDims = fabric.util.limitDimsByArea(ar, perfLimitSizeTotal), + capValue = fabric.util.capValue, + x = capValue(min, limitedDims.x, max), + y = capValue(min, limitedDims.y, max); + if (width > x) { + dims.zoomX /= width / x; + dims.width = x; + dims.capped = true; + } + if (height > y) { + dims.zoomY /= height / y; + dims.height = y; + dims.capped = true; + } + return dims; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @return {Object}.x width of object to be cached + * @return {Object}.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 objectScale = this.getTotalObjectScaling(), + // caculate dimensions without skewing + dim = this._getTransformedDimensions(0, 0), + neededX = dim.x * objectScale.scaleX / this.scaleX, + neededY = dim.y * objectScale.scaleY / this.scaleY; + return { + // for sure this ALIASING_LIMIT is slightly creating problem + // in situation in which the cache canvas gets an upper limit + // also objectScale contains already scaleX and scaleY + width: neededX + ALIASING_LIMIT, + height: neededY + ALIASING_LIMIT, + zoomX: objectScale.scaleX, + zoomY: objectScale.scaleY, + x: neededX, + y: neededY + }; + }, + + /** + * Update width and height of the canvas for cache + * returns true or false if canvas needed resize. + * @private + * @return {Boolean} true if the canvas has been resized + */ + _updateCacheCanvas: function() { + var targetCanvas = this.canvas; + if (this.noScaleCache && targetCanvas && targetCanvas._currentTransform) { + var target = targetCanvas._currentTransform.target, + action = targetCanvas._currentTransform.action; + if (this === target && action.slice && action.slice(0, 5) === 'scale') { + return false; + } + } + var canvas = this._cacheCanvas, + dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + minCacheSize = fabric.minCacheSideLimit, + width = dims.width, height = dims.height, drawingWidth, drawingHeight, + zoomX = dims.zoomX, zoomY = dims.zoomY, + dimensionsChanged = width !== this.cacheWidth || height !== this.cacheHeight, + zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY, + shouldRedraw = dimensionsChanged || zoomChanged, + additionalWidth = 0, additionalHeight = 0, shouldResizeCanvas = false; + if (dimensionsChanged) { + var canvasWidth = this._cacheCanvas.width, + canvasHeight = this._cacheCanvas.height, + sizeGrowing = width > canvasWidth || height > canvasHeight, + sizeShrinking = (width < canvasWidth * 0.9 || height < canvasHeight * 0.9) && + canvasWidth > minCacheSize && canvasHeight > minCacheSize; + shouldResizeCanvas = sizeGrowing || sizeShrinking; + if (sizeGrowing && !dims.capped && (width > minCacheSize || height > minCacheSize)) { + additionalWidth = width * 0.1; + additionalHeight = height * 0.1; + } + } + if (shouldRedraw) { + if (shouldResizeCanvas) { + canvas.width = Math.ceil(width + additionalWidth); + canvas.height = Math.ceil(height + additionalHeight); + } + else { + this._cacheContext.setTransform(1, 0, 0, 1, 0, 0); + this._cacheContext.clearRect(0, 0, canvas.width, canvas.height); + } + drawingWidth = dims.x / 2; + drawingHeight = dims.y / 2; + this.cacheTranslationX = Math.round(canvas.width / 2 - drawingWidth) + drawingWidth; + this.cacheTranslationY = Math.round(canvas.height / 2 - drawingHeight) + drawingHeight; + this.cacheWidth = width; + this.cacheHeight = height; + this._cacheContext.translate(this.cacheTranslationX, this.cacheTranslationY); + this._cacheContext.scale(zoomX, zoomY); + this.zoomX = zoomX; + this.zoomY = zoomY; + return true; + } + return false; + }, + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + this._setOptions(options); + this._initGradient(options.fill, 'fill'); + this._initGradient(options.stroke, 'stroke'); + this._initClipping(options); + this._initPattern(options.fill, 'fill'); + this._initPattern(options.stroke, 'stroke'); + }, + + /** + * Transforms context when rendering an object + * @param {CanvasRenderingContext2D} ctx Context + */ + transform: function(ctx) { + var m; + if (this.group && !this.group._transformDone) { + m = this.calcTransformMatrix(); + } + else { + m = this.calcOwnMatrix(); + } + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + }, + + /** + * Returns an 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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + + object = { + type: this.type, + version: fabric.version, + originX: this.originX, + originY: this.originY, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, + strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), + strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : this.strokeDashArray, + strokeLineCap: this.strokeLineCap, + strokeDashOffset: this.strokeDashOffset, + strokeLineJoin: this.strokeLineJoin, + strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.angle, NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow, + visible: this.visible, + clipTo: this.clipTo && String(this.clipTo), + backgroundColor: this.backgroundColor, + fillRule: this.fillRule, + paintFirst: this.paintFirst, + globalCompositeOperation: this.globalCompositeOperation, + transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : null, + skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), + skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS), + }; + + if (this.clipPath) { + object.clipPath = this.clipPath.toObject(propertiesToInclude); + object.clipPath.inverted = this.clipPath.inverted; + object.clipPath.absolutePositioned = this.clipPath.absolutePositioned; + } + + fabric.util.populateWithProperties(this, object, propertiesToInclude); + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) 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 + */ + toDatalessObject: function(propertiesToInclude) { + // will be overwritten by subclasses + return this.toObject(propertiesToInclude); + }, + + /** + * @private + * @param {Object} object + */ + _removeDefaultValues: function(object) { + var prototype = fabric.util.getKlass(object.type).prototype, + stateProperties = prototype.stateProperties; + stateProperties.forEach(function(prop) { + if (prop === 'left' || prop === 'top') { + return; + } + if (object[prop] === prototype[prop]) { + delete object[prop]; + } + var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && + Object.prototype.toString.call(prototype[prop]) === '[object Array]'; + + // basically a check for [] === [] + if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { + delete object[prop]; + } + }); + + return object; + }, + + /** + * Returns a string representation of an instance + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Return the object scale factor counting also the group scaling + * @return {Object} object with scaleX and scaleY properties + */ + getObjectScaling: function() { + var scaleX = this.scaleX, scaleY = this.scaleY; + if (this.group) { + var scaling = this.group.getObjectScaling(); + scaleX *= scaling.scaleX; + scaleY *= scaling.scaleY; + } + return { scaleX: scaleX, scaleY: scaleY }; + }, + + /** + * Return the object scale factor counting also the group scaling, zoom and retina + * @return {Object} object with scaleX and scaleY properties + */ + getTotalObjectScaling: function() { + var scale = this.getObjectScaling(), scaleX = scale.scaleX, scaleY = scale.scaleY; + if (this.canvas) { + var zoom = this.canvas.getZoom(); + var retina = this.canvas.getRetinaScaling(); + scaleX *= zoom * retina; + scaleY *= zoom * retina; + } + return { scaleX: scaleX, scaleY: scaleY }; + }, + + /** + * Return the object opacity counting also the group property + * @return {Number} + */ + getObjectOpacity: function() { + var opacity = this.opacity; + if (this.group) { + opacity *= this.group.getObjectOpacity(); + } + return opacity; + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Object} thisArg + */ + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'), + isChanged = this[key] !== value, groupNeedsUpdate = false; + + if (shouldConstrainValue) { + value = this._constrainScale(value); + } + if (key === 'scaleX' && value < 0) { + this.flipX = !this.flipX; + value *= -1; + } + else if (key === 'scaleY' && value < 0) { + this.flipY = !this.flipY; + value *= -1; + } + else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) { + value = new fabric.Shadow(value); + } + else if (key === 'dirty' && this.group) { + this.group.set('dirty', value); + } + + this[key] = value; + + if (isChanged) { + groupNeedsUpdate = this.group && this.group.isOnACache(); + if (this.cacheProperties.indexOf(key) > -1) { + this.dirty = true; + groupNeedsUpdate && this.group.set('dirty', true); + } + else if (groupNeedsUpdate && this.stateProperties.indexOf(key) > -1) { + this.group.set('dirty', true); + } + } + + return this; + }, + + /** + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. + */ + setOnGroup: function() { + // implemented by sub-classes, as needed. + }, + + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf fabric.Object.prototype + * @return {Array} + */ + getViewportTransform: function() { + if (this.canvas && this.canvas.viewportTransform) { + return this.canvas.viewportTransform; + } + return fabric.iMatrix.concat(); + }, + + /* + * @private + * return if the object would be visible in rendering + * @memberOf fabric.Object.prototype + * @return {Boolean} + */ + isNotVisible: function() { + return this.opacity === 0 || + (this.width === 0 && this.height === 0 && this.strokeWidth === 0) || + !this.visible; + }, + + /** + * Renders an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx) { + // do not render if width/height are zeros or object is not visible + if (this.isNotVisible()) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + ctx.save(); + this._setupCompositeOperation(ctx); + this.drawSelectionBackground(ctx); + this.transform(ctx); + this._setOpacity(ctx); + this._setShadow(ctx, this); + if (this.transformMatrix) { + ctx.transform.apply(ctx, this.transformMatrix); + } + this.clipTo && fabric.util.clipContext(this, ctx); + if (this.shouldCache()) { + this.renderCache(); + this.drawCacheOnCanvas(ctx); + } + else { + this._removeCacheCanvas(); + this.dirty = false; + this.drawObject(ctx); + if (this.objectCaching && this.statefullCache) { + this.saveState({ propertySet: 'cacheProperties' }); + } + } + this.clipTo && ctx.restore(); + ctx.restore(); + }, + + renderCache: function(options) { + options = options || {}; + if (!this._cacheCanvas) { + this._createCacheCanvas(); + } + if (this.isCacheDirty()) { + this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); + this.drawObject(this._cacheContext, options.forClipping); + this.dirty = false; + } + }, + + /** + * Remove cacheCanvas and its dimensions from the objects + */ + _removeCacheCanvas: function() { + this._cacheCanvas = null; + this.cacheWidth = 0; + this.cacheHeight = 0; + }, + + /** + * return true if the object will draw a stroke + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an aproximation and be fast. + * wrote to avoid extra caching, it has to return true when stroke happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the stroke is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasStroke: function() { + return this.stroke && this.stroke !== 'transparent' && this.strokeWidth !== 0; + }, + + /** + * return true if the object will draw a fill + * Does not consider text styles. This is just a shortcut used at rendering time + * We want it to be an aproximation and be fast. + * wrote to avoid extra caching, it has to return true when fill happens, + * can guess when it will not happen at 100% chance, does not matter if it misses + * some use case where the fill is invisible. + * @since 3.0.0 + * @returns Boolean + */ + hasFill: function() { + return this.fill && this.fill !== 'transparent'; + }, + + /** + * When set to `true`, force the object to have its own cache, even if it is inside a group + * it may be needed when your object behave in a particular way on the cache and always needs + * its own isolated canvas to render correctly. + * Created to be overridden + * since 1.7.12 + * @returns Boolean + */ + needsItsOwnCache: function() { + if (this.paintFirst === 'stroke' && + this.hasFill() && this.hasStroke() && typeof this.shadow === 'object') { + return true; + } + if (this.clipPath) { + return true; + } + return false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * Read as: cache if is needed, or if the feature is enabled but we are not already caching. + * @return {Boolean} + */ + shouldCache: function() { + this.ownCaching = this.needsItsOwnCache() || ( + this.objectCaching && + (!this.group || !this.group.isOnACache()) + ); + return this.ownCaching; + }, + + /** + * Check if this object or a child object will cast a shadow + * used by Group.shouldCache to know if child has a shadow recursively + * @return {Boolean} + */ + willDrawShadow: function() { + return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); + }, + + /** + * Execute the drawing operation for an object clipPath + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCache: function(ctx) { + var path = this.clipPath; + ctx.save(); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4 + if (path.inverted) { + ctx.globalCompositeOperation = 'destination-out'; + } + else { + ctx.globalCompositeOperation = 'destination-in'; + } + //ctx.scale(1 / 2, 1 / 2); + if (path.absolutePositioned) { + var m = fabric.util.invertTransform(this.calcTransformMatrix()); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + path.transform(ctx); + ctx.scale(1 / path.zoomX, 1 / path.zoomY); + ctx.drawImage(path._cacheCanvas, -path.cacheTranslationX, -path.cacheTranslationY); + ctx.restore(); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx, forClipping) { + var originalFill = this.fill, originalStroke = this.stroke; + if (forClipping) { + this.fill = 'black'; + this.stroke = ''; + this._setClippingProperties(ctx); + } + else { + this._renderBackground(ctx); + this._setStrokeStyles(ctx, this); + this._setFillStyles(ctx, this); + } + this._render(ctx); + this._drawClipPath(ctx); + this.fill = originalFill; + this.stroke = originalStroke; + }, + + _drawClipPath: function(ctx) { + var path = this.clipPath; + if (!path) { return; } + // needed to setup a couple of variables + // path canvas gets overridden with this one. + // TODO find a better solution? + path.canvas = this.canvas; + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCache(ctx); + }, + + /** + * Paint the cached copy of the object on the target context. + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + ctx.scale(1 / this.zoomX, 1 / this.zoomY); + ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); + }, + + /** + * Check if cache is dirty + * @param {Boolean} skipCanvas skip canvas checks because this object is painted + * on parent canvas. + */ + isCacheDirty: function(skipCanvas) { + if (this.isNotVisible()) { + return false; + } + if (this._cacheCanvas && !skipCanvas && this._updateCacheCanvas()) { + // in this case the context is already cleared. + return true; + } + else { + if (this.dirty || + (this.clipPath && this.clipPath.absolutePositioned) || + (this.statefullCache && this.hasStateChanged('cacheProperties')) + ) { + if (this._cacheCanvas && !skipCanvas) { + var width = this.cacheWidth / this.zoomX; + var height = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-width / 2, -height / 2, width, height); + } + return true; + } + } + return false; + }, + + /** + * Draws a background for the object big as its untransformed dimensions + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (!this.backgroundColor) { + return; + } + var dim = this._getNonTransformedDimensions(); + ctx.fillStyle = this.backgroundColor; + + ctx.fillRect( + -dim.x / 2, + -dim.y / 2, + dim.x, + dim.y + ); + // if there is background color no other shadows + // should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setOpacity: function(ctx) { + if (this.group && !this.group._transformDone) { + ctx.globalAlpha = this.getObjectOpacity(); + } + else { + ctx.globalAlpha *= this.opacity; + } + }, + + _setStrokeStyles: function(ctx, decl) { + if (decl.stroke) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = decl.strokeLineCap; + ctx.lineDashOffset = decl.strokeDashOffset; + ctx.lineJoin = decl.strokeLineJoin; + ctx.miterLimit = decl.strokeMiterLimit; + ctx.strokeStyle = decl.stroke.toLive + ? decl.stroke.toLive(ctx, this) + : decl.stroke; + } + }, + + _setFillStyles: function(ctx, decl) { + if (decl.fill) { + ctx.fillStyle = decl.fill.toLive + ? decl.fill.toLive(ctx, this) + : decl.fill; + } + }, + + _setClippingProperties: function(ctx) { + ctx.globalAlpha = 1; + ctx.strokeStyle = 'transparent'; + ctx.fillStyle = '#000000'; + }, + + /** + * @private + * Sets line dash + * @param {CanvasRenderingContext2D} ctx Context to set the dash line on + * @param {Array} dashArray array representing dashes + * @param {Function} alternative function to call if browser does not support lineDash + */ + _setLineDash: function(ctx, dashArray, alternative) { + if (!dashArray) { + return; + } + // Spec requires the concatenation of two copies the dash list when the number of elements is odd + if (1 & dashArray.length) { + dashArray.push.apply(dashArray, dashArray); + } + if (supportsLineDash) { + ctx.setLineDash(dashArray); + } + else { + alternative && alternative(ctx); + } + if (this.strokeUniform) { + ctx.setLineDash(ctx.getLineDash().map(function(value) { return value * ctx.lineWidth; })); + } + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + */ + _renderControls: function(ctx, styleOverride) { + var vpt = this.getViewportTransform(), + matrix = this.calcTransformMatrix(), + options, drawBorders, drawControls; + styleOverride = styleOverride || { }; + drawBorders = typeof styleOverride.hasBorders !== 'undefined' ? styleOverride.hasBorders : this.hasBorders; + drawControls = typeof styleOverride.hasControls !== 'undefined' ? styleOverride.hasControls : this.hasControls; + matrix = fabric.util.multiplyTransformMatrices(vpt, matrix); + options = fabric.util.qrDecompose(matrix); + ctx.save(); + ctx.translate(options.translateX, options.translateY); + ctx.lineWidth = 1 * this.borderScaleFactor; + if (!this.group) { + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + } + if (styleOverride.forActiveSelection) { + ctx.rotate(degreesToRadians(options.angle)); + drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); + } + else { + ctx.rotate(degreesToRadians(this.angle)); + drawBorders && this.drawBorders(ctx, styleOverride); + } + drawControls && this.drawControls(ctx, styleOverride); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setShadow: function(ctx) { + if (!this.shadow) { + return; + } + + var shadow = this.shadow, canvas = this.canvas, scaling, + multX = (canvas && canvas.viewportTransform[0]) || 1, + multY = (canvas && canvas.viewportTransform[3]) || 1; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + if (canvas && canvas._isRetinaScaling()) { + multX *= fabric.devicePixelRatio; + multY *= fabric.devicePixelRatio; + } + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * fabric.browserShadowBlurConstant * + (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; + ctx.shadowOffsetX = shadow.offsetX * multX * scaling.scaleX; + ctx.shadowOffsetY = shadow.offsetY * multY * scaling.scaleY; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _removeShadow: function(ctx) { + if (!this.shadow) { + return; + } + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} filler fabric.Pattern or fabric.Gradient + * @return {Object} offset.offsetX offset for text rendering + * @return {Object} offset.offsetY offset for text rendering + */ + _applyPatternGradientTransform: function(ctx, filler) { + if (!filler || !filler.toLive) { + return { offsetX: 0, offsetY: 0 }; + } + var t = filler.gradientTransform || filler.patternTransform; + var offsetX = -this.width / 2 + filler.offsetX || 0, + offsetY = -this.height / 2 + filler.offsetY || 0; + + if (filler.gradientUnits === 'percentage') { + ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); + } + else { + ctx.transform(1, 0, 0, 1, offsetX, offsetY); + } + if (t) { + ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); + } + return { offsetX: offsetX, offsetY: offsetY }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderPaintInOrder: function(ctx) { + if (this.paintFirst === 'stroke') { + this._renderStroke(ctx); + this._renderFill(ctx); + } + else { + this._renderFill(ctx); + this._renderStroke(ctx); + } + }, + + /** + * @private + * function that actually render something on the context. + * empty here to allow Obects to work on tests to benchmark fabric functionalites + * not related to rendering + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(/* ctx */) { + + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderFill: function(ctx) { + if (!this.fill) { + return; + } + + ctx.save(); + this._applyPatternGradientTransform(ctx, this.fill); + if (this.fillRule === 'evenodd') { + ctx.fill('evenodd'); + } + else { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderStroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + if (this.strokeUniform) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); + } + this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke); + if (this.stroke.toLive && this.stroke.gradientUnits === 'percentage') { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + this._applyPatternForTransformedGradient(ctx, this.stroke); + } + else { + this._applyPatternGradientTransform(ctx, this.stroke); + } + ctx.stroke(); + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drwabacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Gradient} filler a fabric gradient instance + */ + _applyPatternForTransformedGradient: function(ctx, filler) { + var dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + pCanvas = fabric.util.createCanvasElement(), pCtx, retinaScaling = this.canvas.getRetinaScaling(), + width = dims.x / this.scaleX / retinaScaling, height = dims.y / this.scaleY / retinaScaling; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.scale( + dims.zoomX / this.scaleX / retinaScaling, + dims.zoomY / this.scaleY / retinaScaling + ); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fillStyle = filler.toLive(ctx); + pCtx.fill(); + ctx.translate(-this.width / 2 - this.strokeWidth / 2, -this.height / 2 - this.strokeWidth / 2); + ctx.scale( + retinaScaling * this.scaleX / dims.zoomX, + retinaScaling * this.scaleY / dims.zoomY + ); + ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { x: this.left + this.width / 2, y: this.top + this.height / 2 }; + }, + + /** + * This function is an helper for svg import. it decompose the transformMatrix + * and assign properties to object. + * untransformed coordinates + * @private + * @chainable + */ + _assignTransformMatrixProps: function() { + if (this.transformMatrix) { + var options = fabric.util.qrDecompose(this.transformMatrix); + this.flipX = false; + this.flipY = false; + this.set('scaleX', options.scaleX); + this.set('scaleY', options.scaleY); + this.angle = options.angle; + this.skewX = options.skewX; + this.skewY = 0; + } + }, + + /** + * This function is an helper for svg import. it removes the transform matrix + * and set to object properties that fabricjs can handle + * @private + * @param {Object} preserveAspectRatioOptions + * @return {thisArg} + */ + _removeTransformMatrix: function(preserveAspectRatioOptions) { + var center = this._findCenterFromElement(); + if (this.transformMatrix) { + this._assignTransformMatrixProps(); + center = fabric.util.transformPoint(center, this.transformMatrix); + } + this.transformMatrix = null; + if (preserveAspectRatioOptions) { + this.scaleX *= preserveAspectRatioOptions.scaleX; + this.scaleY *= preserveAspectRatioOptions.scaleY; + this.cropX = preserveAspectRatioOptions.cropX; + this.cropY = preserveAspectRatioOptions.cropY; + center.x += preserveAspectRatioOptions.offsetLeft; + center.y += preserveAspectRatioOptions.offsetTop; + this.width = preserveAspectRatioOptions.width; + this.height = preserveAspectRatioOptions.height; + } + this.setPositionByOrigin(center, 'center', 'center'); + }, + + /** + * Clones an instance, using a callback method will work for every object. + * @param {Function} callback Callback is invoked with a clone as a first argument + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + */ + clone: function(callback, propertiesToInclude) { + var objectForm = this.toObject(propertiesToInclude); + if (this.constructor.fromObject) { + this.constructor.fromObject(objectForm, callback); + } + else { + fabric.Object._fromObject('Object', objectForm, callback); + } + }, + + /** + * Creates an instance of fabric.Image out of an object + * could make use of both toDataUrl or toCanvasElement. + * @param {Function} callback callback, invoked with an instance as a first argument + * @param {Object} [options] for clone as image, passed to toDataURL + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {fabric.Object} thisArg + */ + cloneAsImage: function(callback, options) { + var canvasEl = this.toCanvasElement(options); + if (callback) { + callback(new fabric.Image(canvasEl)); + } + return this; + }, + + /** + * Converts an object into a HTMLCanvas element + * @param {Object} options Options object + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + */ + toCanvasElement: function(options) { + options || (options = { }); + + var utils = fabric.util, origParams = utils.saveObjectTransform(this), + originalShadow = this.shadow, abs = Math.abs, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); + + if (options.withoutTransform) { + utils.resetObjectTransform(this); + } + if (options.withoutShadow) { + this.shadow = null; + } + + var el = fabric.util.createCanvasElement(), + // skip canvas zoom and calculate with setCoords now. + boundingRect = this.getBoundingRect(true, true), + shadow = this.shadow, scaling, + shadowOffset = { x: 0, y: 0 }, shadowBlur; + + if (shadow) { + shadowBlur = shadow.blur; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); + } + el.width = boundingRect.width + shadowOffset.x; + el.height = boundingRect.height + shadowOffset.y; + el.width += el.width % 2 ? 2 - el.width % 2 : 0; + el.height += el.height % 2 ? 2 - el.height % 2 : 0; + var canvas = new fabric.StaticCanvas(el, { + enableRetinaScaling: false, + renderOnAddRemove: false, + skipOffscreen: false, + }); + if (options.format === 'jpeg') { + canvas.backgroundColor = '#fff'; + } + this.setPositionByOrigin(new fabric.Point(canvas.width / 2, canvas.height / 2), 'center', 'center'); + + var originalCanvas = this.canvas; + canvas.add(this); + var canvasEl = canvas.toCanvasElement(multiplier || 1, options); + this.shadow = originalShadow; + this.canvas = originalCanvas; + this.set(origParams).setCoords(); + // canvas.dispose will call image.dispose that will nullify the elements + // since this canvas is a simple element for the process, we remove references + // to objects in this way in order to avoid object trashing. + canvas._objects = []; + canvas.dispose(); + canvas = null; + + return canvasEl; + }, + + /** + * Converts an object into a data-url-like string + * @param {Object} options Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 + * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 + * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + */ + toDataURL: function(options) { + options || (options = { }); + return fabric.util.toDataURL(this.toCanvasElement(options), options.format || 'png', options.quality || 1); + }, + + /** + * Returns true if specified type is identical to the type of an instance + * @param {String} type Type to check against + * @return {Boolean} + */ + isType: function(type) { + return this.type === type; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance (is 1 unless subclassed) + */ + complexity: function() { + return 1; + }, + + /** + * Returns a JSON representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} JSON + */ + toJSON: function(propertiesToInclude) { + // delegate, not alias + return this.toObject(propertiesToInclude); + }, + + /** + * Sets gradient (fill or stroke) of an object + * percentages for x1,x2,y1,y2,r1,r2 together with gradientUnits 'pixels', are not supported. + * Backwards incompatibility note: This method was named "setGradientFill" until v1.1.0 + * @param {String} property Property name 'stroke' or 'fill' + * @param {Object} [options] Options object + * @param {String} [options.type] Type of gradient 'radial' or 'linear' + * @param {Number} [options.x1=0] x-coordinate of start point + * @param {Number} [options.y1=0] y-coordinate of start point + * @param {Number} [options.x2=0] x-coordinate of end point + * @param {Number} [options.y2=0] y-coordinate of end point + * @param {Number} [options.r1=0] Radius of start point (only for radial gradients) + * @param {Number} [options.r2=0] Radius of end point (only for radial gradients) + * @param {Object} [options.colorStops] Color stops object eg. {0: 'ff0000', 1: '000000'} + * @param {Object} [options.gradientTransform] transformMatrix for gradient + * @return {fabric.Object} thisArg + * @chainable + * @deprecated since 3.4.0 + * @see {@link http://jsfiddle.net/fabricjs/58y8b/|jsFiddle demo} + * @example Set linear gradient + * object.setGradient('fill', { + * type: 'linear', + * x1: -object.width / 2, + * y1: 0, + * x2: object.width / 2, + * y2: 0, + * colorStops: { + * 0: 'red', + * 0.5: '#005555', + * 1: 'rgba(0,0,255,0.5)' + * } + * }); + * canvas.renderAll(); + * @example Set radial gradient + * object.setGradient('fill', { + * type: 'radial', + * x1: 0, + * y1: 0, + * x2: 0, + * y2: 0, + * r1: object.width / 2, + * r2: 10, + * colorStops: { + * 0: 'red', + * 0.5: '#005555', + * 1: 'rgba(0,0,255,0.5)' + * } + * }); + * canvas.renderAll(); + */ + setGradient: function(property, options) { + options || (options = { }); + + var gradient = { colorStops: [] }; + + gradient.type = options.type || (options.r1 || options.r2 ? 'radial' : 'linear'); + gradient.coords = { + x1: options.x1, + y1: options.y1, + x2: options.x2, + y2: options.y2 + }; + gradient.gradientUnits = options.gradientUnits || 'pixels'; + if (options.r1 || options.r2) { + gradient.coords.r1 = options.r1; + gradient.coords.r2 = options.r2; + } + + gradient.gradientTransform = options.gradientTransform; + fabric.Gradient.prototype.addColorStop.call(gradient, options.colorStops); + + return this.set(property, fabric.Gradient.forObject(this, gradient)); + }, + + /** + * Sets pattern fill of an object + * @param {Object} options Options object + * @param {(String|HTMLImageElement)} options.source Pattern source + * @param {String} [options.repeat=repeat] Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @param {Number} [options.offsetX=0] Pattern horizontal offset from object's left/top corner + * @param {Number} [options.offsetY=0] Pattern vertical offset from object's left/top corner + * @param {Function} [callback] Callback to invoke when image set as a pattern + * @return {fabric.Object} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/QT3pa/|jsFiddle demo} + * @example Set pattern + * object.setPatternFill({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat' + * },canvas.renderAll.bind(canvas)); + */ + setPatternFill: function(options, callback) { + return this.set('fill', new fabric.Pattern(options, callback)); + }, + + /** + * Sets {@link fabric.Object#shadow|shadow} of an object + * @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)") + * @param {String} [options.color=rgb(0,0,0)] Shadow color + * @param {Number} [options.blur=0] Shadow blur + * @param {Number} [options.offsetX=0] Shadow horizontal offset + * @param {Number} [options.offsetY=0] Shadow vertical offset + * @return {fabric.Object} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/7gvJG/|jsFiddle demo} + * @example Set shadow with string notation + * object.setShadow('2px 2px 10px rgba(0,0,0,0.2)'); + * canvas.renderAll(); + * @example Set shadow with object notation + * object.setShadow({ + * color: 'red', + * blur: 10, + * offsetX: 20, + * offsetY: 20 + * }); + * canvas.renderAll(); + */ + setShadow: function(options) { + return this.set('shadow', options ? new fabric.Shadow(options) : null); + }, + + /** + * Sets "color" of an instance (alias of `set('fill', …)`) + * @param {String} color Color value + * @return {fabric.Object} thisArg + * @chainable + */ + setColor: function(color) { + this.set('fill', color); + return this; + }, + + /** + * Sets "angle" of an instance with centered rotation + * @param {Number} angle Angle value (in degrees) + * @return {fabric.Object} thisArg + * @chainable + */ + rotate: function(angle) { + var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; + + if (shouldCenterOrigin) { + this._setOriginToCenter(); + } + + this.set('angle', angle); + + if (shouldCenterOrigin) { + this._resetOrigin(); + } + + return this; + }, + + /** + * Centers object horizontally on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerH: function () { + this.canvas && this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object horizontally on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterH: function () { + this.canvas && this.canvas.viewportCenterObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas && this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenterV: function () { + this.canvas && this.canvas.viewportCenterObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + this.canvas && this.canvas.centerObject(this); + return this; + }, + + /** + * Centers object on current viewport of canvas to which it was added last. + * You might need to call `setCoords` on an object after centering, to update controls area. + * @return {fabric.Object} thisArg + * @chainable + */ + viewportCenter: function () { + this.canvas && this.canvas.viewportCenterObject(this); + return this; + }, + + /** + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) + */ + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var pClicked = new fabric.Point(pointer.x, pointer.y), + objectLeftTop = this._getLeftTopCoords(); + if (this.angle) { + pClicked = fabric.util.rotatePoint( + pClicked, objectLeftTop, degreesToRadians(-this.angle)); + } + return { + x: pClicked.x - objectLeftTop.x, + y: pClicked.y - objectLeftTop.y + }; + }, + + /** + * Sets canvas globalCompositeOperation for specific object + * custom composition operation for the particular object can be specified using globalCompositeOperation property + * @param {CanvasRenderingContext2D} ctx Rendering canvas context + */ + _setupCompositeOperation: function (ctx) { + if (this.globalCompositeOperation) { + ctx.globalCompositeOperation = this.globalCompositeOperation; + } + } + }); + + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Object); + + extend(fabric.Object.prototype, fabric.Observable); + + /** + * Defines the number of fraction digits to use when serializing object values. + * You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc. + * @static + * @memberOf fabric.Object + * @constant + * @type Number + */ + fabric.Object.NUM_FRACTION_DIGITS = 2; + + fabric.Object._fromObject = function(className, object, callback, extraParam) { + var klass = fabric[className]; + object = clone(object, true); + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + if (typeof patterns[0] !== 'undefined') { + object.fill = patterns[0]; + } + if (typeof patterns[1] !== 'undefined') { + object.stroke = patterns[1]; + } + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + }); + }); + }; + + /** + * Unique id used internally when creating SVG elements + * @static + * @memberOf fabric.Object + * @type Number + */ + fabric.Object.__uid = 0; + +})(typeof exports !== 'undefined' ? exports : this); +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + originXOffset = { + left: -0.5, + center: 0, + right: 0.5 + }, + originYOffset = { + top: -0.5, + center: 0, + bottom: 0.5 + }; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Translates the coordinates from a set of origin to another (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { + var x = point.x, + y = point.y, + offsetX, offsetY, dim; + + if (typeof fromOriginX === 'string') { + fromOriginX = originXOffset[fromOriginX]; + } + else { + fromOriginX -= 0.5; + } + + if (typeof toOriginX === 'string') { + toOriginX = originXOffset[toOriginX]; + } + else { + toOriginX -= 0.5; + } + + offsetX = toOriginX - fromOriginX; + + if (typeof fromOriginY === 'string') { + fromOriginY = originYOffset[fromOriginY]; + } + else { + fromOriginY -= 0.5; + } + + if (typeof toOriginY === 'string') { + toOriginY = originYOffset[toOriginY]; + } + else { + toOriginY -= 0.5; + } + + offsetY = toOriginY - fromOriginY; + + if (offsetX || offsetY) { + dim = this._getTransformedDimensions(); + x = point.x + offsetX * dim.x; + y = point.y + offsetY * dim.y; + } + + return new fabric.Point(x, y); + }, + + /** + * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToCenterPoint: function(point, originX, originY) { + var p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); + if (this.angle) { + return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Translates the coordinates from center to origin coordinates (based on the object's dimensions) + * @param {fabric.Point} center The point which corresponds to center of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToOriginPoint: function(center, originX, originY) { + var p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + if (this.angle) { + return fabric.util.rotatePoint(p, center, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Returns the real center coordinates of the object + * @return {fabric.Point} + */ + getCenterPoint: function() { + var leftTop = new fabric.Point(this.left, this.top); + return this.translateToCenterPoint(leftTop, this.originX, this.originY); + }, + + /** + * Returns the coordinates of the object based on center coordinates + * @param {fabric.Point} point The point which corresponds to the originX and originY params + * @return {fabric.Point} + */ + // getOriginPoint: function(center) { + // return this.translateToOriginPoint(center, this.originX, this.originY); + // }, + + /** + * Returns the coordinates of the object as if it has a different origin + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + getPointByOrigin: function(originX, originY) { + var center = this.getCenterPoint(); + return this.translateToOriginPoint(center, originX, originY); + }, + + /** + * Returns the point in local coordinates + * @param {fabric.Point} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + toLocalPoint: function(point, originX, originY) { + var center = this.getCenterPoint(), + p, p2; + + if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { + p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + } + else { + p = new fabric.Point(this.left, this.top); + } + + p2 = new fabric.Point(point.x, point.y); + if (this.angle) { + p2 = fabric.util.rotatePoint(p2, center, -degreesToRadians(this.angle)); + } + return p2.subtractEquals(p); + }, + + /** + * Returns the point in global coordinates + * @param {fabric.Point} The point relative to the local coordinate system + * @return {fabric.Point} + */ + // toGlobalPoint: function(point) { + // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top)); + // }, + + /** + * Sets the position of the object taking into consideration the object's origin + * @param {fabric.Point} pos The new position of the object + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {void} + */ + setPositionByOrigin: function(pos, originX, originY) { + var center = this.translateToCenterPoint(pos, originX, originY), + position = this.translateToOriginPoint(center, this.originX, this.originY); + this.set('left', position.x); + this.set('top', position.y); + }, + + /** + * @param {String} to One of 'left', 'center', 'right' + */ + adjustPosition: function(to) { + var angle = degreesToRadians(this.angle), + hypotFull = this.getScaledWidth(), + xFull = fabric.util.cos(angle) * hypotFull, + yFull = fabric.util.sin(angle) * hypotFull, + offsetFrom, offsetTo; + + //TODO: this function does not consider mixed situation like top, center. + if (typeof this.originX === 'string') { + offsetFrom = originXOffset[this.originX]; + } + else { + offsetFrom = this.originX - 0.5; + } + if (typeof to === 'string') { + offsetTo = originXOffset[to]; + } + else { + offsetTo = to - 0.5; + } + this.left += xFull * (offsetTo - offsetFrom); + this.top += yFull * (offsetTo - offsetFrom); + this.setCoords(); + this.originX = to; + }, + + /** + * Sets the origin/position of the object to it's center point + * @private + * @return {void} + */ + _setOriginToCenter: function() { + this._originalOriginX = this.originX; + this._originalOriginY = this.originY; + + var center = this.getCenterPoint(); + + this.originX = 'center'; + this.originY = 'center'; + + this.left = center.x; + this.top = center.y; + }, + + /** + * Resets the origin/position of the object to it's original origin + * @private + * @return {void} + */ + _resetOrigin: function() { + var originPoint = this.translateToOriginPoint( + this.getCenterPoint(), + this._originalOriginX, + this._originalOriginY); + + this.originX = this._originalOriginX; + this.originY = this._originalOriginY; + + this.left = originPoint.x; + this.top = originPoint.y; + + this._originalOriginX = null; + this._originalOriginY = null; + }, + + /** + * @private + */ + _getLeftTopCoords: function() { + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + }, + }); + +})(); +(function() { + + function getCoords(coords) { + return [ + new fabric.Point(coords.tl.x, coords.tl.y), + new fabric.Point(coords.tr.x, coords.tr.y), + new fabric.Point(coords.br.x, coords.br.y), + new fabric.Point(coords.bl.x, coords.bl.y) + ]; + } + + var degreesToRadians = fabric.util.degreesToRadians, + multiplyMatrices = fabric.util.multiplyTransformMatrices, + transformPoint = fabric.util.transformPoint; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Describe object's corner position in canvas element coordinates. + * properties are tl,mt,tr,ml,mr,bl,mb,br,mtr for the main controls. + * each property is an object with x, y and corner. + * The `corner` property contains in a similar manner the 4 points of the + * interactive area of the corner. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, viewportTransform, top, left, padding. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcCoords; + * @memberOf fabric.Object.prototype + */ + oCoords: null, + + /** + * Describe object's corner position in canvas object absolute coordinates + * properties are tl,tr,bl,br and describe the four main corner. + * each property is an object with x, y, instance of Fabric.Point. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, top, left. + * Those coordinates are useful to understand where an object is. They get updated + * with oCoords but they do not need to be updated when zoom or panning change. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcCoords(true); + * @memberOf fabric.Object.prototype + */ + aCoords: null, + + /** + * storage for object transform matrix + */ + ownMatrixCache: null, + + /** + * storage for object full transform matrix + */ + matrixCache: null, + + /** + * return correct set of coordinates for intersection + */ + getCoords: function(absolute, calculate) { + if (!this.oCoords) { + this.setCoords(); + } + var coords = absolute ? this.aCoords : this.oCoords; + return getCoords(calculate ? this.calcCoords(absolute) : coords); + }, + + /** + * Checks if object intersects with an area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with an area formed by 2 points + */ + intersectsWithRect: function(pointTL, pointBR, absolute, calculate) { + var coords = this.getCoords(absolute, calculate), + intersection = fabric.Intersection.intersectPolygonRectangle( + coords, + pointTL, + pointBR + ); + return intersection.status === 'Intersection'; + }, + + /** + * Checks if object intersects with another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with another object + */ + intersectsWithObject: function(other, absolute, calculate) { + var intersection = fabric.Intersection.intersectPolygonPolygon( + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) + ); + + return intersection.status === 'Intersection' + || other.isContainedWithinObject(this, absolute, calculate) + || this.isContainedWithinObject(other, absolute, calculate); + }, + + /** + * Checks if object is fully contained within area of another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area of another object + */ + isContainedWithinObject: function(other, absolute, calculate) { + var points = this.getCoords(absolute, calculate), + i = 0, lines = other._getImageLines( + calculate ? other.calcCoords(absolute) : absolute ? other.aCoords : other.oCoords + ); + for (; i < 4; i++) { + if (!other.containsPoint(points[i], lines)) { + return false; + } + } + return true; + }, + + /** + * Checks if object is fully contained within area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area formed by 2 points + */ + isContainedWithinRect: function(pointTL, pointBR, absolute, calculate) { + var boundingRect = this.getBoundingRect(absolute, calculate); + + return ( + boundingRect.left >= pointTL.x && + boundingRect.left + boundingRect.width <= pointBR.x && + boundingRect.top >= pointTL.y && + boundingRect.top + boundingRect.height <= pointBR.y + ); + }, + + /** + * Checks if point is inside the object + * @param {fabric.Point} point Point to check against + * @param {Object} [lines] object returned from @method _getImageLines + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if point is inside the object + */ + containsPoint: function(point, lines, absolute, calculate) { + var lines = lines || this._getImageLines( + calculate ? this.calcCoords(absolute) : absolute ? this.aCoords : this.oCoords + ), + xPoints = this._findCrossPoints(point, lines); + + // if xPoints is odd then point is inside the object + return (xPoints !== 0 && xPoints % 2 === 1); + }, + + /** + * Checks if object is contained within the canvas with current viewportTransform + * the check is done stopping at first point that appears on screen + * @param {Boolean} [calculate] use coordinates of current position instead of .aCoords + * @return {Boolean} true if object is fully or partially contained within canvas + */ + isOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + var points = this.getCoords(true, calculate), point; + for (var i = 0; i < 4; i++) { + point = points[i]; + if (point.x <= pointBR.x && point.x >= pointTL.x && point.y <= pointBR.y && point.y >= pointTL.y) { + return true; + } + } + // no points on screen, check intersection with absolute coordinates + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + return this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Checks if the object contains the midpoint between canvas extremities + * Does not make sense outside the context of isOnScreen and isPartiallyOnScreen + * @private + * @param {Fabric.Point} pointTL Top Left point + * @param {Fabric.Point} pointBR Top Right point + * @param {Boolean} calculate use coordinates of current position instead of .oCoords + * @return {Boolean} true if the object contains the point + */ + _containsCenterOfCanvas: function(pointTL, pointBR, calculate) { + // worst case scenario the object is so big that contains the screen + var centerPoint = { x: (pointTL.x + pointBR.x) / 2, y: (pointTL.y + pointBR.y) / 2 }; + if (this.containsPoint(centerPoint, null, true, calculate)) { + return true; + } + return false; + }, + + /** + * Checks if object is partially contained within the canvas with current viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is partially contained within canvas + */ + isPartiallyOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { + return true; + } + return this._containsCenterOfCanvas(pointTL, pointBR, calculate); + }, + + /** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} oCoords Coordinates of the object corners + */ + _getImageLines: function(oCoords) { + return { + topline: { + o: oCoords.tl, + d: oCoords.tr + }, + rightline: { + o: oCoords.tr, + d: oCoords.br + }, + bottomline: { + o: oCoords.br, + d: oCoords.bl + }, + leftline: { + o: oCoords.bl, + d: oCoords.tl + } + }; + }, + + /** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {fabric.Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + */ + // remove yi, not used but left code here just in case. + _findCrossPoints: function(point, lines) { + var b1, b2, a1, a2, xi, // yi, + xcount = 0, + iLine; + + for (var lineKey in lines) { + iLine = lines[lineKey]; + // optimisation 1: line below point. no cross + if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { + continue; + } + // optimisation 2: line above point. no cross + if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { + xi = iLine.o.x; + // yi = point.y; + } + // calculate the intersection point + else { + b1 = 0; + b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + a1 = point.y - b1 * point.x; + a2 = iLine.o.y - b2 * iLine.o.x; + + xi = -(a1 - a2) / (b1 - b2); + // yi = a1 + b1 * xi; + } + // dont count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimisation 4: specific for square images + if (xcount === 2) { + break; + } + } + return xcount; + }, + + /** + * Returns coordinates of object's bounding rectangle (left, top, width, height) + * the box is intended as aligned to axis of canvas. + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords / .aCoords + * @return {Object} Object with left, top, width, height properties + */ + getBoundingRect: function(absolute, calculate) { + var coords = this.getCoords(absolute, calculate); + return fabric.util.makeBoundingBoxFromPoints(coords); + }, + + /** + * Returns width of an object's bounding box counting transformations + * before 2.0 it was named getWidth(); + * @return {Number} width value + */ + getScaledWidth: function() { + return this._getTransformedDimensions().x; + }, + + /** + * Returns height of an object bounding box counting transformations + * before 2.0 it was named getHeight(); + * @return {Number} height value + */ + getScaledHeight: function() { + return this._getTransformedDimensions().y; + }, + + /** + * Makes sure the scale is valid and modifies it if necessary + * @private + * @param {Number} value + * @return {Number} + */ + _constrainScale: function(value) { + if (Math.abs(value) < this.minScaleLimit) { + if (value < 0) { + return -this.minScaleLimit; + } + else { + return this.minScaleLimit; + } + } + else if (value === 0) { + return 0.0001; + } + return value; + }, + + /** + * Scales an object (equally by x and y) + * @param {Number} value Scale factor + * @return {fabric.Object} thisArg + * @chainable + */ + scale: function(value) { + this._set('scaleX', value); + this._set('scaleY', value); + return this.setCoords(); + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New width value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).width / this.getScaledWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New height value + * @param {Boolean} absolute ignore viewport + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value, absolute) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect(absolute).height / this.getScaledHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * Calculates and returns the .coords of an object. + * @return {Object} Object with tl, tr, br, bl .... + * @chainable + */ + calcCoords: function(absolute) { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + startMatrix = multiplyMatrices(translateMatrix, rotateMatrix), + vpt = this.getViewportTransform(), + finalMatrix = absolute ? startMatrix : multiplyMatrices(vpt, startMatrix), + dim = this._getTransformedDimensions(), + w = dim.x / 2, h = dim.y / 2, + tl = transformPoint({ x: -w, y: -h }, finalMatrix), + tr = transformPoint({ x: w, y: -h }, finalMatrix), + bl = transformPoint({ x: -w, y: h }, finalMatrix), + br = transformPoint({ x: w, y: h }, finalMatrix); + if (!absolute) { + var padding = this.padding, angle = degreesToRadians(this.angle), + cos = fabric.util.cos(angle), sin = fabric.util.sin(angle), + cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, + cosPMinusSinP = cosP - sinP; + if (padding) { + tl.x -= cosPMinusSinP; + tl.y -= cosPSinP; + tr.x += cosPSinP; + tr.y -= cosPMinusSinP; + bl.x -= cosPSinP; + bl.y += cosPMinusSinP; + br.x += cosPMinusSinP; + br.y += cosPSinP; + } + var ml = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2), + mt = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2), + mr = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2), + mb = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2), + mtr = new fabric.Point(mt.x + sin * this.rotatingPointOffset, mt.y - cos * this.rotatingPointOffset); + } + + // if (!absolute) { + // var canvas = this.canvas; + // setTimeout(function() { + // canvas.contextTop.clearRect(0, 0, 700, 700); + // canvas.contextTop.fillStyle = 'green'; + // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + // canvas.contextTop.fillRect(br.x, br.y, 3, 3); + // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + // canvas.contextTop.fillRect(mtr.x, mtr.y, 3, 3); + // }, 50); + // } + + var coords = { + // corners + tl: tl, tr: tr, br: br, bl: bl, + }; + if (!absolute) { + // middle + coords.ml = ml; + coords.mt = mt; + coords.mr = mr; + coords.mb = mb; + // rotating point + coords.mtr = mtr; + } + return coords; + }, + + /** + * Sets corner position coordinates based on current angle, width and height. + * See {@link https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords|When-to-call-setCoords} + * @param {Boolean} [ignoreZoom] set oCoords with or without the viewport transform. + * @param {Boolean} [skipAbsolute] skip calculation of aCoords, useful in setViewportTransform + * @return {fabric.Object} thisArg + * @chainable + */ + setCoords: function(ignoreZoom, skipAbsolute) { + this.oCoords = this.calcCoords(ignoreZoom); + if (!skipAbsolute) { + this.aCoords = this.calcCoords(true); + } + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + ignoreZoom || (this._setCornerCoords && this._setCornerCoords()); + + return this; + }, + + /** + * calculate rotation matrix of an object + * @return {Array} rotation matrix for the object + */ + _calcRotateMatrix: function() { + return fabric.util.calcRotateMatrix(this); + }, + + /** + * calculate the translation matrix for an object transform + * @return {Array} rotation matrix for the object + */ + _calcTranslateMatrix: function() { + var center = this.getCenterPoint(); + return [1, 0, 0, 1, center.x, center.y]; + }, + + transformMatrixKey: function(skipGroup) { + var sep = '_', prefix = ''; + if (!skipGroup && this.group) { + prefix = this.group.transformMatrixKey(skipGroup) + sep; + }; + return prefix + this.top + sep + this.left + sep + this.scaleX + sep + this.scaleY + + sep + this.skewX + sep + this.skewY + sep + this.angle + sep + this.originX + sep + this.originY + + sep + this.width + sep + this.height + sep + this.strokeWidth + this.flipX + this.flipY; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties. + * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations + * @return {Array} transform matrix for the object + */ + calcTransformMatrix: function(skipGroup) { + if (skipGroup) { + return this.calcOwnMatrix(); + } + var key = this.transformMatrixKey(), cache = this.matrixCache || (this.matrixCache = {}); + if (cache.key === key) { + return cache.value; + } + var matrix = this.calcOwnMatrix(); + if (this.group) { + matrix = multiplyMatrices(this.group.calcTransformMatrix(), matrix); + } + cache.key = key; + cache.value = matrix; + return matrix; + }, + + /** + * calculate transform matrix that represents the current transformations from the + * object's properties, this matrix does not include the group transformation + * @return {Array} transform matrix for the object + */ + calcOwnMatrix: function() { + var key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {}); + if (cache.key === key) { + return cache.value; + } + var tMatrix = this._calcTranslateMatrix(); + this.translateX = tMatrix[4]; + this.translateY = tMatrix[5]; + cache.key = key; + cache.value = fabric.util.composeMatrix(this); + return cache.value; + }, + + /* + * Calculate object dimensions from its properties + * @private + * @deprecated since 3.4.0, please use fabric.util._calcDimensionsTransformMatrix + * not including or including flipX, flipY to emulate the flipping boolean + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { + return fabric.util.calcDimensionsMatrix({ + skewX: skewX, + skewY: skewY, + scaleX: this.scaleX * (flipping && this.flipX ? -1 : 1), + scaleY: this.scaleY * (flipping && this.flipY ? -1 : 1) + }); + }, + + /* + * Calculate object dimensions from its properties + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getNonTransformedDimensions: function() { + var strokeWidth = this.strokeWidth, + w = this.width + strokeWidth, + h = this.height + strokeWidth; + return { x: w, y: h }; + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param {Number} skewX, a value to override current skewX + * @param {Number} skewY, a value to override current skewY + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; + } + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions = this._getNonTransformedDimensions(), dimX, dimY, + noSkew = skewX === 0 && skewY === 0; + + if (this.strokeUniform) { + dimX = this.width; + dimY = this.height; + } + else { + dimX = dimensions.x; + dimY = dimensions.y; + } + if (noSkew) { + return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); + } + else { + dimX /= 2; + dimY /= 2; + } + var points = [ + { + x: -dimX, + y: -dimY + }, + { + x: dimX, + y: -dimY + }, + { + x: -dimX, + y: dimY + }, + { + x: dimX, + y: dimY + }], + transformMatrix = fabric.util.calcDimensionsMatrix({ + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + }), + bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); + return this._finalizeDimensions(bbox.width, bbox.height); + }, + + /* + * Calculate object bounding box dimensions from its properties scale, skew. + * @param Number width width of the bbox + * @param Number height height of the bbox + * @private + * @return {Object} .x finalized width dimension + * @return {Object} .y finalized height dimension + */ + _finalizeDimensions: function(width, height) { + return this.strokeUniform ? + { x: width + this.strokeWidth, y: height + this.strokeWidth } + : + { x: width, y: height }; + }, + /* + * Calculate object dimensions for controls, including padding and canvas zoom. + * private + */ + _calculateCurrentDimensions: function() { + var vpt = this.getViewportTransform(), + dim = this._getTransformedDimensions(), + p = fabric.util.transformPoint(dim, vpt, true); + + return p.scalarAdd(2 * this.padding); + }, + }); +})(); +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Moves an object to the bottom of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + if (this.group) { + fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); + } + else { + this.canvas.sendToBack(this); + } + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + if (this.group) { + fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); + } + else { + this.canvas.bringToFront(this); + } + return this; + }, + + /** + * Moves an object down in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); + } + else { + this.canvas.sendBackwards(this, intersecting); + } + return this; + }, + + /** + * Moves an object up in stack of drawn objects + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function(intersecting) { + if (this.group) { + fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); + } + else { + this.canvas.bringForward(this, intersecting); + } + return this; + }, + + /** + * Moves an object to specified level in stack of drawn objects + * @param {Number} index New position of object + * @return {fabric.Object} thisArg + * @chainable + */ + moveTo: function(index) { + if (this.group && this.group.type !== 'activeSelection') { + fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); + } + else { + this.canvas.moveTo(this, index); + } + return this; + } +}); +/* _TO_SVG_START_ */ +(function() { + function getSvgColorString(prop, value) { + if (!value) { + return prop + ': none; '; + } + else if (value.toLive) { + return prop + ': url(#SVGID_' + value.id + '); '; + } + else { + var color = new fabric.Color(value), + str = prop + ': ' + color.toRgb() + '; ', + opacity = color.getAlpha(); + if (opacity !== 1) { + //change the color in rgb + opacity + str += prop + '-opacity: ' + opacity.toString() + '; '; + } + return str; + } + } + + var toFixed = fabric.util.toFixed; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + + var fillRule = this.fillRule ? this.fillRule : 'nonzero', + strokeWidth = this.strokeWidth ? this.strokeWidth : '0', + strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none', + strokeDashOffset = this.strokeDashOffset ? this.strokeDashOffset : '0', + strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt', + strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter', + strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4', + opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', + visibility = this.visible ? '' : ' visibility: hidden;', + filter = skipShadow ? '' : this.getSvgFilter(), + fill = getSvgColorString('fill', this.fill), + stroke = getSvgColorString('stroke', this.stroke); + + return [ + stroke, + 'stroke-width: ', strokeWidth, '; ', + 'stroke-dasharray: ', strokeDashArray, '; ', + 'stroke-linecap: ', strokeLineCap, '; ', + 'stroke-dashoffset: ', strokeDashOffset, '; ', + 'stroke-linejoin: ', strokeLineJoin, '; ', + 'stroke-miterlimit: ', strokeMiterLimit, '; ', + fill, + 'fill-rule: ', fillRule, '; ', + 'opacity: ', opacity, ';', + filter, + visibility + ].join(''); + }, + + /** + * Returns styles-string for svg-export + * @param {Object} style the object from which to retrieve style properties + * @param {Boolean} useWhiteSpace a boolean to include an additional attribute in the style. + * @return {String} + */ + getSvgSpanStyles: function(style, useWhiteSpace) { + var term = '; '; + var fontFamily = style.fontFamily ? + 'font-family: ' + (((style.fontFamily.indexOf('\'') === -1 && style.fontFamily.indexOf('"') === -1) ? + '\'' + style.fontFamily + '\'' : style.fontFamily)) + term : ''; + var strokeWidth = style.strokeWidth ? 'stroke-width: ' + style.strokeWidth + term : '', + fontFamily = fontFamily, + fontSize = style.fontSize ? 'font-size: ' + style.fontSize + 'px' + term : '', + fontStyle = style.fontStyle ? 'font-style: ' + style.fontStyle + term : '', + fontWeight = style.fontWeight ? 'font-weight: ' + style.fontWeight + term : '', + fill = style.fill ? getSvgColorString('fill', style.fill) : '', + stroke = style.stroke ? getSvgColorString('stroke', style.stroke) : '', + textDecoration = this.getSvgTextDecoration(style), + deltaY = style.deltaY ? 'baseline-shift: ' + (-style.deltaY) + '; ' : ''; + if (textDecoration) { + textDecoration = 'text-decoration: ' + textDecoration + term; + } + + return [ + stroke, + strokeWidth, + fontFamily, + fontSize, + fontStyle, + fontWeight, + textDecoration, + fill, + deltaY, + useWhiteSpace ? 'white-space: pre; ' : '' + ].join(''); + }, + + /** + * Returns text-decoration property for svg-export + * @param {Object} style the object from which to retrieve style properties + * @return {String} + */ + getSvgTextDecoration: function(style) { + if ('overline' in style || 'underline' in style || 'linethrough' in style) { + return (style.overline ? 'overline ' : '') + + (style.underline ? 'underline ' : '') + (style.linethrough ? 'line-through ' : ''); + } + return ''; + }, + + /** + * Returns filter for svg shadow + * @return {String} + */ + getSvgFilter: function() { + return this.shadow ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + }, + + /** + * Returns id attribute for svg output + * @return {String} + */ + getSvgCommons: function() { + return [ + this.id ? 'id="' + this.id + '" ' : '', + this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '', + ].join(''); + }, + + /** + * Returns transform-string for svg-export + * @param {Boolean} use the full transform or the single object one. + * @return {String} + */ + getSvgTransform: function(full, additionalTransform) { + var transform = full ? this.calcTransformMatrix() : this.calcOwnMatrix(), + svgTransform = 'transform="' + fabric.util.matrixToSVG(transform); + return svgTransform + + (additionalTransform || '') + this.getSvgTransformMatrix() + '" '; + }, + + /** + * Returns transform-string for svg-export from the transform matrix of single elements + * @return {String} + */ + getSvgTransformMatrix: function() { + return this.transformMatrix ? ' ' + fabric.util.matrixToSVG(this.transformMatrix) : ''; + }, + + _setSVGBg: function(textBgRects) { + if (this.backgroundColor) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + textBgRects.push( + '\t\t\n'); + } + }, + + /** + * 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: reviver }); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + return '\t' + this._createBaseClipPathSVGMarkup(this._toSVG(reviver), { reviver: reviver }); + }, + + /** + * @private + */ + _createBaseClipPathSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var reviver = options.reviver, + additionalTransform = options.additionalTransform || '', + commonPieces = [ + this.getSvgTransform(true, additionalTransform), + this.getSvgCommons(), + ].join(''), + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'); + objectMarkup[index] = commonPieces; + return reviver ? reviver(objectMarkup.join('')) : objectMarkup.join(''); + }, + + /** + * @private + */ + _createBaseSVGMarkup: function(objectMarkup, options) { + options = options || {}; + var noStyle = options.noStyle, + reviver = options.reviver, + styleInfo = noStyle ? '' : 'style="' + this.getSvgStyles() + '" ', + shadowInfo = options.withShadow ? 'style="' + this.getSvgFilter() + '" ' : '', + clipPath = this.clipPath, + vectorEffect = this.strokeUniform ? 'vector-effect="non-scaling-stroke" ' : '', + absoluteClipPath = clipPath && clipPath.absolutePositioned, + stroke = this.stroke, fill = this.fill, shadow = this.shadow, + commonPieces, markup = [], clipPathMarkup, + // insert commons in the markup, style and svgCommons + index = objectMarkup.indexOf('COMMON_PARTS'), + additionalTransform = options.additionalTransform; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + fabric.Object.__uid++; + clipPathMarkup = '\n' + + clipPath.toClipPathSVG(reviver) + + '\n'; + } + if (absoluteClipPath) { + markup.push( + '\n' + ); + } + markup.push( + '\n' + ); + commonPieces = [ + styleInfo, + vectorEffect, + noStyle ? '' : this.addPaintOrder(), ' ', + additionalTransform ? 'transform="' + additionalTransform + '" ' : '', + ].join(''); + objectMarkup[index] = commonPieces; + if (fill && fill.toLive) { + markup.push(fill.toSVG(this)); + } + if (stroke && stroke.toLive) { + markup.push(stroke.toSVG(this)); + } + if (shadow) { + markup.push(shadow.toSVG(this)); + } + if (clipPath) { + markup.push(clipPathMarkup); + } + markup.push(objectMarkup.join('')); + markup.push('\n'); + absoluteClipPath && markup.push('\n'); + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + addPaintOrder: function() { + return this.paintFirst !== 'fill' ? ' paint-order="' + this.paintFirst + '" ' : ''; + } + }); +})(); +/* _TO_SVG_END_ */ +(function() { + + var extend = fabric.util.object.extend, + originalSet = 'stateProperties'; + + /* + Depends on `stateProperties` + */ + function saveProps(origin, destination, props) { + var tmpObj = { }, deep = true; + props.forEach(function(prop) { + tmpObj[prop] = origin[prop]; + }); + extend(origin[destination], tmpObj, deep); + } + + function _isEqual(origValue, currentValue, firstPass) { + if (origValue === currentValue) { + // if the objects are identical, return + return true; + } + else if (Array.isArray(origValue)) { + if (!Array.isArray(currentValue) || origValue.length !== currentValue.length) { + return false; + } + for (var i = 0, len = origValue.length; i < len; i++) { + if (!_isEqual(origValue[i], currentValue[i])) { + return false; + } + } + return true; + } + else if (origValue && typeof origValue === 'object') { + var keys = Object.keys(origValue), key; + if (!currentValue || + typeof currentValue !== 'object' || + (!firstPass && keys.length !== Object.keys(currentValue).length) + ) { + return false; + } + for (var i = 0, len = keys.length; i < len; i++) { + key = keys[i]; + // since clipPath is in the statefull cache list and the clipPath objects + // would be iterated as an object, this would lead to possible infinite recursion + if (key === 'canvas') { + continue; + } + if (!_isEqual(origValue[key], currentValue[key])) { + return false; + } + } + return true; + } + } + + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns true if object state (one of its state properties) was changed + * @param {String} [propertySet] optional name for the set of property we want to save + * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called + */ + hasStateChanged: function(propertySet) { + propertySet = propertySet || originalSet; + var dashedPropertySet = '_' + propertySet; + if (Object.keys(this[dashedPropertySet]).length < this[propertySet].length) { + return true; + } + return !_isEqual(this[dashedPropertySet], this, true); + }, + + /** + * Saves state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + saveState: function(options) { + var propertySet = options && options.propertySet || originalSet, + destination = '_' + propertySet; + if (!this[destination]) { + return this.setupState(options); + } + saveProps(this, destination, this[propertySet]); + if (options && options.stateProperties) { + saveProps(this, destination, options.stateProperties); + } + return this; + }, + + /** + * Setups state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + setupState: function(options) { + options = options || { }; + var propertySet = options.propertySet || originalSet; + options.propertySet = propertySet; + this['_' + propertySet] = { }; + this.saveState(options); + return this; + } + }); +})(); +(function() { + + var degreesToRadians = fabric.util.degreesToRadians; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * The object interactivity controls. + * @private + */ + _controlsVisibility: null, + + /** + * Determines which corner has been clicked + * @private + * @param {Object} pointer The pointer indicating the mouse position + * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found + */ + _findTargetCorner: function(pointer) { + // objects in group, anykind, are not self modificable, + // must not return an hovered corner. + if (!this.hasControls || this.group || (!this.canvas || this.canvas._activeObject !== this)) { + return false; + } + + var ex = pointer.x, + ey = pointer.y, + xPoints, + lines; + this.__corner = 0; + for (var i in this.oCoords) { + + if (!this.isControlVisible(i)) { + continue; + } + + if (i === 'mtr' && !this.hasRotatingPoint) { + continue; + } + + if (this.get('lockUniScaling') && + (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { + continue; + } + + lines = this._getImageLines(this.oCoords[i].corner); + + // debugging + + // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + xPoints = this._findCrossPoints({ x: ex, y: ey }, lines); + if (xPoints !== 0 && xPoints % 2 === 1) { + this.__corner = i; + return i; + } + } + return false; + }, + + /** + * Sets the coordinates of the draggable boxes in the corners of + * the image used to scale/rotate it. + * @private + */ + _setCornerCoords: function() { + var coords = this.oCoords, + newTheta = degreesToRadians(45 - this.angle), + /* Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, */ + /* 0.707106 stands for sqrt(2)/2 */ + cornerHypotenuse = this.cornerSize * 0.707106, + cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta), + sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta), + x, y; + + for (var point in coords) { + x = coords[point].x; + y = coords[point].y; + coords[point].corner = { + tl: { + x: x - sinHalfOffset, + y: y - cosHalfOffset + }, + tr: { + x: x + cosHalfOffset, + y: y - sinHalfOffset + }, + bl: { + x: x - cosHalfOffset, + y: y + sinHalfOffset + }, + br: { + x: x + sinHalfOffset, + y: y + cosHalfOffset + } + }; + } + }, + + /** + * Draws a colored layer behind the object, inside its selection borders. + * Requires public options: padding, selectionBackgroundColor + * this function is called when the context is transformed + * has checks to be skipped when the object is on a staticCanvas + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawSelectionBackground: function(ctx) { + if (!this.selectionBackgroundColor || + (this.canvas && !this.canvas.interactive) || + (this.canvas && this.canvas._activeObject !== this) + ) { + return this; + } + ctx.save(); + var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), + vpt = this.canvas.viewportTransform; + ctx.translate(center.x, center.y); + ctx.scale(1 / vpt[0], 1 / vpt[3]); + ctx.rotate(degreesToRadians(this.angle)); + ctx.fillStyle = this.selectionBackgroundColor; + ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBorders: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + var wh = this._calculateCurrentDimensions(), + strokeWidth = 1 / this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth, + drawRotatingPoint = typeof styleOverride.hasRotatingPoint !== 'undefined' ? + styleOverride.hasRotatingPoint : this.hasRotatingPoint, + hasControls = typeof styleOverride.hasControls !== 'undefined' ? + styleOverride.hasControls : this.hasControls, + rotatingPointOffset = typeof styleOverride.rotatingPointOffset !== 'undefined' ? + styleOverride.rotatingPointOffset : this.rotatingPointOffset; + + ctx.save(); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray, null); + + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + if (drawRotatingPoint && this.isControlVisible('mtr') && hasControls) { + + var rotateHeight = -height / 2; + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight - rotatingPointOffset); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box when it is inside a group. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {object} options object representing current object parameters + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawBordersInGroup: function(ctx, options, styleOverride) { + styleOverride = styleOverride || {}; + var p = this._getNonTransformedDimensions(), + matrix = fabric.util.composeMatrix({ + scaleX: options.scaleX, + scaleY: options.scaleY, + skewX: options.skewX + }), + wh = fabric.util.transformPoint(p, matrix), + strokeWidth = 1 / this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth; + + ctx.save(); + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray, null); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + ctx.restore(); + return this; + }, + + /** + * Draws corners of an object's bounding box. + * Requires public properties: width, height + * Requires public options: cornerSize, padding + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {Object} styleOverride object to override the object style + * @return {fabric.Object} thisArg + * @chainable + */ + drawControls: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; + var wh = this._calculateCurrentDimensions(), + width = wh.x, + height = wh.y, + scaleOffset = styleOverride.cornerSize || this.cornerSize, + left = -(width + scaleOffset) / 2, + top = -(height + scaleOffset) / 2, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : this.transparentCorners, + hasRotatingPoint = typeof styleOverride.hasRotatingPoint !== 'undefined' ? + styleOverride.hasRotatingPoint : this.hasRotatingPoint, + methodName = transparentCorners ? 'stroke' : 'fill'; + + ctx.save(); + ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; + if (!this.transparentCorners) { + ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor; + } + this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray, null); + + // top-left + this._drawControl('tl', ctx, methodName, + left, + top, styleOverride); + + // top-right + this._drawControl('tr', ctx, methodName, + left + width, + top, styleOverride); + + // bottom-left + this._drawControl('bl', ctx, methodName, + left, + top + height, styleOverride); + + // bottom-right + this._drawControl('br', ctx, methodName, + left + width, + top + height, styleOverride); + + if (!this.get('lockUniScaling')) { + + // middle-top + this._drawControl('mt', ctx, methodName, + left + width / 2, + top, styleOverride); + + // middle-bottom + this._drawControl('mb', ctx, methodName, + left + width / 2, + top + height, styleOverride); + + // middle-right + this._drawControl('mr', ctx, methodName, + left + width, + top + height / 2, styleOverride); + + // middle-left + this._drawControl('ml', ctx, methodName, + left, + top + height / 2, styleOverride); + } + + // middle-top-rotate + if (hasRotatingPoint) { + this._drawControl('mtr', ctx, methodName, + left + width / 2, + top - this.rotatingPointOffset, styleOverride); + } + + ctx.restore(); + + return this; + }, + + /** + * @private + */ + _drawControl: function(control, ctx, methodName, left, top, styleOverride) { + styleOverride = styleOverride || {}; + if (!this.isControlVisible(control)) { + return; + } + var size = this.cornerSize, stroke = !this.transparentCorners && this.cornerStrokeColor; + switch (styleOverride.cornerStyle || this.cornerStyle) { + case 'circle': + ctx.beginPath(); + ctx.arc(left + size / 2, top + size / 2, size / 2, 0, 2 * Math.PI, false); + ctx[methodName](); + if (stroke) { + ctx.stroke(); + } + break; + default: + this.transparentCorners || ctx.clearRect(left, top, size, size); + ctx[methodName + 'Rect'](left, top, size, size); + if (stroke) { + ctx.strokeRect(left, top, size, size); + } + } + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlName) { + return this._getControlsVisibility()[controlName]; + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlName, visible) { + this._getControlsVisibility()[controlName] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + /** + * Returns the instance of the control visibility set for this object. + * @private + * @returns {Object} + */ + _getControlsVisibility: function() { + if (!this._controlsVisibility) { + this._controlsVisibility = { + tl: true, + tr: true, + br: true, + bl: true, + ml: true, + mt: true, + mr: true, + mb: true, + mtr: true + }; + } + return this._controlsVisibility; + }, + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to deselect this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onDeselect: function() { + // implemented by sub-classes, as needed. + }, + + + /** + * This callback function is called every time _discardActiveObject or _setActiveObject + * try to to select this object. If the function returns true, the process is cancelled + * @param {Object} [options] options sent from the upper functions + * @param {Event} [options.e] event if the process is generated by an event + */ + onSelect: function() { + // implemented by sub-classes, as needed. + } + }); +})(); +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Animation duration (in ms) for fx* methods + * @type Number + * @default + */ + FX_DURATION: 500, + + /** + * Centers object horizontally with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectH: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.left, + endValue: this.getCenter().left, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('left', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Centers object vertically with animation. + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectV: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.top, + endValue: this.getCenter().top, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('top', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Same as `fabric.Canvas#remove` but animated + * @param {fabric.Object} object Object to remove + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxRemove: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.opacity, + endValue: 0, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('opacity', value); + _this.requestRenderAll(); + onChange(); + }, + onComplete: function () { + _this.remove(object); + onComplete(); + } + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Animates object's properties + * @param {String|Object} property Property to animate (if string) or properties to animate (if object) + * @param {Number|Object} value Value to animate property to (if string was given first) or options object + * @return {fabric.Object} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#animation} + * @chainable + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + var propsToAnimate = [], prop, skipCallbacks; + for (prop in arguments[0]) { + propsToAnimate.push(prop); + } + for (var i = 0, len = propsToAnimate.length; i < len; i++) { + prop = propsToAnimate[i]; + skipCallbacks = i !== len - 1; + this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @param {String} property Property to animate + * @param {String} to Value to animate to + * @param {Object} [options] Options object + * @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked + */ + _animate: function(property, to, options, skipCallbacks) { + var _this = this, propPair; + + to = to.toString(); + + if (!options) { + options = { }; + } + else { + options = fabric.util.object.clone(options); + } + + if (~property.indexOf('.')) { + propPair = property.split('.'); + } + + var currentValue = propPair + ? this.get(propPair[0])[propPair[1]] + : this.get(property); + + if (!('from' in options)) { + options.from = currentValue; + } + + if (~to.indexOf('=')) { + to = currentValue + parseFloat(to.replace('=', '')); + } + else { + to = parseFloat(to); + } + + fabric.util.animate({ + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + abort: options.abort && function() { + return options.abort.call(_this); + }, + onChange: function(value, valueProgress, timeProgress) { + if (propPair) { + _this[propPair[0]][propPair[1]] = value; + } + else { + _this.set(property, value); + } + if (skipCallbacks) { + return; + } + options.onChange && options.onChange(value, valueProgress, timeProgress); + }, + onComplete: function(value, valueProgress, timeProgress) { + if (skipCallbacks) { + return; + } + + _this.setCoords(); + options.onComplete && options.onComplete(value, valueProgress, timeProgress); + } + }); + } +}); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }, + supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + /** + * Line class + * @class fabric.Line + * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'line', + + /** + * x value or first line edge + * @type Number + * @default + */ + x1: 0, + + /** + * y value or first line edge + * @type Number + * @default + */ + y1: 0, + + /** + * x value or second line edge + * @type Number + * @default + */ + x2: 0, + + /** + * y value or second line edge + * @type Number + * @default + */ + y2: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('x1', 'x2', 'y1', 'y2'), + + /** + * Constructor + * @param {Array} [points] Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} thisArg + */ + initialize: function(points, options) { + if (!points) { + points = [0, 0, 0, 0]; + } + + this.callSuper('initialize', options); + + this.set('x1', points[0]); + this.set('y1', points[1]); + this.set('x2', points[2]); + this.set('y2', points[3]); + + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Object} [options] Options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.width = Math.abs(this.x2 - this.x1); + this.height = Math.abs(this.y2 - this.y1); + + this.left = 'left' in options + ? options.left + : this._getLeftToOriginX(); + + this.top = 'top' in options + ? options.top + : this._getTopToOriginY(); + }, + + /** + * @private + * @param {String} key + * @param {*} value + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + if (typeof coordProps[key] !== 'undefined') { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. + */ + _getLeftToOriginX: makeEdgeToOriginGetter( + { // property names + origin: 'originX', + axis1: 'x1', + axis2: 'x2', + dimension: 'width' + }, + { // possible values of origin + nearest: 'left', + center: 'center', + farthest: 'right' + } + ), + + /** + * @private + * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. + */ + _getTopToOriginY: makeEdgeToOriginGetter( + { // property names + origin: 'originY', + axis1: 'y1', + axis2: 'y2', + dimension: 'height' + }, + { // possible values of origin + nearest: 'top', + center: 'center', + farthest: 'bottom' + } + ), + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + + if (!this.strokeDashArray || this.strokeDashArray && supportsLineDash) { + // move from center (of virtual box) to its left/top corner + // we can't assume x1, y1 is top left and x2, y2 is bottom right + var p = this.calcLinePoints(); + ctx.moveTo(p.x1, p.y1); + ctx.lineTo(p.x2, p.y2); + } + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = this.stroke || ctx.fillStyle; + this.stroke && this._renderStroke(ctx); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var p = this.calcLinePoints(); + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, p.x1, p.y1, p.x2, p.y2, this.strokeDashArray); + ctx.closePath(); + }, + + /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates + * @private + * @return {Object} center point from element coordinates + */ + _findCenterFromElement: function() { + return { + x: (this.x1 + this.x2) / 2, + y: (this.y1 + this.y2) / 2, + }; + }, + + /** + * Returns object representation of an instance + * @methd 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 extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); + }, + + /* + * Calculate object dimensions from its properties + * @private + */ + _getNonTransformedDimensions: function() { + var dim = this.callSuper('_getNonTransformedDimensions'); + if (this.strokeLineCap === 'butt') { + if (this.width === 0) { + dim.y -= this.strokeWidth; + } + if (this.height === 0) { + dim.x -= this.strokeWidth; + } + } + return dim; + }, + + /** + * Recalculates line points given width and height + * @private + */ + calcLinePoints: function() { + var xMult = this.x1 <= this.x2 ? -1 : 1, + yMult = this.y1 <= this.y2 ? -1 : 1, + x1 = (xMult * this.width * 0.5), + y1 = (yMult * this.height * 0.5), + x2 = (xMult * this.width * -0.5), + y2 = (yMult * this.height * -0.5); + + return { + x1: x1, + x2: x2, + y1: y1, + y2: y2 + }; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var p = this.calcLinePoints(); + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) + * @static + * @memberOf fabric.Line + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @memberOf fabric.Line + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} [callback] callback function invoked after parsing + */ + fabric.Line.fromElement = function(element, callback, options) { + options = options || { }; + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES), + points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + callback(new fabric.Line(points, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Line instance from an object representation + * @static + * @memberOf fabric.Line + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Line.fromObject = function(object, callback) { + function _callback(instance) { + delete instance.points; + callback && callback(instance); + }; + var options = clone(object, true); + options.points = [object.x1, object.y1, object.x2, object.y2]; + fabric.Object._fromObject('Line', options, _callback, 'points'); + }; + + /** + * Produces a function that calculates distance from canvas edge to Line origin. + */ + function makeEdgeToOriginGetter(propertyNames, originValues) { + var origin = propertyNames.origin, + axis1 = propertyNames.axis1, + axis2 = propertyNames.axis2, + dimension = propertyNames.dimension, + nearest = originValues.nearest, + center = originValues.center, + farthest = originValues.farthest; + + return function() { + switch (this.get(origin)) { + case nearest: + return Math.min(this.get(axis1), this.get(axis2)); + case center: + return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); + case farthest: + return Math.max(this.get(axis1), this.get(axis2)); + } + }; + + } + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + pi = Math.PI; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * Circle class + * @class fabric.Circle + * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'circle', + + /** + * Radius of this circle + * @type Number + * @default + */ + radius: 0, + + /** + * Start angle of the circle, moving clockwise + * deprectated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 0 + */ + startAngle: 0, + + /** + * End angle of the circle + * deprectated type, this should be in degree, this was an oversight. + * probably will change to degrees in next major version + * @type Number + * @default 2Pi + */ + endAngle: pi * 2, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('radius', 'startAngle', 'endAngle'), + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Circle} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + + if (key === 'radius') { + this.setRadius(value); + } + + return this; + }, + + /** + * 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) { + return this.callSuper('toObject', ['radius', 'startAngle', 'endAngle'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString, x = 0, y = 0, + angle = (this.endAngle - this.startAngle) % ( 2 * pi); + + if (angle === 0) { + svgString = [ + '\n' + ]; + } + else { + var startX = fabric.util.cos(this.startAngle) * this.radius, + startY = fabric.util.sin(this.startAngle) * this.radius, + endX = fabric.util.cos(this.endAngle) * this.radius, + endY = fabric.util.sin(this.endAngle) * this.radius, + largeFlag = angle > pi ? '1' : '0'; + svgString = [ + '\n' + ]; + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.arc( + 0, + 0, + this.radius, + this.startAngle, + this.endAngle, false); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @return {fabric.Circle} thisArg + */ + setRadius: function(value) { + this.radius = value; + return this.set('width', value * 2).set('height', value * 2); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @memberOf fabric.Circle + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @static + * @memberOf fabric.Circle + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @param {Object} [options] Options object + * @throws {Error} If value of `r` attribute is missing or invalid + */ + fabric.Circle.fromElement = function(element, callback) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); + + if (!isValidRadius(parsedAttributes)) { + throw new Error('value of `r` attribute is required and can not be negative'); + } + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.radius; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.radius; + callback(new fabric.Circle(parsedAttributes)); + }; + + /** + * @private + */ + function isValidRadius(attributes) { + return (('radius' in attributes) && (attributes.radius >= 0)); + } + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @static + * @memberOf fabric.Circle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {Object} Instance of fabric.Circle + */ + fabric.Circle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Circle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Triangle) { + fabric.warn('fabric.Triangle is already defined'); + return; + } + + /** + * Triangle class + * @class fabric.Triangle + * @extends fabric.Object + * @return {fabric.Triangle} thisArg + * @see {@link fabric.Triangle#initialize} for constructor definition + */ + fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'triangle', + + /** + * Width is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + width: 100, + + /** + * Height is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default + */ + height: 100, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + ctx.moveTo(-widthBy2, heightBy2); + ctx.lineTo(0, -heightBy2); + ctx.lineTo(widthBy2, heightBy2); + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, -widthBy2, heightBy2, 0, -heightBy2, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, 0, -heightBy2, widthBy2, heightBy2, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, widthBy2, heightBy2, -widthBy2, heightBy2, this.strokeDashArray); + ctx.closePath(); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2, + points = [ + -widthBy2 + ' ' + heightBy2, + '0 ' + -heightBy2, + widthBy2 + ' ' + heightBy2 + ].join(','); + return [ + '' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Triangle} instance from an object representation + * @static + * @memberOf fabric.Triangle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + */ + fabric.Triangle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Triangle', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2; + + if (fabric.Ellipse) { + fabric.warn('fabric.Ellipse is already defined.'); + return; + } + + /** + * Ellipse class + * @class fabric.Ellipse + * @extends fabric.Object + * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition + */ + fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'ellipse', + + /** + * Horizontal radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Ellipse} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this.set('rx', options && options.rx || 0); + this.set('ry', options && options.ry || 0); + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Ellipse} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + switch (key) { + + case 'rx': + this.rx = value; + this.set('width', value * 2); + break; + + case 'ry': + this.ry = value; + this.set('height', value * 2); + break; + + } + return this; + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRx: function() { + return this.get('rx') * this.get('scaleX'); + }, + + /** + * Returns Vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRy: function() { + return this.get('ry') * this.get('scaleY'); + }, + + /** + * 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) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + ctx.save(); + ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0); + ctx.arc( + 0, + 0, + this.rx, + 0, + piBy2, + false); + ctx.restore(); + this._renderPaintInOrder(ctx); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @memberOf fabric.Ellipse + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' ')); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @memberOf fabric.Ellipse + * @param {SVGElement} element Element to parse + * @param {Function} [callback] Options callback invoked after parsing is finished + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, callback) { + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.rx; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.ry; + callback(new fabric.Ellipse(parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Ellipse} instance from an object representation + * @static + * @memberOf fabric.Ellipse + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromObject = function(object, callback) { + return fabric.Object._fromObject('Ellipse', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.Rect) { + fabric.warn('fabric.Rect is already defined'); + return; + } + + /** + * Rectangle class + * @class fabric.Rect + * @extends fabric.Object + * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition + */ + fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { + + /** + * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('rx', 'ry'), + + /** + * Type of an object + * @type String + * @default + */ + type: 'rect', + + /** + * Horizontal border radius + * @type Number + * @default + */ + rx: 0, + + /** + * Vertical border radius + * @type Number + * @default + */ + ry: 0, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), + + /** + * Constructor + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * Initializes rx/ry attributes + * @private + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + + // 1x1 case (used in spray brush) optimization was removed because + // with caching and higher zoom level this makes more damage than help + + var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, + ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, + w = this.width, + h = this.height, + x = -this.width / 2, + y = -this.height / 2, + isRounded = rx !== 0 || ry !== 0, + /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ + k = 1 - 0.5522847498; + ctx.beginPath(); + + ctx.moveTo(x + rx, y); + + ctx.lineTo(x + w - rx, y); + isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); + + ctx.lineTo(x + w, y + h - ry); + isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h); + + ctx.lineTo(x + rx, y + h); + isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); + + ctx.lineTo(x, y + ry); + isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); + + ctx.closePath(); + + this._renderPaintInOrder(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var x = -this.width / 2, + y = -this.height / 2, + w = this.width, + h = this.height; + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y, x + w, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y + h, x, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x, y + h, x, y, this.strokeDashArray); + ctx.closePath(); + }, + + /** + * 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) { + return this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var x = -this.width / 2, y = -this.height / 2; + return [ + '\n' + ]; + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + * @memberOf fabric.Rect + * @see: http://www.w3.org/TR/SVG/shapes.html#RectElement + */ + fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' ')); + + /** + * Returns {@link fabric.Rect} instance from an SVG element + * @static + * @memberOf fabric.Rect + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Rect.fromElement = function(element, callback, options) { + if (!element) { + return callback(null); + } + options = options || { }; + + var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); + + parsedAttributes.left = parsedAttributes.left || 0; + parsedAttributes.top = parsedAttributes.top || 0; + parsedAttributes.height = parsedAttributes.height || 0; + parsedAttributes.width = parsedAttributes.width || 0; + var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect.visible = rect.visible && rect.width > 0 && rect.height > 0; + callback(rect); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Rect} instance from an object representation + * @static + * @memberOf fabric.Rect + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + */ + fabric.Rect.fromObject = function(object, callback) { + return fabric.Object._fromObject('Rect', object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * Polyline class + * @class fabric.Polyline + * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polyline', + + /** + * Points array + * @type Array + * @default + */ + points: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('points'), + + /** + * Constructor + * @param {Array} points Array of points (where each point is an object with x and y) + * @param {Object} [options] Options object + * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); + */ + initialize: function(points, options) { + options = options || {}; + this.points = points || []; + this.callSuper('initialize', options); + this._setPositionDimensions(options); + }, + + _setPositionDimensions: function(options) { + var calcDim = this._calcDimensions(options), correctLeftTop; + this.width = calcDim.width; + this.height = calcDim.height; + if (!options.fromSVG) { + correctLeftTop = this.translateToGivenOrigin( + { x: calcDim.left - this.strokeWidth / 2, y: calcDim.top - this.strokeWidth / 2 }, + 'left', + 'top', + this.originX, + this.originY + ); + } + if (typeof options.left === 'undefined') { + this.left = options.fromSVG ? calcDim.left : correctLeftTop.x; + } + if (typeof options.top === 'undefined') { + this.top = options.fromSVG ? calcDim.top : correctLeftTop.y; + } + this.pathOffset = { + x: calcDim.left + this.width / 2, + y: calcDim.top + this.height / 2 + }; + }, + + /** + * Calculate the polygon min and max point from points array, + * returning an object with left, top, widht, height to measure the + * polygon size + * @return {Object} object.left X coordinate of the polygon leftmost point + * @return {Object} object.top Y coordinate of the polygon topmost point + * @return {Object} object.width distance between X coordinates of the polygon leftmost and rightmost point + * @return {Object} object.height distance between Y coordinates of the polygon topmost and bottommost point + * @private + */ + _calcDimensions: function() { + + var points = this.points, + minX = min(points, 'x') || 0, + minY = min(points, 'y') || 0, + maxX = max(points, 'x') || 0, + maxY = max(points, 'y') || 0, + width = (maxX - minX), + height = (maxY - minY); + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * 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) { + return extend(this.callSuper('toObject', propertiesToInclude), { + points: this.points.concat() + }); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var points = [], diffX = this.pathOffset.x, diffY = this.pathOffset.y, + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push( + toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', + toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ' + ); + } + return [ + '<' + this.type + ' ', 'COMMON_PARTS', + 'points="', points.join(''), + '" />\n' + ]; + }, + /* _TO_SVG_END_ */ + + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + commonRender: function(ctx) { + var point, len = this.points.length, + x = this.pathOffset.x, + y = this.pathOffset.y; + + if (!len || isNaN(this.points[len - 1].y)) { + // do not draw if no points or odd points + // NaN comes from parseFloat of a empty string in parser + return false; + } + ctx.beginPath(); + ctx.moveTo(this.points[0].x - x, this.points[0].y - y); + for (var i = 0; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x - x, point.y - y); + } + return true; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + this._renderPaintInOrder(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var p1, p2; + + ctx.beginPath(); + for (var i = 0, len = this.points.length; i < len; i++) { + p1 = this.points[i]; + p2 = this.points[i + 1] || p1; + fabric.util.drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, this.strokeDashArray); + } + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.get('points').length; + } + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) + * @static + * @memberOf fabric.Polyline + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @memberOf fabric.Polyline + * @param {SVGElement} element Element to parser + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polyline.fromElementGenerator = function(_class) { + return function(element, callback, options) { + if (!element) { + return callback(null); + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric[_class].ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric[_class](points, extend(parsedAttributes, options))); + }; + }; + + fabric.Polyline.fromElement = fabric.Polyline.fromElementGenerator('Polyline'); + + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @memberOf fabric.Polyline + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Polyline.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polyline', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * Polygon class + * @class fabric.Polygon + * @extends fabric.Polyline + * @see {@link fabric.Polygon#initialize} for constructor definition + */ + fabric.Polygon = fabric.util.createClass(fabric.Polyline, /** @lends fabric.Polygon.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polygon', + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } + ctx.closePath(); + this._renderPaintInOrder(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + this.callSuper('_renderDashedStroke', ctx); + ctx.closePath(); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @memberOf fabric.Polygon + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); + + /** + * Returns {@link fabric.Polygon} instance from an SVG element + * @static + * @memberOf fabric.Polygon + * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing + * @param {Object} [options] Options object + */ + fabric.Polygon.fromElement = fabric.Polyline.fromElementGenerator('Polygon'); + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @memberOf fabric.Polygon + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Polygon.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polygon', object, callback, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max, + extend = fabric.util.object.extend, + _toString = Object.prototype.toString, + drawArc = fabric.util.drawArc, + toFixed = fabric.util.toFixed, + commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }, + repeatedCommands = { + m: 'l', + M: 'L' + }; + + if (fabric.Path) { + fabric.warn('fabric.Path is already defined'); + return; + } + + /** + * Path class + * @class fabric.Path + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition + */ + fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'path', + + /** + * Array of path points + * @type Array + * @default + */ + path: null, + + cacheProperties: fabric.Object.prototype.cacheProperties.concat('path', 'fillRule'), + + stateProperties: fabric.Object.prototype.stateProperties.concat('path'), + + /** + * Constructor + * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) + * @param {Object} [options] Options object + * @return {fabric.Path} thisArg + */ + initialize: function(path, options) { + options = options || { }; + this.callSuper('initialize', options); + + if (!path) { + path = []; + } + + var fromArray = _toString.call(path) === '[object Array]'; + + this.path = fromArray + ? path + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + : path.match && path.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + if (!this.path) { + return; + } + + if (!fromArray) { + this.path = this._parsePath(); + } + + fabric.Polyline.prototype._setPositionDimensions.call(this, options); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _renderPathCommands: function(ctx) { + var current, // current instruction + previous = null, + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + tempX, + tempY, + l = -this.pathOffset.x, + t = -this.pathOffset.y; + + ctx.beginPath(); + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'l': // lineto, relative + x += current[1]; + y += current[2]; + ctx.lineTo(x + l, y + t); + break; + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + ctx.lineTo(x + l, y + t); + break; + + case 'h': // horizontal lineto, relative + x += current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'H': // horizontal lineto, absolute + x = current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'v': // vertical lineto, relative + y += current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'V': // verical lineto, absolute + y = current[1]; + ctx.lineTo(x + l, y + t); + break; + + case 'm': // moveTo, relative + x += current[1]; + y += current[2]; + subpathStartX = x; + subpathStartY = y; + ctx.moveTo(x + l, y + t); + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + ctx.moveTo(x + l, y + t); + break; + + case 'c': // bezierCurveTo, relative + tempX = x + current[5]; + tempY = y + current[6]; + controlX = x + current[3]; + controlY = y + current[4]; + ctx.bezierCurveTo( + x + current[1] + l, // x1 + y + current[2] + t, // y1 + controlX + l, // x2 + controlY + t, // y2 + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'C': // bezierCurveTo, absolute + x = current[5]; + y = current[6]; + controlX = current[3]; + controlY = current[4]; + ctx.bezierCurveTo( + current[1] + l, + current[2] + t, + controlX + l, + controlY + t, + x + l, + y + t + ); + break; + + case 's': // shorthand cubic bezierCurveTo, relative + + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + ctx.bezierCurveTo( + controlX + l, + controlY + t, + x + current[1] + l, + y + current[2] + t, + tempX + l, + tempY + t + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = x + current[1]; + controlY = y + current[2]; + + x = tempX; + y = tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + ctx.bezierCurveTo( + controlX + l, + controlY + t, + current[1] + l, + current[2] + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = current[1]; + controlY = current[2]; + + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + controlX = x + current[1]; + controlY = y + current[2]; + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + + ctx.quadraticCurveTo( + current[1] + l, + current[2] + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + controlX = current[1]; + controlY = current[2]; + break; + + case 't': // shorthand quadraticCurveTo, relative + + // transform to absolute x,y + tempX = x + current[1]; + tempY = y + current[2]; + + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + + break; + + case 'T': + tempX = current[1]; + tempY = current[2]; + + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'a': + // TODO: optimize this + drawArc(ctx, x + l, y + t, [ + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + x + l, + current[7] + y + t + ]); + x += current[6]; + y += current[7]; + break; + + case 'A': + // TODO: optimize this + drawArc(ctx, x + l, y + t, [ + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + l, + current[7] + t + ]); + x = current[6]; + y = current[7]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + ctx.closePath(); + break; + } + previous = current; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _render: function(ctx) { + this._renderPathCommands(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Returns string representation of an instance + * @return {String} string representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * 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) { + return extend(this.callSuper('toObject', propertiesToInclude), { + path: this.path.map(function(item) { return item.slice(); }), + }); + }, + + /** + * Returns dataless 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 + */ + toDatalessObject: function(propertiesToInclude) { + var o = this.toObject(['sourcePath'].concat(propertiesToInclude)); + if (o.sourcePath) { + delete o.path; + } + return o; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var path = this.path.map(function(path) { + return path.join(' '); + }).join(' '); + return [ + '\n' + ]; + }, + + _getOffsetTransform: function() { + var digits = fabric.Object.NUM_FRACTION_DIGITS; + return ' translate(' + toFixed(-this.pathOffset.x, digits) + ', ' + + toFixed(-this.pathOffset.y, digits) + ')'; + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return '\t' + this._createBaseClipPathSVGMarkup( + this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform } + ); + }, + + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var additionalTransform = this._getOffsetTransform(); + return this._createBaseSVGMarkup(this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform }); + }, + /* _TO_SVG_END_ */ + + /** + * Returns number representation of an instance complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.path.length; + }, + + /** + * @private + */ + _parsePath: function() { + var result = [], + coords = [], + currentPath, + parsed, + re = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/ig, + match, + coordsStr; + + for (var i = 0, coordsParsed, len = this.path.length; i < len; i++) { + currentPath = this.path[i]; + + coordsStr = currentPath.slice(1).trim(); + coords.length = 0; + + while ((match = re.exec(coordsStr))) { + coords.push(match[0]); + } + + coordsParsed = [currentPath.charAt(0)]; + + for (var j = 0, jlen = coords.length; j < jlen; j++) { + parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + coordsParsed.push(parsed); + } + } + + var command = coordsParsed[0], + commandLength = commandLengths[command.toLowerCase()], + repeatedCommand = repeatedCommands[command] || command; + + if (coordsParsed.length - 1 > commandLength) { + for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { + result.push([command].concat(coordsParsed.slice(k, k + commandLength))); + command = repeatedCommand; + } + } + else { + result.push(coordsParsed); + } + } + + return result; + }, + + /** + * @private + */ + _calcDimensions: function() { + + var aX = [], + aY = [], + current, // current instruction + previous = null, + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + tempX, + tempY, + bounds; + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'l': // lineto, relative + x += current[1]; + y += current[2]; + bounds = []; + break; + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + bounds = []; + break; + + case 'h': // horizontal lineto, relative + x += current[1]; + bounds = []; + break; + + case 'H': // horizontal lineto, absolute + x = current[1]; + bounds = []; + break; + + case 'v': // vertical lineto, relative + y += current[1]; + bounds = []; + break; + + case 'V': // verical lineto, absolute + y = current[1]; + bounds = []; + break; + + case 'm': // moveTo, relative + x += current[1]; + y += current[2]; + subpathStartX = x; + subpathStartY = y; + bounds = []; + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + bounds = []; + break; + + case 'c': // bezierCurveTo, relative + tempX = x + current[5]; + tempY = y + current[6]; + controlX = x + current[3]; + controlY = y + current[4]; + bounds = fabric.util.getBoundsOfCurve(x, y, + x + current[1], // x1 + y + current[2], // y1 + controlX, // x2 + controlY, // y2 + tempX, + tempY + ); + x = tempX; + y = tempY; + break; + + case 'C': // bezierCurveTo, absolute + controlX = current[3]; + controlY = current[4]; + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + controlX, + controlY, + current[5], + current[6] + ); + x = current[5]; + y = current[6]; + break; + + case 's': // shorthand cubic bezierCurveTo, relative + + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + x + current[1], + y + current[2], + tempX, + tempY + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = x + current[1]; + controlY = y + current[2]; + x = tempX; + y = tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + current[1], + current[2], + tempX, + tempY + ); + x = tempX; + y = tempY; + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = current[1]; + controlY = current[2]; + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + controlX = x + current[1]; + controlY = y + current[2]; + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + tempX, + tempY + ); + x = tempX; + y = tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + controlX = current[1]; + controlY = current[2]; + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + current[3], + current[4] + ); + x = current[3]; + y = current[4]; + break; + + case 't': // shorthand quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[1]; + tempY = y + current[2]; + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + tempX, + tempY + ); + x = tempX; + y = tempY; + + break; + + case 'T': + tempX = current[1]; + tempY = current[2]; + + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + tempX, + tempY + ); + x = tempX; + y = tempY; + break; + + case 'a': + // TODO: optimize this + bounds = fabric.util.getBoundsOfArc(x, y, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + x, + current[7] + y + ); + x += current[6]; + y += current[7]; + break; + + case 'A': + // TODO: optimize this + bounds = fabric.util.getBoundsOfArc(x, y, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6], + current[7] + ); + x = current[6]; + y = current[7]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + break; + } + previous = current; + bounds.forEach(function (point) { + aX.push(point.x); + aY.push(point.y); + }); + aX.push(x); + aY.push(y); + } + + var minX = min(aX) || 0, + minY = min(aY) || 0, + maxX = max(aX) || 0, + maxY = max(aY) || 0, + deltaX = maxX - minX, + deltaY = maxY - minY; + + return { + left: minX, + top: minY, + width: deltaX, + height: deltaY + }; + } + }); + + /** + * Creates an instance of fabric.Path from an object + * @static + * @memberOf fabric.Path + * @param {Object} object + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + */ + fabric.Path.fromObject = function(object, callback) { + if (typeof object.sourcePath === 'string') { + var pathUrl = object.sourcePath; + fabric.loadSVGFromURL(pathUrl, function (elements) { + var path = elements[0]; + path.setOptions(object); + callback && callback(path); + }); + } + else { + fabric.Object._fromObject('Path', object, callback, 'path'); + } + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @memberOf fabric.Path + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); + + /** + * Creates an instance of fabric.Path from an SVG element + * @static + * @memberOf fabric.Path + * @param {SVGElement} element to parse + * @param {Function} callback Callback to invoke when an fabric.Path instance is created + * @param {Object} [options] Options object + * @param {Function} [callback] Options callback invoked after parsing is finished + */ + fabric.Path.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); + parsedAttributes.fromSVG = true; + callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + min = fabric.util.array.min, + max = fabric.util.array.max; + + if (fabric.Group) { + return; + } + + /** + * Group class + * @class fabric.Group + * @extends fabric.Object + * @mixes fabric.Collection + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.Group#initialize} for constructor definition + */ + fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'group', + + /** + * Width of stroke + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * Indicates if click events should also check for subtargets + * @type Boolean + * @default + */ + subTargetCheck: false, + + /** + * Groups are container, do not render anything on theyr own, ence no cache properties + * @type Array + * @default + */ + cacheProperties: [], + + /** + * setOnGroup is a method used for TextBox that is no more used since 2.0.0 The behavior is still + * available setting this boolean to true. + * @type Boolean + * @since 2.0.0 + * @default + */ + useSetOnGroup: false, + + /** + * Constructor + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. + * @return {Object} thisArg + */ + initialize: function(objects, options, isAlreadyGrouped) { + options = options || {}; + this._objects = []; + // if objects enclosed in a group have been grouped already, + // we cannot change properties of objects. + // Thus we need to set options to group without objects, + isAlreadyGrouped && this.callSuper('initialize', options); + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (!isAlreadyGrouped) { + var center = options && options.centerPoint; + // we want to set origins before calculating the bounding box. + // so that the topleft can be set with that in mind. + // if specific top and left are passed, are overwritten later + // with the callSuper('initialize', options) + if (options.originX !== undefined) { + this.originX = options.originX; + } + if (options.originY !== undefined) { + this.originY = options.originY; + } + // if coming from svg i do not want to calc bounds. + // i assume width and height are passed along options + center || this._calcBounds(); + this._updateObjectsCoords(center); + delete options.centerPoint; + this.callSuper('initialize', options); + } + else { + this._updateObjectsACoords(); + } + + this.setCoords(); + }, + + /** + * @private + * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + */ + _updateObjectsACoords: function() { + var ignoreZoom = true, skipAbsolute = true; + for (var i = this._objects.length; i--; ){ + this._objects[i].setCoords(ignoreZoom, skipAbsolute); + } + }, + + /** + * @private + * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + */ + _updateObjectsCoords: function(center) { + var center = center || this.getCenterPoint(); + for (var i = this._objects.length; i--; ){ + this._updateObjectCoords(this._objects[i], center); + } + }, + + /** + * @private + * @param {Object} object + * @param {fabric.Point} center, current center of group. + */ + _updateObjectCoords: function(object, center) { + var objectLeft = object.left, + objectTop = object.top, + ignoreZoom = true, skipAbsolute = true; + + object.set({ + left: objectLeft - center.x, + top: objectTop - center.y + }); + object.group = this; + object.setCoords(ignoreZoom, skipAbsolute); + }, + + /** + * Returns string represenation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Adds an object to a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + addWithUpdate: function(object) { + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + if (object) { + this._objects.push(object); + object.group = this; + object._set('canvas', this.canvas); + } + this._calcBounds(); + this._updateObjectsCoords(); + this.setCoords(); + this.dirty = true; + return this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + + this.remove(object); + this._calcBounds(); + this._updateObjectsCoords(); + this.setCoords(); + this.dirty = true; + return this; + }, + + /** + * @private + */ + _onObjectAdded: function(object) { + this.dirty = true; + object.group = this; + object._set('canvas', this.canvas); + }, + + /** + * @private + */ + _onObjectRemoved: function(object) { + this.dirty = true; + delete object.group; + }, + + /** + * @private + */ + _set: function(key, value) { + var i = this._objects.length; + if (this.useSetOnGroup) { + while (i--) { + this._objects[i].setOnGroup(key, value); + } + } + if (key === 'canvas') { + while (i--) { + this._objects[i]._set(key, value); + } + } + fabric.Object.prototype._set.call(this, key, value); + }, + + /** + * 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 _includeDefaultValues = this.includeDefaultValues; + var objsToObject = this._objects.map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + var obj = fabric.Object.prototype.toObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Returns object representation of an instance, in dataless mode. + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var objsToObject, sourcePath = this.sourcePath; + if (sourcePath) { + objsToObject = sourcePath; + } + else { + var _includeDefaultValues = this.includeDefaultValues; + objsToObject = this._objects.map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = _includeDefaultValues; + var _obj = obj.toDatalessObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + } + var obj = fabric.Object.prototype.toDatalessObject.call(this, propertiesToInclude); + obj.objects = objsToObject; + return obj; + }, + + /** + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx) { + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group is already cached. + * @return {Boolean} + */ + shouldCache: function() { + var ownCache = fabric.Object.prototype.shouldCache.call(this); + if (ownCache) { + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + this.ownCaching = false; + return false; + } + } + } + return ownCache; + }, + + /** + * Check if this object or a child object will cast a shadow + * @return {Boolean} + */ + willDrawShadow: function() { + if (this.shadow) { + return fabric.Object.prototype.willDrawShadow.call(this); + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + return true; + } + } + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return this.ownCaching || (this.group && this.group.isOnACache()); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawObject: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].render(ctx); + } + this._drawClipPath(ctx); + }, + + /** + * Check if cache is dirty + */ + isCacheDirty: function(skipCanvas) { + if (this.callSuper('isCacheDirty', skipCanvas)) { + return true; + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].isCacheDirty(true)) { + if (this._cacheCanvas) { + // if this group has not a cache canvas there is nothing to clean + var x = this.cacheWidth / this.zoomX, y = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-x / 2, -y / 2, x, y); + } + return true; + } + } + return false; + }, + + /** + * Retores original state of each of group objects (original state is that which was before group was created). + * @private + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + this._objects.forEach(this._restoreObjectState, this); + return this; + }, + + /** + * Realises the transform from this group onto the supplied object + * i.e. it tells you what would happen if the supplied object was in + * the group, and then the group was destroyed. It mutates the supplied + * object. + * @param {fabric.Object} object + * @return {fabric.Object} transformedObject + */ + realizeTransform: function(object) { + var matrix = object.calcTransformMatrix(), + options = fabric.util.qrDecompose(matrix), + center = new fabric.Point(options.translateX, options.translateY); + object.flipX = false; + object.flipY = false; + object.set('scaleX', options.scaleX); + object.set('scaleY', options.scaleY); + object.skewX = options.skewX; + object.skewY = options.skewY; + object.angle = options.angle; + object.setPositionByOrigin(center, 'center', 'center'); + return object; + }, + + /** + * Restores original state of a specified object in group + * @private + * @param {fabric.Object} object + * @return {fabric.Group} thisArg + */ + _restoreObjectState: function(object) { + this.realizeTransform(object); + object.setCoords(); + delete object.group; + return this; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + destroy: function() { + // when group is destroyed objects needs to get a repaint to be eventually + // displayed on canvas. + this._objects.forEach(function(object) { + object.set('dirty', true); + }); + return this._restoreObjectsState(); + }, + + /** + * make a group an active selection, remove the group from canvas + * the group has to be on canvas for this to work. + * @return {fabric.ActiveSelection} thisArg + * @chainable + */ + toActiveSelection: function() { + if (!this.canvas) { + return; + } + var objects = this._objects, canvas = this.canvas; + this._objects = []; + var options = this.toObject(); + delete options.objects; + var activeSelection = new fabric.ActiveSelection([]); + activeSelection.set(options); + activeSelection.type = 'activeSelection'; + canvas.remove(this); + objects.forEach(function(object) { + object.group = activeSelection; + object.dirty = true; + canvas.add(object); + }); + activeSelection.canvas = canvas; + activeSelection._objects = objects; + canvas._activeObject = activeSelection; + activeSelection.setCoords(); + return activeSelection; + }, + + /** + * Destroys a group (restoring state of its objects) + * @return {fabric.Group} thisArg + * @chainable + */ + ungroupOnCanvas: function() { + return this._restoreObjectsState(); + }, + + /** + * Sets coordinates of all objects inside group + * @return {fabric.Group} thisArg + * @chainable + */ + setObjectsCoords: function() { + var ignoreZoom = true, skipAbsolute = true; + this.forEachObject(function(object) { + object.setCoords(ignoreZoom, skipAbsolute); + }); + return this; + }, + + /** + * @private + */ + _calcBounds: function(onlyWidthHeight) { + var aX = [], + aY = [], + o, prop, + props = ['tr', 'br', 'bl', 'tl'], + i = 0, iLen = this._objects.length, + j, jLen = props.length, + ignoreZoom = true; + + for ( ; i < iLen; ++i) { + o = this._objects[i]; + o.setCoords(ignoreZoom); + for (j = 0; j < jLen; j++) { + prop = props[j]; + aX.push(o.oCoords[prop].x); + aY.push(o.oCoords[prop].y); + } + } + + this._getBounds(aX, aY, onlyWidthHeight); + }, + + /** + * @private + */ + _getBounds: function(aX, aY, onlyWidthHeight) { + var minXY = new fabric.Point(min(aX), min(aY)), + maxXY = new fabric.Point(max(aX), max(aY)), + top = minXY.y || 0, left = minXY.x || 0, + width = (maxXY.x - minXY.x) || 0, + height = (maxXY.y - minXY.y) || 0; + this.width = width; + this.height = height; + if (!onlyWidthHeight) { + // the bounding box always finds the topleft most corner. + // whatever is the group origin, we set up here the left/top position. + this.setPositionByOrigin({ x: left, y: top }, 'left', 'top'); + } + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + _toSVG: function(reviver) { + var svgString = ['\n']; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t\t', this._objects[i].toSVG(reviver)); + } + svgString.push('\n'); + return svgString; + }, + + /** + * Returns styles-string for svg-export, specific version for group + * @return {String} + */ + getSvgStyles: function() { + var opacity = typeof this.opacity !== 'undefined' && this.opacity !== 1 ? + 'opacity: ' + this.opacity + ';' : '', + visibility = this.visible ? '' : ' visibility: hidden;'; + return [ + opacity, + this.getSvgFilter(), + visibility + ].join(''); + }, + + /** + * Returns svg clipPath representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toClipPathSVG: function(reviver) { + var svgString = []; + + for (var i = 0, len = this._objects.length; i < len; i++) { + svgString.push('\t', this._objects[i].toClipPathSVG(reviver)); + } + + return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Group} instance from an object representation + * @static + * @memberOf fabric.Group + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an group instance is created + */ + fabric.Group.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + fabric.util.enlivenObjects([object.clipPath], function(enlivedClipPath) { + var options = fabric.util.object.clone(object, true); + options.clipPath = enlivedClipPath[0]; + delete options.objects; + callback && callback(new fabric.Group(enlivenedObjects, options, true)); + }); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.ActiveSelection) { + return; + } + + /** + * Group class + * @class fabric.ActiveSelection + * @extends fabric.Group + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.ActiveSelection#initialize} for constructor definition + */ + fabric.ActiveSelection = fabric.util.createClass(fabric.Group, /** @lends fabric.ActiveSelection.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'activeSelection', + + /** + * Constructor + * @param {Object} objects ActiveSelection objects + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(objects, options) { + options = options || {}; + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (options.originX) { + this.originX = options.originX; + } + if (options.originY) { + this.originY = options.originY; + } + this._calcBounds(); + this._updateObjectsCoords(); + fabric.Object.prototype.initialize.call(this, options); + this.setCoords(); + }, + + /** + * Change te activeSelection to a normal group, + * High level function that automatically adds it to canvas as + * active object. no events fired. + * @since 2.0.0 + * @return {fabric.Group} + */ + toGroup: function() { + var objects = this._objects.concat(); + this._objects = []; + var options = fabric.Object.prototype.toObject.call(this); + var newGroup = new fabric.Group([]); + delete options.type; + newGroup.set(options); + objects.forEach(function(object) { + object.canvas.remove(object); + object.group = newGroup; + }); + newGroup._objects = objects; + if (!this.canvas) { + return newGroup; + } + var canvas = this.canvas; + canvas.add(newGroup); + canvas._activeObject = newGroup; + newGroup.setCoords(); + return newGroup; + }, + + /** + * If returns true, deselection is cancelled. + * @since 2.0.0 + * @return {Boolean} [cancel] + */ + onDeselect: function() { + this.destroy(); + return false; + }, + + /** + * Returns string representation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isOnACache: function() { + return false; + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style + * @param {Object} [childrenOverride] properties to override the children overrides + */ + _renderControls: function(ctx, styleOverride, childrenOverride) { + ctx.save(); + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + this.callSuper('_renderControls', ctx, styleOverride); + childrenOverride = childrenOverride || { }; + if (typeof childrenOverride.hasControls === 'undefined') { + childrenOverride.hasControls = false; + } + if (typeof childrenOverride.hasRotatingPoint === 'undefined') { + childrenOverride.hasRotatingPoint = false; + } + childrenOverride.forActiveSelection = true; + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i]._renderControls(ctx, childrenOverride); + } + ctx.restore(); + }, + }); + + /** + * Returns {@link fabric.ActiveSelection} instance from an object representation + * @static + * @memberOf fabric.ActiveSelection + * @param {Object} object Object to create a group from + * @param {Function} [callback] Callback to invoke when an ActiveSelection instance is created + */ + fabric.ActiveSelection.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.ActiveSelection(enlivenedObjects, object, true)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + } + + /** + * Image class + * @class fabric.Image + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} + * @see {@link fabric.Image#initialize} for constructor definition + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'image', + + /** + * crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + + /** + * Width of a stroke. + * For image quality a stroke multiple of 2 gives better results. + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * When calling {@link fabric.Image.getSrc}, return value from element src with `element.getAttribute('src')`. + * This allows for relative urls as image src. + * @since 2.7.0 + * @type Boolean + * @default + */ + srcFromAttribute: false, + + /** + * private + * contains last value of scaleX to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleX: 1, + + /** + * private + * contains last value of scaleY to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleY: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingX: 1, + + /** + * private + * contains last value of scaling applied by the apply filter chain + * @type Number + */ + _filterScalingY: 1, + + /** + * minimum scale factor under which any resizeFilter is triggered to resize the image + * 0 will disable the automatic resize. 1 will trigger automatically always. + * number bigger than 1 are not implemented yet. + * @type Number + */ + minimumScaleTrigger: 0.5, + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'), + + /** + * key used to retrieve the texture representing this image + * @since 2.0.0 + * @type String + * @default + */ + cacheKey: '', + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropX: 0, + + /** + * Image crop in pixels from original image size. + * @since 2.0.0 + * @type Number + * @default + */ + cropY: 0, + + /** + * Constructor + * @param {HTMLImageElement | String} element Image element + * @param {Object} [options] Options object + * @param {function} [callback] callback function to call after eventual filters applied. + * @return {fabric.Image} thisArg + */ + initialize: function(element, options) { + options || (options = { }); + this.filters = []; + this.cacheKey = 'texture' + fabric.Object.__uid++; + this.callSuper('initialize', options); + this._initElement(element, options); + }, + + /** + * Returns image element which this instance if based on + * @return {HTMLImageElement} Image element + */ + getElement: function() { + return this._element || {}; + }, + + /** + * Sets image element for this instance to a specified one. + * If filters defined they are applied to new image. + * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. + * @param {HTMLImageElement} element + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element, options) { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._element = element; + this._originalElement = element; + this._initConfig(options); + if (this.filters.length !== 0) { + this.applyFilters(); + } + // resizeFilters work on the already filtered copy. + // we need to apply resizeFilters AFTER normal filters. + // applyResizeFilters is run more often than normal fiters + // and is triggered by user interactions rather than dev code + if (this.resizeFilter) { + this.applyResizeFilters(); + } + return this; + }, + + /** + * Delete a single texture if in webgl mode + */ + removeTexture: function(key) { + var backend = fabric.filterBackend; + if (backend && backend.evictCachesForKey) { + backend.evictCachesForKey(key); + } + }, + + /** + * Delete textures, reference to elements and eventually JSDOM cleanup + */ + dispose: function() { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); + this._cacheContext = undefined; + ['_originalElement', '_element', '_filteredEl', '_cacheCanvas'].forEach((function(element) { + fabric.util.cleanUpJsdomNode(this[element]); + this[element] = undefined; + }).bind(this)); + }, + + /** + * Sets crossOrigin value (on an instance and corresponding image element) + * @return {fabric.Image} thisArg + * @chainable + */ + setCrossOrigin: function(value) { + this.crossOrigin = value; + this._element.crossOrigin = value; + + return this; + }, + + /** + * Returns original size of an image + * @return {Object} Object with "width" and "height" properties + */ + getOriginalSize: function() { + var element = this.getElement(); + return { + width: element.naturalWidth || element.width, + height: element.naturalHeight || element.height + }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _stroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + var w = this.width / 2, h = this.height / 2; + ctx.beginPath(); + ctx.moveTo(-w, -h); + ctx.lineTo(w, -h); + ctx.lineTo(w, h); + ctx.lineTo(-w, h); + ctx.lineTo(-w, -h); + ctx.closePath(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var x = -this.width / 2, + y = -this.height / 2, + w = this.width, + h = this.height; + + ctx.save(); + this._setStrokeStyles(ctx, this); + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y, x + w, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y + h, x, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x, y + h, x, y, this.strokeDashArray); + ctx.closePath(); + ctx.restore(); + }, + + /** + * 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 filters = []; + + this.filters.forEach(function(filterObj) { + if (filterObj) { + filters.push(filterObj.toObject()); + } + }); + var object = extend( + this.callSuper( + 'toObject', + ['crossOrigin', 'cropX', 'cropY'].concat(propertiesToInclude) + ), { + src: this.getSrc(), + filters: filters, + }); + if (this.resizeFilter) { + object.resizeFilter = this.resizeFilter.toObject(); + } + return object; + }, + + /** + * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,hight. + * @return {Boolean} + */ + hasCrop: function() { + return this.cropX || this.cropY || this.width < this._element.width || this.height < this._element.height; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance + */ + _toSVG: function() { + var svgString = [], imageMarkup = [], strokeSvg, + x = -this.width / 2, y = -this.height / 2, clipPath = ''; + if (this.hasCrop()) { + var clipPathId = fabric.Object.__uid++; + svgString.push( + '\n', + '\t\n', + '\n' + ); + clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; + } + imageMarkup.push('\t\n'); + + if (this.stroke || this.strokeDashArray) { + var origFill = this.fill; + this.fill = null; + strokeSvg = [ + '\t\n' + ]; + this.fill = origFill; + } + if (this.paintFirst !== 'fill') { + svgString = svgString.concat(strokeSvg, imageMarkup); + } + else { + svgString = svgString.concat(imageMarkup, strokeSvg); + } + return svgString; + }, + /* _TO_SVG_END_ */ + + /** + * Returns source of an image + * @param {Boolean} filtered indicates if the src is needed for svg + * @return {String} Source of an image + */ + getSrc: function(filtered) { + var element = filtered ? this._element : this._originalElement; + if (element) { + if (element.toDataURL) { + return element.toDataURL(); + } + + if (this.srcFromAttribute) { + return element.getAttribute('src'); + } + else { + return element.src; + } + } + else { + return this.src || ''; + } + }, + + /** + * Sets source of an image + * @param {String} src Source string (URL) + * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + * @chainable + */ + setSrc: function(src, callback, options) { + fabric.util.loadImage(src, function(img) { + this.setElement(img, options); + this._setWidthHeight(); + callback && callback(this); + }, this, options && options.crossOrigin); + return this; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + applyResizeFilters: function() { + var filter = this.resizeFilter, + minimumScale = this.minimumScaleTrigger, + objectScale = this.getTotalObjectScaling(), + scaleX = objectScale.scaleX, + scaleY = objectScale.scaleY, + elementToFilter = this._filteredEl || this._originalElement; + if (this.group) { + this.set('dirty', true); + } + if (!filter || (scaleX > minimumScale && scaleY > minimumScale)) { + this._element = elementToFilter; + this._filterScalingX = 1; + this._filterScalingY = 1; + this._lastScaleX = scaleX; + this._lastScaleY = scaleY; + return; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + var canvasEl = fabric.util.createCanvasElement(), + cacheKey = this._filteredEl ? (this.cacheKey + '_filtered') : this.cacheKey, + sourceWidth = elementToFilter.width, sourceHeight = elementToFilter.height; + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._lastScaleX = filter.scaleX = scaleX; + this._lastScaleY = filter.scaleY = scaleY; + fabric.filterBackend.applyFilters( + [filter], elementToFilter, sourceWidth, sourceHeight, this._element, cacheKey); + this._filterScalingX = canvasEl.width / this._originalElement.width; + this._filterScalingY = canvasEl.height / this._originalElement.height; + }, + + /** + * Applies filters assigned to this image (from "filters" array) or from filter param + * @method applyFilters + * @param {Array} filters to be applied + * @param {Boolean} forResizing specify if the filter operation is a resize operation + * @return {thisArg} return the fabric.Image object + * @chainable + */ + applyFilters: function(filters) { + + filters = filters || this.filters || []; + filters = filters.filter(function(filter) { return filter && !filter.isNeutralState(); }); + this.set('dirty', true); + + // needs to clear out or WEBGL will not resize correctly + this.removeTexture(this.cacheKey + '_filtered'); + + if (filters.length === 0) { + this._element = this._originalElement; + this._filteredEl = null; + this._filterScalingX = 1; + this._filterScalingY = 1; + return this; + } + + var imgElement = this._originalElement, + sourceWidth = imgElement.naturalWidth || imgElement.width, + sourceHeight = imgElement.naturalHeight || imgElement.height; + + if (this._element === this._originalElement) { + // if the element is the same we need to create a new element + var canvasEl = fabric.util.createCanvasElement(); + canvasEl.width = sourceWidth; + canvasEl.height = sourceHeight; + this._element = canvasEl; + this._filteredEl = canvasEl; + } + else { + // clear the existing element to get new filter data + // also dereference the eventual resized _element + this._element = this._filteredEl; + this._filteredEl.getContext('2d').clearRect(0, 0, sourceWidth, sourceHeight); + // we also need to resize again at next renderAll, so remove saved _lastScaleX/Y + this._lastScaleX = 1; + this._lastScaleY = 1; + } + if (!fabric.filterBackend) { + fabric.filterBackend = fabric.initFilterBackend(); + } + fabric.filterBackend.applyFilters( + filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey); + if (this._originalElement.width !== this._element.width || + this._originalElement.height !== this._element.height) { + this._filterScalingX = this._element.width / this._originalElement.width; + this._filterScalingY = this._element.height / this._originalElement.height; + } + return this; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { + this.applyResizeFilters(); + } + this._stroke(ctx); + this._renderPaintInOrder(ctx); + }, + + /** + * Decide if the object should cache or not. Create its own cache level + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * This is the special image version where we would like to avoid caching where possible. + * Essentially images do not benefit from caching. They may require caching, and in that + * case we do it. Also caching an image usually ends in a loss of details. + * A full performance audit should be done. + * @return {Boolean} + */ + shouldCache: function() { + return this.needsItsOwnCache(); + }, + + _renderFill: function(ctx) { + var elementToDraw = this._element, + w = this.width, h = this.height, + sW = Math.min(elementToDraw.naturalWidth || elementToDraw.width, w * this._filterScalingX), + sH = Math.min(elementToDraw.naturalHeight || elementToDraw.height, h * this._filterScalingY), + x = -w / 2, y = -h / 2, + sX = Math.max(0, this.cropX * this._filterScalingX), + sY = Math.max(0, this.cropY * this._filterScalingY); + + elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, w, h); + }, + + /** + * @private, needed to check if image needs resize + */ + _needsResize: function() { + var scale = this.getTotalObjectScaling(); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); + }, + + /** + * @private + */ + _resetWidthHeight: function() { + this.set(this.getOriginalSize()); + }, + + /** + * The Image class's initialization method. This method is automatically + * called by the constructor. + * @private + * @param {HTMLImageElement|String} element The element representing the image + * @param {Object} [options] Options object + */ + _initElement: function(element, options) { + this.setElement(fabric.util.getById(element), options); + fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initConfig: function(options) { + options || (options = { }); + this.setOptions(options); + this._setWidthHeight(options); + if (this._element && this.crossOrigin) { + this._element.crossOrigin = this.crossOrigin; + } + }, + + /** + * @private + * @param {Array} filters to be initialized + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(filters, callback) { + if (filters && filters.length) { + fabric.util.enlivenObjects(filters, function(enlivenedObjects) { + callback && callback(enlivenedObjects); + }, 'fabric.Image.filters'); + } + else { + callback && callback(); + } + }, + + /** + * @private + * Set the width and the height of the image object, using the element or the + * options. + * @param {Object} [options] Object with width/height properties + */ + _setWidthHeight: function(options) { + options || (options = { }); + var el = this.getElement(); + this.width = options.width || el.naturalWidth || el.width || 0; + this.height = options.height || el.naturalHeight || el.height || 0; + }, + + /** + * Calculate offset for center and scale factor for the image in order to respect + * the preserveAspectRatio attribute + * @private + * @return {Object} + */ + parsePreserveAspectRatioAttribute: function() { + var pAR = fabric.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio || ''), + rWidth = this._element.width, rHeight = this._element.height, + scaleX = 1, scaleY = 1, offsetLeft = 0, offsetTop = 0, cropX = 0, cropY = 0, + offset, pWidth = this.width, pHeight = this.height, parsedAttributes = { width: pWidth, height: pHeight }; + if (pAR && (pAR.alignX !== 'none' || pAR.alignY !== 'none')) { + if (pAR.meetOrSlice === 'meet') { + scaleX = scaleY = fabric.util.findScaleToFit(this._element, parsedAttributes); + offset = (pWidth - rWidth * scaleX) / 2; + if (pAR.alignX === 'Min') { + offsetLeft = -offset; + } + if (pAR.alignX === 'Max') { + offsetLeft = offset; + } + offset = (pHeight - rHeight * scaleY) / 2; + if (pAR.alignY === 'Min') { + offsetTop = -offset; + } + if (pAR.alignY === 'Max') { + offsetTop = offset; + } + } + if (pAR.meetOrSlice === 'slice') { + scaleX = scaleY = fabric.util.findScaleToCover(this._element, parsedAttributes); + offset = rWidth - pWidth / scaleX; + if (pAR.alignX === 'Mid') { + cropX = offset / 2; + } + if (pAR.alignX === 'Max') { + cropX = offset; + } + offset = rHeight - pHeight / scaleY; + if (pAR.alignY === 'Mid') { + cropY = offset / 2; + } + if (pAR.alignY === 'Max') { + cropY = offset; + } + rWidth = pWidth / scaleX; + rHeight = pHeight / scaleY; + } + } + else { + scaleX = pWidth / rWidth; + scaleY = pHeight / rHeight; + } + return { + width: rWidth, + height: rHeight, + scaleX: scaleX, + scaleY: scaleY, + offsetLeft: offsetLeft, + offsetTop: offsetTop, + cropX: cropX, + cropY: cropY + }; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + * @default + */ + fabric.Image.CSS_CANVAS = 'canvas-img'; + + /** + * Alias for getSrc + * @static + */ + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} callback Callback to invoke when an image instance is created + */ + fabric.Image.fromObject = function(_object, callback) { + var object = fabric.util.object.clone(_object); + fabric.util.loadImage(object.src, function(img, error) { + if (error) { + callback && callback(null, error); + return; + } + fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { + object.filters = filters || []; + fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { + object.resizeFilter = resizeFilters[0]; + fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) { + object.clipPath = enlivedProps[0]; + var image = new fabric.Image(img, object); + callback(image); + }); + }); + }); + }, null, object.crossOrigin); + }; + + /** + * Creates an instance of fabric.Image from an URL string + * @static + * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument) + * @param {Object} [imgOptions] Options object + */ + fabric.Image.fromURL = function(url, callback, imgOptions) { + fabric.util.loadImage(url, function(img) { + callback && callback(new fabric.Image(img, imgOptions)); + }, null, imgOptions && imgOptions.crossOrigin); + }; + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) + * @static + * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} + */ + fabric.Image.ATTRIBUTE_NAMES = + fabric.SHARED_ATTRIBUTES.concat('x y width height preserveAspectRatio xlink:href crossOrigin'.split(' ')); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @param {Function} callback Callback to execute when fabric.Image object is created + * @return {fabric.Image} Instance of fabric.Image + */ + fabric.Image.fromElement = function(element, callback, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, + extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + }; + /* _FROM_SVG_END_ */ + +})(typeof exports !== 'undefined' ? exports : this); +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * @private + * @return {Number} angle value + */ + _getAngleValueForStraighten: function() { + var angle = this.angle % 360; + if (angle > 0) { + return Math.round((angle - 1) / 90) * 90; + } + return Math.round(angle / 90) * 90; + }, + + /** + * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer) + * @return {fabric.Object} thisArg + * @chainable + */ + straighten: function() { + this.rotate(this._getAngleValueForStraighten()); + return this; + }, + + /** + * Same as {@link fabric.Object.prototype.straighten} but with animation + * @param {Object} callbacks Object with callback functions + * @param {Function} [callbacks.onComplete] Invoked on completion + * @param {Function} [callbacks.onChange] Invoked on every step of animation + * @return {fabric.Object} thisArg + * @chainable + */ + fxStraighten: function(callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: this.get('angle'), + endValue: this._getAngleValueForStraighten(), + duration: this.FX_DURATION, + onChange: function(value) { + _this.rotate(value); + onChange(); + }, + onComplete: function() { + _this.setCoords(); + onComplete(); + }, + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Straightens object, then rerenders canvas + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.requestRenderAll(); + return this; + }, + + /** + * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.requestRenderAllBound + }); + return this; + } +}); +(function() { + + 'use strict'; + + /** + * Tests if webgl supports certain precision + * @param {WebGL} Canvas WebGL context to test on + * @param {String} Precision to test can be any of following: 'lowp', 'mediump', 'highp' + * @returns {Boolean} Whether the user's browser WebGL supports given precision. + */ + function testPrecision(gl, precision){ + var fragmentSource = 'precision ' + precision + ' float;\nvoid main(){}'; + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + return false; + } + return true; + } + + /** + * Indicate whether this filtering backend is supported by the user's browser. + * @param {Number} tileSize check if the tileSize is supported + * @returns {Boolean} Whether the user's browser supports WebGL. + */ + fabric.isWebglSupported = function(tileSize) { + if (fabric.isLikelyNode) { + return false; + } + tileSize = tileSize || fabric.WebglFilterBackend.prototype.tileSize; + var canvas = document.createElement('canvas'); + var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + var isSupported = false; + // eslint-disable-next-line + if (gl) { + fabric.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); + isSupported = fabric.maxTextureSize >= tileSize; + var precisions = ['highp', 'mediump', 'lowp']; + for (var i = 0; i < 3; i++){ + if (testPrecision(gl, precisions[i])){ + fabric.webGlPrecision = precisions[i]; + break; + }; + } + } + this.isSupported = isSupported; + return isSupported; + }; + + fabric.WebglFilterBackend = WebglFilterBackend; + + /** + * WebGL filter backend. + */ + function WebglFilterBackend(options) { + if (options && options.tileSize) { + this.tileSize = options.tileSize; + } + this.setupGLContext(this.tileSize, this.tileSize); + this.captureGPUInfo(); + }; + + WebglFilterBackend.prototype = /** @lends fabric.WebglFilterBackend.prototype */ { + + tileSize: 2048, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you problably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Setup a WebGL context suitable for filtering, and bind any needed event handlers. + */ + setupGLContext: function(width, height) { + this.dispose(); + this.createWebGLCanvas(width, height); + // eslint-disable-next-line + this.aPosition = new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]); + this.chooseFastestCopyGLTo2DMethod(width, height); + }, + + /** + * Pick a method to copy data from GL context to 2d canvas. In some browsers using + * putImageData is faster than drawImage for that specific operation. + */ + chooseFastestCopyGLTo2DMethod: function(width, height) { + var canMeasurePerf = typeof window.performance !== 'undefined', canUseImageData; + try { + new ImageData(1, 1); + canUseImageData = true; + } + catch (e) { + canUseImageData = false; + } + // eslint-disable-next-line no-undef + var canUseArrayBuffer = typeof ArrayBuffer !== 'undefined'; + // eslint-disable-next-line no-undef + var canUseUint8Clamped = typeof Uint8ClampedArray !== 'undefined'; + + if (!(canMeasurePerf && canUseImageData && canUseArrayBuffer && canUseUint8Clamped)) { + return; + } + + var targetCanvas = fabric.util.createCanvasElement(); + // eslint-disable-next-line no-undef + var imageBuffer = new ArrayBuffer(width * height * 4); + if (fabric.forceGLPutImageData) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + return; + } + var testContext = { + imageBuffer: imageBuffer, + destinationWidth: width, + destinationHeight: height, + targetCanvas: targetCanvas + }; + var startTime, drawImageTime, putImageDataTime; + targetCanvas.width = width; + targetCanvas.height = height; + + startTime = window.performance.now(); + copyGLTo2DDrawImage.call(testContext, this.gl, testContext); + drawImageTime = window.performance.now() - startTime; + + startTime = window.performance.now(); + copyGLTo2DPutImageData.call(testContext, this.gl, testContext); + putImageDataTime = window.performance.now() - startTime; + + if (drawImageTime > putImageDataTime) { + this.imageBuffer = imageBuffer; + this.copyGLTo2D = copyGLTo2DPutImageData; + } + else { + this.copyGLTo2D = copyGLTo2DDrawImage; + } + }, + + /** + * Create a canvas element and associated WebGL context and attaches them as + * class properties to the GLFilterBackend class. + */ + createWebGLCanvas: function(width, height) { + var canvas = fabric.util.createCanvasElement(); + canvas.width = width; + canvas.height = height; + var glOptions = { + alpha: true, + premultipliedAlpha: false, + depth: false, + stencil: false, + antialias: false + }, + gl = canvas.getContext('webgl', glOptions); + if (!gl) { + gl = canvas.getContext('experimental-webgl', glOptions); + } + if (!gl) { + return; + } + gl.clearColor(0, 0, 0, 0); + // this canvas can fire webglcontextlost and webglcontextrestored + this.canvas = canvas; + this.gl = gl; + }, + + /** + * Attempts to apply the requested filters to the source provided, drawing the filtered output + * to the provided target canvas. + * + * @param {Array} filters The filters to apply. + * @param {HTMLImageElement|HTMLCanvasElement} source The source to be filtered. + * @param {Number} width The width of the source input. + * @param {Number} height The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + * @param {String|undefined} cacheKey A key used to cache resources related to the source. If + * omitted, caching will be skipped. + */ + applyFilters: function(filters, source, width, height, targetCanvas, cacheKey) { + var gl = this.gl; + var cachedTexture; + if (cacheKey) { + cachedTexture = this.getCachedTexture(cacheKey, source); + } + var pipelineState = { + originalWidth: source.width || source.originalWidth, + originalHeight: source.height || source.originalHeight, + sourceWidth: width, + sourceHeight: height, + destinationWidth: width, + destinationHeight: height, + context: gl, + sourceTexture: this.createTexture(gl, width, height, !cachedTexture && source), + targetTexture: this.createTexture(gl, width, height), + originalTexture: cachedTexture || + this.createTexture(gl, width, height, !cachedTexture && source), + passes: filters.length, + webgl: true, + aPosition: this.aPosition, + programCache: this.programCache, + pass: 0, + filterBackend: this, + targetCanvas: targetCanvas + }; + var tempFbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo); + filters.forEach(function(filter) { filter && filter.applyTo(pipelineState); }); + resizeCanvasIfNeeded(pipelineState); + this.copyGLTo2D(gl, pipelineState); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.deleteTexture(pipelineState.sourceTexture); + gl.deleteTexture(pipelineState.targetTexture); + gl.deleteFramebuffer(tempFbo); + targetCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); + return pipelineState; + }, + + /** + * Detach event listeners, remove references, and clean up caches. + */ + dispose: function() { + if (this.canvas) { + this.canvas = null; + this.gl = null; + } + this.clearWebGLCaches(); + }, + + /** + * Wipe out WebGL-related caches. + */ + clearWebGLCaches: function() { + this.programCache = {}; + this.textureCache = {}; + }, + + /** + * Create a WebGL texture object. + * + * Accepts specific dimensions to initialize the textuer to or a source image. + * + * @param {WebGLRenderingContext} gl The GL context to use for creating the texture. + * @param {Number} width The width to initialize the texture at. + * @param {Number} height The height to initialize the texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source for the texture data. + * @returns {WebGLTexture} + */ + createTexture: function(gl, width, height, textureImageSource) { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + if (textureImageSource) { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImageSource); + } + else { + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + return texture; + }, + + /** + * Can be optionally used to get a texture from the cache array + * + * If an existing texture is not found, a new texture is created and cached. + * + * @param {String} uniqueId A cache key to use to find an existing texture. + * @param {HTMLImageElement|HTMLCanvasElement} textureImageSource A source to use to create the + * texture cache entry if one does not already exist. + */ + getCachedTexture: function(uniqueId, textureImageSource) { + if (this.textureCache[uniqueId]) { + return this.textureCache[uniqueId]; + } + else { + var texture = this.createTexture( + this.gl, textureImageSource.width, textureImageSource.height, textureImageSource); + this.textureCache[uniqueId] = texture; + return texture; + } + }, + + /** + * Clear out cached resources related to a source image that has been + * filtered previously. + * + * @param {String} cacheKey The cache key provided when the source image was filtered. + */ + evictCachesForKey: function(cacheKey) { + if (this.textureCache[cacheKey]) { + this.gl.deleteTexture(this.textureCache[cacheKey]); + delete this.textureCache[cacheKey]; + } + }, + + copyGLTo2D: copyGLTo2DDrawImage, + + /** + * Attempt to extract GPU information strings from a WebGL context. + * + * Useful information when debugging or blacklisting specific GPUs. + * + * @returns {Object} A GPU info object with renderer and vendor strings. + */ + captureGPUInfo: function() { + if (this.gpuInfo) { + return this.gpuInfo; + } + var gl = this.gl, gpuInfo = { renderer: '', vendor: '' }; + if (!gl) { + return gpuInfo; + } + var ext = gl.getExtension('WEBGL_debug_renderer_info'); + if (ext) { + var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); + var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); + if (renderer) { + gpuInfo.renderer = renderer.toLowerCase(); + } + if (vendor) { + gpuInfo.vendor = vendor.toLowerCase(); + } + } + this.gpuInfo = gpuInfo; + return gpuInfo; + }, + }; +})(); + +function resizeCanvasIfNeeded(pipelineState) { + var targetCanvas = pipelineState.targetCanvas, + width = targetCanvas.width, height = targetCanvas.height, + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight; + + if (width !== dWidth || height !== dHeight) { + targetCanvas.width = dWidth; + targetCanvas.height = dHeight; + } +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas. + * + * The WebGL canvas is assumed to be upside down, with the top-left pixel of the + * desired output image appearing in the bottom-left corner of the WebGL canvas. + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DDrawImage(gl, pipelineState) { + var glCanvas = gl.canvas, targetCanvas = pipelineState.targetCanvas, + ctx = targetCanvas.getContext('2d'); + ctx.translate(0, targetCanvas.height); // move it down again + ctx.scale(1, -1); // vertical flip + // where is my image on the big glcanvas? + var sourceY = glCanvas.height - targetCanvas.height; + ctx.drawImage(glCanvas, 0, sourceY, targetCanvas.width, targetCanvas.height, 0, 0, + targetCanvas.width, targetCanvas.height); +} + +/** + * Copy an input WebGL canvas on to an output 2D canvas using 2d canvas' putImageData + * API. Measurably faster than using ctx.drawImage in Firefox (version 54 on OSX Sierra). + * + * @param {WebGLRenderingContext} sourceContext The WebGL context to copy from. + * @param {HTMLCanvasElement} targetCanvas The 2D target canvas to copy on to. + * @param {Object} pipelineState The 2D target canvas to copy on to. + */ +function copyGLTo2DPutImageData(gl, pipelineState) { + var targetCanvas = pipelineState.targetCanvas, ctx = targetCanvas.getContext('2d'), + dWidth = pipelineState.destinationWidth, + dHeight = pipelineState.destinationHeight, + numBytes = dWidth * dHeight * 4; + + // eslint-disable-next-line no-undef + var u8 = new Uint8Array(this.imageBuffer, 0, numBytes); + // eslint-disable-next-line no-undef + var u8Clamped = new Uint8ClampedArray(this.imageBuffer, 0, numBytes); + + gl.readPixels(0, 0, dWidth, dHeight, gl.RGBA, gl.UNSIGNED_BYTE, u8); + var imgData = new ImageData(u8Clamped, dWidth, dHeight); + ctx.putImageData(imgData, 0, 0); +} +(function() { + + 'use strict'; + + var noop = function() {}; + + fabric.Canvas2dFilterBackend = Canvas2dFilterBackend; + + /** + * Canvas 2D filter backend. + */ + function Canvas2dFilterBackend() {}; + + Canvas2dFilterBackend.prototype = /** @lends fabric.Canvas2dFilterBackend.prototype */ { + evictCachesForKey: noop, + dispose: noop, + clearWebGLCaches: noop, + + /** + * Experimental. This object is a sort of repository of help layers used to avoid + * of recreating them during frequent filtering. If you are previewing a filter with + * a slider you probably do not want to create help layers every filter step. + * in this object there will be appended some canvases, created once, resized sometimes + * cleared never. Clearing is left to the developer. + **/ + resources: { + + }, + + /** + * Apply a set of filters against a source image and draw the filtered output + * to the provided destination canvas. + * + * @param {EnhancedFilter} filters The filter to apply. + * @param {HTMLImageElement|HTMLCanvasElement} sourceElement The source to be filtered. + * @param {Number} sourceWidth The width of the source input. + * @param {Number} sourceHeight The height of the source input. + * @param {HTMLCanvasElement} targetCanvas The destination for filtered output to be drawn. + */ + applyFilters: function(filters, sourceElement, sourceWidth, sourceHeight, targetCanvas) { + var ctx = targetCanvas.getContext('2d'); + ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight); + var imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var originalImageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight); + var pipelineState = { + sourceWidth: sourceWidth, + sourceHeight: sourceHeight, + imageData: imageData, + originalEl: sourceElement, + originalImageData: originalImageData, + canvasEl: targetCanvas, + ctx: ctx, + filterBackend: this, + }; + filters.forEach(function(filter) { filter.applyTo(pipelineState); }); + if (pipelineState.imageData.width !== sourceWidth || pipelineState.imageData.height !== sourceHeight) { + targetCanvas.width = pipelineState.imageData.width; + targetCanvas.height = pipelineState.imageData.height; + } + ctx.putImageData(pipelineState.imageData, 0, 0); + return pipelineState; + }, + + }; +})(); +/** + * @namespace fabric.Image.filters + * @memberOf fabric.Image + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + */ +fabric.Image = fabric.Image || { }; +fabric.Image.filters = fabric.Image.filters || { }; + +/** + * Root filter class from which all filter classes inherit from + * @class fabric.Image.filters.BaseFilter + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Image.filters.BaseFilter.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'BaseFilter', + + /** + * Array of attributes to send with buffers. do not modify + * @private + */ + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + fragmentSource: 'precision highp float;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform sampler2D uTexture;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + '}', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Sets filter's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * Compile this filter's shader program. + * + * @param {WebGLRenderingContext} gl The GL canvas context to use for shader compilation. + * @param {String} fragmentSource fragmentShader source for compilation + * @param {String} vertexSource vertexShader source for compilation + */ + createProgram: function(gl, fragmentSource, vertexSource) { + fragmentSource = fragmentSource || this.fragmentSource; + vertexSource = vertexSource || this.vertexSource; + if (fabric.webGlPrecision !== 'highp'){ + fragmentSource = fragmentSource.replace( + /precision highp float/g, + 'precision ' + fabric.webGlPrecision + ' float' + ); + } + var vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexSource); + gl.compileShader(vertexShader); + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Vertex shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(vertexShader) + ); + } + + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Fragment shader compile error for ' + this.type + ': ' + + gl.getShaderInfoLog(fragmentShader) + ); + } + + var program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error( + // eslint-disable-next-line prefer-template + 'Shader link error for "${this.type}" ' + + gl.getProgramInfoLog(program) + ); + } + + var attributeLocations = this.getAttributeLocations(gl, program); + var uniformLocations = this.getUniformLocations(gl, program) || { }; + uniformLocations.uStepW = gl.getUniformLocation(program, 'uStepW'); + uniformLocations.uStepH = gl.getUniformLocation(program, 'uStepH'); + return { + program: program, + attributeLocations: attributeLocations, + uniformLocations: uniformLocations + }; + }, + + /** + * Return a map of attribute names to WebGLAttributeLocation objects. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take attribute locations. + * @returns {Object} A map of attribute names to attribute locations. + */ + getAttributeLocations: function(gl, program) { + return { + aPosition: gl.getAttribLocation(program, 'aPosition'), + }; + }, + + /** + * Return a map of uniform names to WebGLUniformLocation objects. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {WebGLShaderProgram} program The shader program from which to take uniform locations. + * @returns {Object} A map of uniform names to uniform locations. + */ + getUniformLocations: function (/* gl, program */) { + // in case i do not need any special uniform i need to return an empty object + return { }; + }, + + /** + * Send attribute data from this filter to its shader program on the GPU. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} attributeLocations A map of shader attribute names to their locations. + */ + sendAttributeData: function(gl, attributeLocations, aPositionData) { + var attributeLocation = attributeLocations.aPosition; + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(attributeLocation); + gl.vertexAttribPointer(attributeLocation, 2, gl.FLOAT, false, 0, 0); + gl.bufferData(gl.ARRAY_BUFFER, aPositionData, gl.STATIC_DRAW); + }, + + _setupFrameBuffer: function(options) { + var gl = options.context, width, height; + if (options.passes > 1) { + width = options.destinationWidth; + height = options.destinationHeight; + if (options.sourceWidth !== width || options.sourceHeight !== height) { + gl.deleteTexture(options.targetTexture); + options.targetTexture = options.filterBackend.createTexture(gl, width, height); + } + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, + options.targetTexture, 0); + } + else { + // draw last filter on canvas and not to framebuffer. + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.finish(); + } + }, + + _swapTextures: function(options) { + options.passes--; + options.pass++; + var temp = options.targetTexture; + options.targetTexture = options.sourceTexture; + options.sourceTexture = temp; + }, + + /** + * Generic isNeutral implementation for one parameter based filters. + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * Other filters may need their own verison ( ColorMatrix, HueRotation, gamma, ComposedFilter ) + * @param {Object} options + **/ + isNeutralState: function(/* options */) { + var main = this.mainParameter, + _class = fabric.Image.filters[this.type].prototype; + if (main) { + if (Array.isArray(_class[main])) { + for (var i = _class[main].length; i--;) { + if (this[main][i] !== _class[main][i]) { + return false; + } + } + return true; + } + else { + return _class[main] === this[main]; + } + } + else { + return false; + } + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + if (!options.programCache.hasOwnProperty(this.type)) { + options.programCache[this.type] = this.createProgram(options.context); + } + return options.programCache[this.type]; + }, + + /** + * Apply this filter using webgl. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.originalTexture The texture of the original input image. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyToWebGL: function(options) { + var gl = options.context; + var shader = this.retrieveShader(options); + if (options.pass === 0 && options.originalTexture) { + gl.bindTexture(gl.TEXTURE_2D, options.originalTexture); + } + else { + gl.bindTexture(gl.TEXTURE_2D, options.sourceTexture); + } + gl.useProgram(shader.program); + this.sendAttributeData(gl, shader.attributeLocations, options.aPosition); + + gl.uniform1f(shader.uniformLocations.uStepW, 1 / options.sourceWidth); + gl.uniform1f(shader.uniformLocations.uStepH, 1 / options.sourceHeight); + + this.sendUniformData(gl, shader.uniformLocations); + gl.viewport(0, 0, options.destinationWidth, options.destinationHeight); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + + bindAdditionalTexture: function(gl, texture, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, texture); + // reset active texture to 0 as usual + gl.activeTexture(gl.TEXTURE0); + }, + + unbindAdditionalTexture: function(gl, textureUnit) { + gl.activeTexture(textureUnit); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.activeTexture(gl.TEXTURE0); + }, + + getMainParameter: function() { + return this[this.mainParameter]; + }, + + setMainParameter: function(value) { + this[this.mainParameter] = value; + }, + + /** + * Send uniform data from this filter to its shader program on the GPU. + * + * Intended to be overridden by subclasses. + * + * @param {WebGLRenderingContext} gl The canvas context used to compile the shader program. + * @param {Object} uniformLocations A map of shader uniform names to their locations. + */ + sendUniformData: function(/* gl, uniformLocations */) { + // Intentionally left blank. Override me in subclasses. + }, + + /** + * If needed by a 2d filter, this functions can create an helper canvas to be used + * remember that options.targetCanvas is available for use till end of chain. + */ + createHelpLayer: function(options) { + if (!options.helpLayer) { + var helpLayer = document.createElement('canvas'); + helpLayer.width = options.sourceWidth; + helpLayer.height = options.sourceHeight; + options.helpLayer = helpLayer; + } + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + var object = { type: this.type }, mainP = this.mainParameter; + if (mainP) { + object[mainP] = this[mainP]; + } + return object; + }, + + /** + * Returns a JSON representation of an instance + * @return {Object} JSON + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + } +}); + +fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { + var filter = new fabric.Image.filters[object.type](object); + callback && callback(filter); + return filter; +}; +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Matrix filter class + * @class fabric.Image.filters.ColorMatrix + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.ColorMatrix#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @see {@Link http://www.webwasp.co.uk/tutorials/219/Color_Matrix_Filter.php} + * @see {@Link http://phoboslab.org/log/2013/11/fast-image-filters-with-webgl} + * @example Kodachrome filter + * var filter = new fabric.Image.filters.ColorMatrix({ + * matrix: [ + 1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502, + -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203, + -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946, + 0, 0, 0, 1, 0 + ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'ColorMatrix', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'uniform mat4 uColorMatrix;\n' + + 'uniform vec4 uConstants;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color *= uColorMatrix;\n' + + 'color += uConstants;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Colormatrix for pixels. + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ], + + mainParameter: 'matrix', + + /** + * Lock the colormatrix on the color part, skipping alpha, manly for non webgl scenario + * to save some calculation + */ + colorsOnly: true, + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.matrix = this.matrix.slice(0); + }, + + /** + * Apply the ColorMatrix operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = data.length, + m = this.matrix, + r, g, b, a, i, colorsOnly = this.colorsOnly; + + for (i = 0; i < iLen; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + if (colorsOnly) { + data[i] = r * m[0] + g * m[1] + b * m[2] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + m[14] * 255; + } + else { + a = data[i + 3]; + data[i] = r * m[0] + g * m[1] + b * m[2] + a * m[3] + m[4] * 255; + data[i + 1] = r * m[5] + g * m[6] + b * m[7] + a * m[8] + m[9] * 255; + data[i + 2] = r * m[10] + g * m[11] + b * m[12] + a * m[13] + m[14] * 255; + data[i + 3] = r * m[15] + g * m[16] + b * m[17] + a * m[18] + m[19] * 255; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColorMatrix: gl.getUniformLocation(program, 'uColorMatrix'), + uConstants: gl.getUniformLocation(program, 'uConstants'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var m = this.matrix, + matrix = [ + m[0], m[1], m[2], m[3], + m[5], m[6], m[7], m[8], + m[10], m[11], m[12], m[13], + m[15], m[16], m[17], m[18] + ], + constants = [m[4], m[9], m[14], m[19]]; + gl.uniformMatrix4fv(uniformLocations.uColorMatrix, false, matrix); + gl.uniform4fv(uniformLocations.uConstants, constants); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] function to invoke after filter creation + * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix + */ + fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Brightness filter class + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 0.05 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Brightness = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Brightness', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBrightness;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += uBrightness;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Brightness value, from -1 to 1. + * translated to -255 to 255 for 2d + * 0.0039215686 is the part of 1 that get translated to 1 in 2d + * @param {Number} brightness + * @default + */ + brightness: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'brightness', + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.brightness === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + brightness = Math.round(this.brightness * 255); + for (i = 0; i < len; i += 4) { + data[i] = data[i] + brightness; + data[i + 1] = data[i + 1] + brightness; + data[i + 2] = data[i + 2] + brightness; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBrightness: gl.getUniformLocation(program, 'uBrightness'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBrightness, this.brightness); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + */ + fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Adapted from html5rocks article + * @class fabric.Image.filters.Convolute + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Convolute = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Convolute', + + /* + * Opaque value (true/false) + */ + opaque: false, + + /* + * matrix for the filter, max 9x9 + */ + matrix: [0, 0, 0, 0, 1, 0, 0, 0, 0], + + /** + * Fragment source for the brightness program + */ + fragmentSource: { + Convolute_3_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1), uStepH * (h - 1));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_3_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[9];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 3.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 3.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 1.0), uStepH * (h - 1.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 3.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_5_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_5_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[25];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 5.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 5.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 2.0), uStepH * (h - 2.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 5.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_7_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_7_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[49];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 7.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 7.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 3.0), uStepH * (h - 3.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 7.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + Convolute_9_1: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 0);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color += texture2D(uTexture, vTexCoord + matrixPos) * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'gl_FragColor = color;\n' + + '}', + Convolute_9_0: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uMatrix[81];\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = vec4(0, 0, 0, 1);\n' + + 'for (float h = 0.0; h < 9.0; h+=1.0) {\n' + + 'for (float w = 0.0; w < 9.0; w+=1.0) {\n' + + 'vec2 matrixPos = vec2(uStepW * (w - 4.0), uStepH * (h - 4.0));\n' + + 'color.rgb += texture2D(uTexture, vTexCoord + matrixPos).rgb * uMatrix[int(h * 9.0 + w)];\n' + + '}\n' + + '}\n' + + 'float alpha = texture2D(uTexture, vTexCoord).a;\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.a = alpha;\n' + + '}', + }, + + /** + * Constructor + * @memberOf fabric.Image.filters.Convolute.prototype + * @param {Object} [options] Options object + * @param {Boolean} [options.opaque=false] Opaque value (true/false) + * @param {Array} [options.matrix] Filter matrix + */ + + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var size = Math.sqrt(this.matrix.length); + var cacheKey = this.type + '_' + size + '_' + (this.opaque ? 1 : 0); + var shaderSource = this.fragmentSource[cacheKey]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + weights = this.matrix, + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side / 2), + sw = imageData.width, + sh = imageData.height, + output = options.ctx.createImageData(sw, sh), + dst = output.data, + // go through the destination image pixels + alphaFac = this.opaque ? 1 : 0, + r, g, b, a, dstOff, + scx, scy, srcOff, wt, + x, y, cx, cy; + + for (y = 0; y < sh; y++) { + for (x = 0; x < sw; x++) { + dstOff = (y * sw + x) * 4; + // calculate the weighed sum of the source image pixels that + // fall under the convolution matrix + r = 0; g = 0; b = 0; a = 0; + + for (cy = 0; cy < side; cy++) { + for (cx = 0; cx < side; cx++) { + scy = y + cy - halfSide; + scx = x + cx - halfSide; + + // eslint-disable-next-line max-depth + if (scy < 0 || scy > sh || scx < 0 || scx > sw) { + continue; + } + + srcOff = (scy * sw + scx) * 4; + wt = weights[cy * side + cx]; + + r += data[srcOff] * wt; + g += data[srcOff + 1] * wt; + b += data[srcOff + 2] * wt; + // eslint-disable-next-line max-depth + if (!alphaFac) { + a += data[srcOff + 3] * wt; + } + } + } + dst[dstOff] = r; + dst[dstOff + 1] = g; + dst[dstOff + 2] = b; + if (!alphaFac) { + dst[dstOff + 3] = a; + } + else { + dst[dstOff + 3] = data[dstOff + 3]; + } + } + } + options.imageData = output; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMatrix: gl.getUniformLocation(program, 'uMatrix'), + uOpaque: gl.getUniformLocation(program, 'uOpaque'), + uHalfSize: gl.getUniformLocation(program, 'uHalfSize'), + uSize: gl.getUniformLocation(program, 'uSize'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1fv(uniformLocations.uMatrix, this.matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + opaque: this.opaque, + matrix: this.matrix + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + */ + fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Grayscale image filter class + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Grayscale = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Grayscale', + + fragmentSource: { + average: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float average = (color.r + color.b + color.g) / 3.0;\n' + + 'gl_FragColor = vec4(average, average, average, color.a);\n' + + '}', + lightness: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + luminosity: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uMode;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 col = texture2D(uTexture, vTexCoord);\n' + + 'float average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\n' + + 'gl_FragColor = vec4(average, average, average, col.a);\n' + + '}', + }, + + + /** + * Grayscale mode, between 'average', 'lightness', 'luminosity' + * @param {String} type + * @default + */ + mode: 'average', + + mainParameter: 'mode', + + /** + * Apply the Grayscale operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length, value, + mode = this.mode; + for (i = 0; i < len; i += 4) { + if (mode === 'average') { + value = (data[i] + data[i + 1] + data[i + 2]) / 3; + } + else if (mode === 'lightness') { + value = (Math.min(data[i], data[i + 1], data[i + 2]) + + Math.max(data[i], data[i + 1], data[i + 2])) / 2; + } + else if (mode === 'luminosity') { + value = 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2]; + } + data[i] = value; + data[i + 1] = value; + data[i + 2] = value; + } + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var shaderSource = this.fragmentSource[this.mode]; + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uMode: gl.getUniformLocation(program, 'uMode'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + // default average mode. + var mode = 1; + gl.uniform1i(uniformLocations.uMode, mode); + }, + + /** + * Grayscale filter isNeutralState implementation + * The filter is never neutral + * on the image + **/ + isNeutralState: function() { + return false; + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + */ + fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Invert filter class + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Invert = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Invert', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform int uInvert;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'if (uInvert == 1) {\n' + + 'gl_FragColor = vec4(1.0 - color.r,1.0 -color.g,1.0 -color.b,color.a);\n' + + '} else {\n' + + 'gl_FragColor = color;\n' + + '}\n' + + '}', + + /** + * Filter invert. if false, does nothing + * @param {Boolean} invert + * @default + */ + invert: true, + + mainParameter: 'invert', + + /** + * Apply the Invert operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + len = data.length; + for (i = 0; i < len; i += 4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + }, + + /** + * Invert filter isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function() { + return !this.invert; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uInvert: gl.getUniformLocation(program, 'uInvert'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1i(uniformLocations.uInvert, this.invert); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + */ + fabric.Image.filters.Invert.fromObject = fabric.Image.filters.BaseFilter.fromObject; + + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Noise filter class + * @class fabric.Image.filters.Noise + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Noise = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Noise', + + /** + * Fragment source for the noise program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uStepH;\n' + + 'uniform float uNoise;\n' + + 'uniform float uSeed;\n' + + 'varying vec2 vTexCoord;\n' + + 'float rand(vec2 co, float seed, float vScale) {\n' + + 'return fract(sin(dot(co.xy * vScale ,vec2(12.9898 , 78.233))) * 43758.5453 * (seed + 0.01) / 2.0);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'color.rgb += (0.5 - rand(vTexCoord, uSeed, 0.1 / uStepH)) * uNoise;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'noise', + + /** + * Noise value, from + * @param {Number} noise + * @default + */ + noise: 0, + + /** + * Apply the Brightness operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.noise === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, i, len = data.length, + noise = this.noise, rand; + + for (i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uNoise: gl.getUniformLocation(program, 'uNoise'), + uSeed: gl.getUniformLocation(program, 'uSeed'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uNoise, this.noise / 255); + gl.uniform1f(uniformLocations.uSeed, Math.random()); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + noise: this.noise + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + */ + fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Pixelate filter class + * @class fabric.Image.filters.Pixelate + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Pixelate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Pixelate', + + blocksize: 4, + + mainParameter: 'blocksize', + + /** + * Fragment source for the Pixelate program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uBlocksize;\n' + + 'uniform float uStepW;\n' + + 'uniform float uStepH;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'float blockW = uBlocksize * uStepW;\n' + + 'float blockH = uBlocksize * uStepW;\n' + + 'int posX = int(vTexCoord.x / blockW);\n' + + 'int posY = int(vTexCoord.y / blockH);\n' + + 'float fposX = float(posX);\n' + + 'float fposY = float(posY);\n' + + 'vec2 squareCoords = vec2(fposX * blockW, fposY * blockH);\n' + + 'vec4 color = texture2D(uTexture, squareCoords);\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Apply the Pixelate operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, + iLen = imageData.height, + jLen = imageData.width, + index, i, j, r, g, b, a, + _i, _j, _iLen, _jLen; + + for (i = 0; i < iLen; i += this.blocksize) { + for (j = 0; j < jLen; j += this.blocksize) { + + index = (i * 4) * jLen + (j * 4); + + r = data[index]; + g = data[index + 1]; + b = data[index + 2]; + a = data[index + 3]; + + _iLen = Math.min(i + this.blocksize, iLen); + _jLen = Math.min(j + this.blocksize, jLen); + for (_i = i; _i < _iLen; _i++) { + for (_j = j; _j < _jLen; _j++) { + index = (_i * 4) * jLen + (_j * 4); + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = a; + } + } + } + } + }, + + /** + * Indicate when the filter is not gonna apply changes to the image + **/ + isNeutralState: function() { + return this.blocksize === 1; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uBlocksize: gl.getUniformLocation(program, 'uBlocksize'), + uStepW: gl.getUniformLocation(program, 'uStepW'), + uStepH: gl.getUniformLocation(program, 'uStepH'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uBlocksize, this.blocksize); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + */ + fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Remove white filter class + * @class fabric.Image.filters.RemoveColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveColor#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveColor({ + * threshold: 0.2, + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.RemoveColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveColor.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'RemoveColor', + + /** + * Color to remove, in any format understood by fabric.Color. + * @param {String} type + * @default + */ + color: '#FFFFFF', + + /** + * Fragment source for the brightness program + */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uLow;\n' + + 'uniform vec4 uHigh;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'gl_FragColor = texture2D(uTexture, vTexCoord);\n' + + 'if(all(greaterThan(gl_FragColor.rgb,uLow.rgb)) && all(greaterThan(uHigh.rgb,gl_FragColor.rgb))) {\n' + + 'gl_FragColor.a = 0.0;\n' + + '}\n' + + '}', + + /** + * distance to actual color, as value up or down from each r,g,b + * between 0 and 1 + **/ + distance: 0.02, + + /** + * For color to remove inside distance, use alpha channel for a smoother deletion + * NOT IMPLEMENTED YET + **/ + useAlpha: false, + + /** + * Constructor + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + * @param {Number} [options.color=#RRGGBB] Threshold value + * @param {Number} [options.distance=10] Distance value + */ + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, i, + distance = this.distance * 255, + r, g, b, + source = new fabric.Color(this.color).getSource(), + lowC = [ + source[0] - distance, + source[1] - distance, + source[2] - distance, + ], + highC = [ + source[0] + distance, + source[1] + distance, + source[2] + distance, + ]; + + + for (i = 0; i < data.length; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + if (r > lowC[0] && + g > lowC[1] && + b > lowC[2] && + r < highC[0] && + g < highC[1] && + b < highC[2]) { + data[i + 3] = 0; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uLow: gl.getUniformLocation(program, 'uLow'), + uHigh: gl.getUniformLocation(program, 'uHigh'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(), + distance = parseFloat(this.distance), + lowC = [ + 0 + source[0] / 255 - distance, + 0 + source[1] / 255 - distance, + 0 + source[2] / 255 - distance, + 1 + ], + highC = [ + source[0] / 255 + distance, + source[1] / 255 + distance, + source[2] / 255 + distance, + 1 + ]; + gl.uniform4fv(uniformLocations.uLow, lowC); + gl.uniform4fv(uniformLocations.uHigh, highC); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + color: this.color, + distance: this.distance + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite + */ + fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + var matrices = { + Brownie: [ + 0.59970,0.34553,-0.27082,0,0.186, + -0.03770,0.86095,0.15059,0,-0.1449, + 0.24113,-0.07441,0.44972,0,-0.02965, + 0,0,0,1,0 + ], + Vintage: [ + 0.62793,0.32021,-0.03965,0,0.03784, + 0.02578,0.64411,0.03259,0,0.02926, + 0.04660,-0.08512,0.52416,0,0.02023, + 0,0,0,1,0 + ], + Kodachrome: [ + 1.12855,-0.39673,-0.03992,0,0.24991, + -0.16404,1.08352,-0.05498,0,0.09698, + -0.16786,-0.56034,1.60148,0,0.13972, + 0,0,0,1,0 + ], + Technicolor: [ + 1.91252,-0.85453,-0.09155,0,0.04624, + -0.30878,1.76589,-0.10601,0,-0.27589, + -0.23110,-0.75018,1.84759,0,0.12137, + 0,0,0,1,0 + ], + Polaroid: [ + 1.438,-0.062,-0.062,0,0, + -0.122,1.378,-0.122,0,0, + -0.016,-0.016,1.483,0,0, + 0,0,0,1,0 + ], + Sepia: [ + 0.393, 0.769, 0.189, 0, 0, + 0.349, 0.686, 0.168, 0, 0, + 0.272, 0.534, 0.131, 0, 0, + 0, 0, 0, 1, 0 + ], + BlackWhite: [ + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 1.5, 1.5, 1.5, 0, -1, + 0, 0, 0, 1, 0, + ] + }; + + for (var key in matrices) { + filters[key] = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.Sepia.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: key, + + /** + * Colormatrix for the effect + * array of 20 floats. Numbers in positions 4, 9, 14, 19 loose meaning + * outside the -1, 1 range. + * @param {Array} matrix array of 20 numbers. + * @default + */ + matrix: matrices[key], + + /** + * Lock the matrix export for this kind of static, parameter less filters. + */ + mainParameter: false, + /** + * Lock the colormatrix on the color part, skipping alpha + */ + colorsOnly: true, + + }); + fabric.Image.filters[key].fromObject = fabric.Image.filters.BaseFilter.fromObject; + } +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Blend filter class + * @class fabric.Image.filter.BlendColor + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { + type: 'BlendColor', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + color: '#F95C63', + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend color operation. + **/ + alpha: 1, + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'gl_FragColor.rgb *= uColor.rgb;\n', + screen: 'gl_FragColor.rgb = 1.0 - (1.0 - gl_FragColor.rgb) * (1.0 - uColor.rgb);\n', + add: 'gl_FragColor.rgb += uColor.rgb;\n', + diff: 'gl_FragColor.rgb = abs(gl_FragColor.rgb - uColor.rgb);\n', + subtract: 'gl_FragColor.rgb -= uColor.rgb;\n', + lighten: 'gl_FragColor.rgb = max(gl_FragColor.rgb, uColor.rgb);\n', + darken: 'gl_FragColor.rgb = min(gl_FragColor.rgb, uColor.rgb);\n', + exclusion: 'gl_FragColor.rgb += uColor.rgb - 2.0 * (uColor.rgb * gl_FragColor.rgb);\n', + overlay: 'if (uColor.r < 0.5) {\n' + + 'gl_FragColor.r *= 2.0 * uColor.r;\n' + + '} else {\n' + + 'gl_FragColor.r = 1.0 - 2.0 * (1.0 - gl_FragColor.r) * (1.0 - uColor.r);\n' + + '}\n' + + 'if (uColor.g < 0.5) {\n' + + 'gl_FragColor.g *= 2.0 * uColor.g;\n' + + '} else {\n' + + 'gl_FragColor.g = 1.0 - 2.0 * (1.0 - gl_FragColor.g) * (1.0 - uColor.g);\n' + + '}\n' + + 'if (uColor.b < 0.5) {\n' + + 'gl_FragColor.b *= 2.0 * uColor.b;\n' + + '} else {\n' + + 'gl_FragColor.b = 1.0 - 2.0 * (1.0 - gl_FragColor.b) * (1.0 - uColor.b);\n' + + '}\n', + tint: 'gl_FragColor.rgb *= (1.0 - uColor.a);\n' + + 'gl_FragColor.rgb += uColor.rgb;\n', + }, + + /** + * build the fragment source for the filters, joining the common part with + * the specific one. + * @param {String} mode the mode of the filter, a key of this.fragmentSource + * @return {String} the source to be compiled + * @private + */ + buildSource: function(mode) { + return 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'gl_FragColor = color;\n' + + 'if (color.a > 0.0) {\n' + + this.fragmentSource[mode] + + '}\n' + + '}'; + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode, shaderSource; + if (!options.programCache.hasOwnProperty(cacheKey)) { + shaderSource = this.buildSource(this.mode); + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + data = imageData.data, iLen = data.length, + tr, tg, tb, + r, g, b, + source, alpha1 = 1 - this.alpha; + + source = new fabric.Color(this.color).getSource(); + tr = source[0] * this.alpha; + tg = source[1] * this.alpha; + tb = source[2] * this.alpha; + + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + break; + case 'screen': + data[i] = 255 - (255 - r) * (255 - tr) / 255; + data[i + 1] = 255 - (255 - g) * (255 - tg) / 255; + data[i + 2] = 255 - (255 - b) * (255 - tb) / 255; + break; + case 'add': + data[i] = r + tr; + data[i + 1] = g + tg; + data[i + 2] = b + tb; + break; + case 'diff': + case 'difference': + data[i] = Math.abs(r - tr); + data[i + 1] = Math.abs(g - tg); + data[i + 2] = Math.abs(b - tb); + break; + case 'subtract': + data[i] = r - tr; + data[i + 1] = g - tg; + data[i + 2] = b - tb; + break; + case 'darken': + data[i] = Math.min(r, tr); + data[i + 1] = Math.min(g, tg); + data[i + 2] = Math.min(b, tb); + break; + case 'lighten': + data[i] = Math.max(r, tr); + data[i + 1] = Math.max(g, tg); + data[i + 2] = Math.max(b, tb); + break; + case 'overlay': + data[i] = tr < 128 ? (2 * r * tr / 255) : (255 - 2 * (255 - r) * (255 - tr) / 255); + data[i + 1] = tg < 128 ? (2 * g * tg / 255) : (255 - 2 * (255 - g) * (255 - tg) / 255); + data[i + 2] = tb < 128 ? (2 * b * tb / 255) : (255 - 2 * (255 - b) * (255 - tb) / 255); + break; + case 'exclusion': + data[i] = tr + r - ((2 * tr * r) / 255); + data[i + 1] = tg + g - ((2 * tg * g) / 255); + data[i + 2] = tb + b - ((2 * tb * b) / 255); + break; + case 'tint': + data[i] = tr + r * alpha1; + data[i + 1] = tg + g * alpha1; + data[i + 2] = tb + b * alpha1; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uColor: gl.getUniformLocation(program, 'uColor'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var source = new fabric.Color(this.color).getSource(); + source[0] = this.alpha * source[0] / 255; + source[1] = this.alpha * source[1] / 255; + source[2] = this.alpha * source[2] / 255; + source[3] = this.alpha; + gl.uniform4fv(uniformLocations.uColor, source); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + color: this.color, + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor + */ + fabric.Image.filters.BlendColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Image Blend filter class + * @class fabric.Image.filter.BlendImage + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.BlendColor({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.BlendImage({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + + filters.BlendImage = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.BlendImage.prototype */ { + type: 'BlendImage', + + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + **/ + image: null, + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend image operation. + * not implemented. + **/ + alpha: 1, + + vertexSource: 'attribute vec2 aPosition;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'uniform mat3 uTransformMatrix;\n' + + 'void main() {\n' + + 'vTexCoord = aPosition;\n' + + 'vTexCoord2 = (uTransformMatrix * vec3(aPosition, 1.0)).xy;\n' + + 'gl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n' + + '}', + + /** + * Fragment source for the Multiply program + */ + fragmentSource: { + multiply: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.rgba *= color2.rgba;\n' + + 'gl_FragColor = color;\n' + + '}', + mask: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform sampler2D uImage;\n' + + 'uniform vec4 uColor;\n' + + 'varying vec2 vTexCoord;\n' + + 'varying vec2 vTexCoord2;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec4 color2 = texture2D(uImage, vTexCoord2);\n' + + 'color.a = color2.a;\n' + + 'gl_FragColor = color;\n' + + '}', + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var cacheKey = this.type + '_' + this.mode; + var shaderSource = this.fragmentSource[this.mode]; + if (!options.programCache.hasOwnProperty(cacheKey)) { + options.programCache[cacheKey] = this.createProgram(options.context, shaderSource); + } + return options.programCache[cacheKey]; + }, + + applyToWebGL: function(options) { + // load texture to blend. + var gl = options.context, + texture = this.createTexture(options.filterBackend, this.image); + this.bindAdditionalTexture(gl, texture, gl.TEXTURE1); + this.callSuper('applyToWebGL', options); + this.unbindAdditionalTexture(gl, gl.TEXTURE1); + }, + + createTexture: function(backend, image) { + return backend.getCachedTexture(image.cacheKey, image._element); + }, + + /** + * Calculate a transformMatrix to adapt the image to blend over + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + calculateMatrix: function() { + var image = this.image, + width = image._element.width, + height = image._element.height; + return [ + 1 / image.scaleX, 0, 0, + 0, 1 / image.scaleY, 0, + -image.left / width, -image.top / height, 1 + ]; + }, + + /** + * Apply the Blend operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, + resources = options.filterBackend.resources, + data = imageData.data, iLen = data.length, + width = imageData.width, + height = imageData.height, + tr, tg, tb, ta, + r, g, b, a, + canvas1, context, image = this.image, blendData; + + if (!resources.blendImage) { + resources.blendImage = fabric.util.createCanvasElement(); + } + canvas1 = resources.blendImage; + context = canvas1.getContext('2d'); + if (canvas1.width !== width || canvas1.height !== height) { + canvas1.width = width; + canvas1.height = height; + } + else { + context.clearRect(0, 0, width, height); + } + context.setTransform(image.scaleX, 0, 0, image.scaleY, image.left, image.top); + context.drawImage(image._element, 0, 0, width, height); + blendData = context.getImageData(0, 0, width, height).data; + for (var i = 0; i < iLen; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + + tr = blendData[i]; + tg = blendData[i + 1]; + tb = blendData[i + 2]; + ta = blendData[i + 3]; + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + data[i + 3] = a * ta / 255; + break; + case 'mask': + data[i + 3] = ta; + break; + } + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uTransformMatrix: gl.getUniformLocation(program, 'uTransformMatrix'), + uImage: gl.getUniformLocation(program, 'uImage'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var matrix = this.calculateMatrix(); + gl.uniform1i(uniformLocations.uImage, 1); // texture unit 1. + gl.uniformMatrix3fv(uniformLocations.uTransformMatrix, false, matrix); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + image: this.image && this.image.toObject(), + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} callback to be invoked after filter creation + * @return {fabric.Image.filters.BlendImage} Instance of fabric.Image.filters.BlendImage + */ + fabric.Image.filters.BlendImage.fromObject = function(object, callback) { + fabric.Image.fromObject(object.image, function(image) { + var options = fabric.util.object.clone(object); + options.image = image; + callback(new fabric.Image.filters.BlendImage(options)); + }); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor, + sqrt = Math.sqrt, abs = Math.abs, round = Math.round, sin = Math.sin, + ceil = Math.ceil, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Resize image filter class + * @class fabric.Image.filters.Resize + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Resize(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Resize = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Resize', + + /** + * Resize type + * for webgl resizeType is just lanczos, for canvas2d can be: + * bilinear, hermite, sliceHack, lanczos. + * @param {String} resizeType + * @default + */ + resizeType: 'hermite', + + /** + * Scale factor for resizing, x axis + * @param {Number} scaleX + * @default + */ + scaleX: 1, + + /** + * Scale factor for resizing, y axis + * @param {Number} scaleY + * @default + */ + scaleY: 1, + + /** + * LanczosLobes parameter for lanczos filter, valid for resizeType lanczos + * @param {Number} lanczosLobes + * @default + */ + lanczosLobes: 3, + + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uDelta: gl.getUniformLocation(program, 'uDelta'), + uTaps: gl.getUniformLocation(program, 'uTaps'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform2fv(uniformLocations.uDelta, this.horizontal ? [1 / this.width, 0] : [0, 1 / this.height]); + gl.uniform1fv(uniformLocations.uTaps, this.taps); + }, + + /** + * Retrieves the cached shader. + * @param {Object} options + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + retrieveShader: function(options) { + var filterWindow = this.getFilterWindow(), cacheKey = this.type + '_' + filterWindow; + if (!options.programCache.hasOwnProperty(cacheKey)) { + var fragmentShader = this.generateShader(filterWindow); + options.programCache[cacheKey] = this.createProgram(options.context, fragmentShader); + } + return options.programCache[cacheKey]; + }, + + getFilterWindow: function() { + var scale = this.tempScale; + return Math.ceil(this.lanczosLobes / scale); + }, + + getTaps: function() { + var lobeFunction = this.lanczosCreate(this.lanczosLobes), scale = this.tempScale, + filterWindow = this.getFilterWindow(), taps = new Array(filterWindow); + for (var i = 1; i <= filterWindow; i++) { + taps[i - 1] = lobeFunction(i * scale); + } + return taps; + }, + + /** + * Generate vertex and shader sources from the necessary steps numbers + * @param {Number} filterWindow + */ + generateShader: function(filterWindow) { + var offsets = new Array(filterWindow), + fragmentShader = this.fragmentSourceTOP, filterWindow; + + for (var i = 1; i <= filterWindow; i++) { + offsets[i - 1] = i + '.0 * uDelta'; + } + + fragmentShader += 'uniform float uTaps[' + filterWindow + '];\n'; + fragmentShader += 'void main() {\n'; + fragmentShader += ' vec4 color = texture2D(uTexture, vTexCoord);\n'; + fragmentShader += ' float sum = 1.0;\n'; + + offsets.forEach(function(offset, i) { + fragmentShader += ' color += texture2D(uTexture, vTexCoord + ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' color += texture2D(uTexture, vTexCoord - ' + offset + ') * uTaps[' + i + '];\n'; + fragmentShader += ' sum += 2.0 * uTaps[' + i + '];\n'; + }); + fragmentShader += ' gl_FragColor = color / sum;\n'; + fragmentShader += '}'; + return fragmentShader; + }, + + fragmentSourceTOP: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n', + + /** + * Apply the resize filter to the image + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + if (options.webgl) { + options.passes++; + this.width = options.sourceWidth; + this.horizontal = true; + this.dW = Math.round(this.width * this.scaleX); + this.dH = options.sourceHeight; + this.tempScale = this.dW / this.width; + this.taps = this.getTaps(); + options.destinationWidth = this.dW; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceWidth = options.destinationWidth; + + this.height = options.sourceHeight; + this.horizontal = false; + this.dH = Math.round(this.height * this.scaleY); + this.tempScale = this.dH / this.height; + this.taps = this.getTaps(); + options.destinationHeight = this.dH; + this._setupFrameBuffer(options); + this.applyToWebGL(options); + this._swapTextures(options); + options.sourceHeight = options.destinationHeight; + } + else { + this.applyTo2d(options); + } + }, + + isNeutralState: function() { + return this.scaleX === 1 && this.scaleY === 1; + }, + + lanczosCreate: function(lobes) { + return function(x) { + if (x >= lobes || x <= -lobes) { + return 0.0; + } + if (x < 1.19209290E-07 && x > -1.19209290E-07) { + return 1.0; + } + x *= Math.PI; + var xx = x / lobes; + return (sin(x) / x) * sin(xx) / xx; + }; + }, + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Resize.prototype + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} scaleX + * @param {Number} scaleY + */ + applyTo2d: function(options) { + var imageData = options.imageData, + scaleX = this.scaleX, + scaleY = this.scaleY; + + this.rcpScaleX = 1 / scaleX; + this.rcpScaleY = 1 / scaleY; + + var oW = imageData.width, oH = imageData.height, + dW = round(oW * scaleX), dH = round(oH * scaleY), + newData; + + if (this.resizeType === 'sliceHack') { + newData = this.sliceByTwo(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'hermite') { + newData = this.hermiteFastResize(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'bilinear') { + newData = this.bilinearFiltering(options, oW, oH, dW, dH); + } + else if (this.resizeType === 'lanczos') { + newData = this.lanczosResize(options, oW, oH, dW, dH); + } + options.imageData = newData; + }, + + /** + * Filter sliceByTwo + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + sliceByTwo: function(options, oW, oH, dW, dH) { + var imageData = options.imageData, + mult = 0.5, doneW = false, doneH = false, stepW = oW * mult, + stepH = oH * mult, resources = fabric.filterBackend.resources, + tmpCanvas, ctx, sX = 0, sY = 0, dX = oW, dY = 0; + if (!resources.sliceByTwo) { + resources.sliceByTwo = document.createElement('canvas'); + } + tmpCanvas = resources.sliceByTwo; + if (tmpCanvas.width < oW * 1.5 || tmpCanvas.height < oH) { + tmpCanvas.width = oW * 1.5; + tmpCanvas.height = oH; + } + ctx = tmpCanvas.getContext('2d'); + ctx.clearRect(0, 0, oW * 1.5, oH); + ctx.putImageData(imageData, 0, 0); + + dW = floor(dW); + dH = floor(dH); + + while (!doneW || !doneH) { + oW = stepW; + oH = stepH; + if (dW < floor(stepW * mult)) { + stepW = floor(stepW * mult); + } + else { + stepW = dW; + doneW = true; + } + if (dH < floor(stepH * mult)) { + stepH = floor(stepH * mult); + } + else { + stepH = dH; + doneH = true; + } + ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH); + sX = dX; + sY = dY; + dY += stepH; + } + return ctx.getImageData(sX, sY, dW, dH); + }, + + /** + * Filter lanczosResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + lanczosResize: function(options, oW, oH, dW, dH) { + + function process(u) { + var v, i, weight, idx, a, red, green, + blue, alpha, fX, fY; + center.x = (u + 0.5) * ratioX; + icenter.x = floor(center.x); + for (v = 0; v < dH; v++) { + center.y = (v + 0.5) * ratioY; + icenter.y = floor(center.y); + a = 0; red = 0; green = 0; blue = 0; alpha = 0; + for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { + if (i < 0 || i >= oW) { + continue; + } + fX = floor(1000 * abs(i - center.x)); + if (!cacheLanc[fX]) { + cacheLanc[fX] = { }; + } + for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { + if (j < 0 || j >= oH) { + continue; + } + fY = floor(1000 * abs(j - center.y)); + if (!cacheLanc[fX][fY]) { + cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000); + } + weight = cacheLanc[fX][fY]; + if (weight > 0) { + idx = (j * oW + i) * 4; + a += weight; + red += weight * srcData[idx]; + green += weight * srcData[idx + 1]; + blue += weight * srcData[idx + 2]; + alpha += weight * srcData[idx + 3]; + } + } + } + idx = (v * dW + u) * 4; + destData[idx] = red / a; + destData[idx + 1] = green / a; + destData[idx + 2] = blue / a; + destData[idx + 3] = alpha / a; + } + + if (++u < dW) { + return process(u); + } + else { + return destImg; + } + } + + var srcData = options.imageData.data, + destImg = options.ctx.createImageData(dW, dH), + destData = destImg.data, + lanczos = this.lanczosCreate(this.lanczosLobes), + ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, + rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, + range2X = ceil(ratioX * this.lanczosLobes / 2), + range2Y = ceil(ratioY * this.lanczosLobes / 2), + cacheLanc = { }, center = { }, icenter = { }; + + return process(0); + }, + + /** + * bilinearFiltering + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + bilinearFiltering: function(options, oW, oH, dW, dH) { + var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl, + color, offset = 0, origPix, ratioX = this.rcpScaleX, + ratioY = this.rcpScaleY, + w4 = 4 * (oW - 1), img = options.imageData, + pixels = img.data, destImage = options.ctx.createImageData(dW, dH), + destPixels = destImage.data; + for (i = 0; i < dH; i++) { + for (j = 0; j < dW; j++) { + x = floor(ratioX * j); + y = floor(ratioY * i); + xDiff = ratioX * j - x; + yDiff = ratioY * i - y; + origPix = 4 * (y * oW + x); + + for (chnl = 0; chnl < 4; chnl++) { + a = pixels[origPix + chnl]; + b = pixels[origPix + 4 + chnl]; + c = pixels[origPix + w4 + chnl]; + d = pixels[origPix + w4 + 4 + chnl]; + color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + + c * yDiff * (1 - xDiff) + d * xDiff * yDiff; + destPixels[offset++] = color; + } + } + } + return destImage; + }, + + /** + * hermiteFastResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + hermiteFastResize: function(options, oW, oH, dW, dH) { + var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, + ratioWHalf = ceil(ratioW / 2), + ratioHHalf = ceil(ratioH / 2), + img = options.imageData, data = img.data, + img2 = options.ctx.createImageData(dW, dH), data2 = img2.data; + for (var j = 0; j < dH; j++) { + for (var i = 0; i < dW; i++) { + var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0, + gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH; + for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { + var dy = abs(centerY - (yy + 0.5)) / ratioHHalf, + centerX = (i + 0.5) * ratioW, w0 = dy * dy; + for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { + var dx = abs(centerX - (xx + 0.5)) / ratioWHalf, + w = sqrt(w0 + dx * dx); + /* eslint-disable max-depth */ + if (w > 1 && w < -1) { + continue; + } + //hermite filter + weight = 2 * w * w * w - 3 * w * w + 1; + if (weight > 0) { + dx = 4 * (xx + yy * oW); + //alpha + gxA += weight * data[dx + 3]; + weightsAlpha += weight; + //colors + if (data[dx + 3] < 255) { + weight = weight * data[dx + 3] / 250; + } + gxR += weight * data[dx]; + gxG += weight * data[dx + 1]; + gxB += weight * data[dx + 2]; + weights += weight; + } + /* eslint-enable max-depth */ + } + } + data2[x2] = gxR / weights; + data2[x2 + 1] = gxG / weights; + data2[x2 + 2] = gxB / weights; + data2[x2 + 3] = gxA / weightsAlpha; + } + } + return img2; + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + scaleX: this.scaleX, + scaleY: this.scaleY, + resizeType: this.resizeType, + lanczosLobes: this.lanczosLobes + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize + */ + fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Contrast filter class + * @class fabric.Image.filters.Contrast + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Contrast#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Contrast({ + * contrast: 40 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Contrast', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uContrast;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\n' + + 'color.rgb = contrastF * (color.rgb - 0.5) + 0.5;\n' + + 'gl_FragColor = color;\n' + + '}', + + contrast: 0, + + mainParameter: 'contrast', + + /** + * Constructor + * @memberOf fabric.Image.filters.Contrast.prototype + * @param {Object} [options] Options object + * @param {Number} [options.contrast=0] Value to contrast the image up (-1...1) + */ + + /** + * Apply the Contrast operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + if (this.contrast === 0) { + return; + } + var imageData = options.imageData, i, len, + data = imageData.data, len = data.length, + contrast = Math.floor(this.contrast * 255), + contrastF = 259 * (contrast + 255) / (255 * (259 - contrast)); + + for (i = 0; i < len; i += 4) { + data[i] = contrastF * (data[i] - 128) + 128; + data[i + 1] = contrastF * (data[i + 1] - 128) + 128; + data[i + 2] = contrastF * (data[i + 2] - 128) + 128; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uContrast: gl.getUniformLocation(program, 'uContrast'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uContrast, this.contrast); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast + */ + fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Saturate filter class + * @class fabric.Image.filters.Saturation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Saturation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Saturation({ + * saturation: 100 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Saturation = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Saturation', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uSaturation;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float rgMax = max(color.r, color.g);\n' + + 'float rgbMax = max(rgMax, color.b);\n' + + 'color.r += rgbMax != color.r ? (rgbMax - color.r) * uSaturation : 0.00;\n' + + 'color.g += rgbMax != color.g ? (rgbMax - color.g) * uSaturation : 0.00;\n' + + 'color.b += rgbMax != color.b ? (rgbMax - color.b) * uSaturation : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + saturation: 0, + + mainParameter: 'saturation', + + /** + * Constructor + * @memberOf fabric.Image.filters.Saturate.prototype + * @param {Object} [options] Options object + * @param {Number} [options.saturate=0] Value to saturate the image (-1...1) + */ + + /** + * Apply the Saturation operation to a Uint8ClampedArray representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8ClampedArray to be filtered. + */ + applyTo2d: function(options) { + if (this.saturation === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.saturation, i, max; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + data[i] += max !== data[i] ? (max - data[i]) * adjust : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * adjust : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * adjust : 0; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uSaturation: gl.getUniformLocation(program, 'uSaturation'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform1f(uniformLocations.uSaturation, -this.saturation); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate + */ + fabric.Image.filters.Saturation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Blur filter class + * @class fabric.Image.filters.Blur + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Blur#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Blur({ + * blur: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + * canvas.renderAll(); + */ + filters.Blur = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blur.prototype */ { + + type: 'Blur', + + /* +'gl_FragColor = vec4(0.0);', +'gl_FragColor += texture2D(texture, vTexCoord + -7 * uDelta)*0.0044299121055113265;', +'gl_FragColor += texture2D(texture, vTexCoord + -6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + -5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + -4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + -3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + -2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + -1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord )*0.159576912161;', +'gl_FragColor += texture2D(texture, vTexCoord + 1 * uDelta)*0.147308056121;', +'gl_FragColor += texture2D(texture, vTexCoord + 2 * uDelta)*0.115876621105;', +'gl_FragColor += texture2D(texture, vTexCoord + 3 * uDelta)*0.0776744219933;', +'gl_FragColor += texture2D(texture, vTexCoord + 4 * uDelta)*0.0443683338718;', +'gl_FragColor += texture2D(texture, vTexCoord + 5 * uDelta)*0.0215963866053;', +'gl_FragColor += texture2D(texture, vTexCoord + 6 * uDelta)*0.00895781211794;', +'gl_FragColor += texture2D(texture, vTexCoord + 7 * uDelta)*0.0044299121055113265;', +*/ + + /* eslint-disable max-len */ + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec2 uDelta;\n' + + 'varying vec2 vTexCoord;\n' + + 'const float nSamples = 15.0;\n' + + 'vec3 v3offset = vec3(12.9898, 78.233, 151.7182);\n' + + 'float random(vec3 scale) {\n' + + /* use the fragment position for a different seed per-pixel */ + 'return fract(sin(dot(gl_FragCoord.xyz, scale)) * 43758.5453);\n' + + '}\n' + + 'void main() {\n' + + 'vec4 color = vec4(0.0);\n' + + 'float total = 0.0;\n' + + 'float offset = random(v3offset);\n' + + 'for (float t = -nSamples; t <= nSamples; t++) {\n' + + 'float percent = (t + offset - 0.5) / nSamples;\n' + + 'float weight = 1.0 - abs(percent);\n' + + 'color += texture2D(uTexture, vTexCoord + uDelta * percent) * weight;\n' + + 'total += weight;\n' + + '}\n' + + 'gl_FragColor = color / total;\n' + + '}', + /* eslint-enable max-len */ + + /** + * blur value, in percentage of image dimensions. + * specific to keep the image blur constant at different resolutions + * range bewteen 0 and 1. + */ + blur: 0, + + mainParameter: 'blur', + + applyTo: function(options) { + if (options.webgl) { + // this aspectRatio is used to give the same blur to vertical and horizontal + this.aspectRatio = options.sourceWidth / options.sourceHeight; + options.passes++; + this._setupFrameBuffer(options); + this.horizontal = true; + this.applyToWebGL(options); + this._swapTextures(options); + this._setupFrameBuffer(options); + this.horizontal = false; + this.applyToWebGL(options); + this._swapTextures(options); + } + else { + this.applyTo2d(options); + } + }, + + applyTo2d: function(options) { + // paint canvasEl with current image data. + //options.ctx.putImageData(options.imageData, 0, 0); + options.imageData = this.simpleBlur(options); + }, + + simpleBlur: function(options) { + var resources = options.filterBackend.resources, canvas1, canvas2, + width = options.imageData.width, + height = options.imageData.height; + + if (!resources.blurLayer1) { + resources.blurLayer1 = fabric.util.createCanvasElement(); + resources.blurLayer2 = fabric.util.createCanvasElement(); + } + canvas1 = resources.blurLayer1; + canvas2 = resources.blurLayer2; + if (canvas1.width !== width || canvas1.height !== height) { + canvas2.width = canvas1.width = width; + canvas2.height = canvas1.height = height; + } + var ctx1 = canvas1.getContext('2d'), + ctx2 = canvas2.getContext('2d'), + nSamples = 15, + random, percent, j, i, + blur = this.blur * 0.06 * 0.5; + + // load first canvas + ctx1.putImageData(options.imageData, 0, 0); + ctx2.clearRect(0, 0, width, height); + + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * width + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, j, random); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + for (i = -nSamples; i <= nSamples; i++) { + random = (Math.random() - 0.5) / 4; + percent = i / nSamples; + j = blur * percent * height + random; + ctx2.globalAlpha = 1 - Math.abs(percent); + ctx2.drawImage(canvas1, random, j); + ctx1.drawImage(canvas2, 0, 0); + ctx2.globalAlpha = 1; + ctx2.clearRect(0, 0, canvas2.width, canvas2.height); + } + options.ctx.drawImage(canvas1, 0, 0); + var newImageData = options.ctx.getImageData(0, 0, canvas1.width, canvas1.height); + ctx1.globalAlpha = 1; + ctx1.clearRect(0, 0, canvas1.width, canvas1.height); + return newImageData; + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + delta: gl.getUniformLocation(program, 'uDelta'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + var delta = this.chooseRightDelta(); + gl.uniform2fv(uniformLocations.delta, delta); + }, + + /** + * choose right value of image percentage to blur with + * @returns {Array} a numeric array with delta values + */ + chooseRightDelta: function() { + var blurScale = 1, delta = [0, 0], blur; + if (this.horizontal) { + if (this.aspectRatio > 1) { + // image is wide, i want to shrink radius horizontal + blurScale = 1 / this.aspectRatio; + } + } + else { + if (this.aspectRatio < 1) { + // image is tall, i want to shrink radius vertical + blurScale = this.aspectRatio; + } + } + blur = blurScale * this.blur * 0.12; + if (this.horizontal) { + delta[0] = blur; + } + else { + delta[1] = blur; + } + return delta; + }, + }); + + /** + * Deserialize a JSON definition of a BlurFilter into a concrete instance. + */ + filters.Blur.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Gamma filter class + * @class fabric.Image.filters.Gamma + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Gamma#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Gamma({ + * brightness: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Gamma = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Gamma.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Gamma', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform vec3 uGamma;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'vec3 correction = (1.0 / uGamma);\n' + + 'color.r = pow(color.r, correction.r);\n' + + 'color.g = pow(color.g, correction.g);\n' + + 'color.b = pow(color.b, correction.b);\n' + + 'gl_FragColor = color;\n' + + 'gl_FragColor.rgb *= color.a;\n' + + '}', + + /** + * Gamma array value, from 0.01 to 2.2. + * @param {Array} gamma + * @default + */ + gamma: [1, 1, 1], + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'gamma', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.gamma = [1, 1, 1]; + filters.BaseFilter.prototype.initialize.call(this, options); + }, + + /** + * Apply the Gamma operation to a Uint8Array representing the pixels of an image. + * + * @param {Object} options + * @param {ImageData} options.imageData The Uint8Array to be filtered. + */ + applyTo2d: function(options) { + var imageData = options.imageData, data = imageData.data, + gamma = this.gamma, len = data.length, + rInv = 1 / gamma[0], gInv = 1 / gamma[1], + bInv = 1 / gamma[2], i; + + if (!this.rVals) { + // eslint-disable-next-line + this.rVals = new Uint8Array(256); + // eslint-disable-next-line + this.gVals = new Uint8Array(256); + // eslint-disable-next-line + this.bVals = new Uint8Array(256); + } + + // This is an optimization - pre-compute a look-up table for each color channel + // instead of performing these pow calls for each pixel in the image. + for (i = 0, len = 256; i < len; i++) { + this.rVals[i] = Math.pow(i / 255, rInv) * 255; + this.gVals[i] = Math.pow(i / 255, gInv) * 255; + this.bVals[i] = Math.pow(i / 255, bInv) * 255; + } + for (i = 0, len = data.length; i < len; i += 4) { + data[i] = this.rVals[data[i]]; + data[i + 1] = this.gVals[data[i + 1]]; + data[i + 2] = this.bVals[data[i + 2]]; + } + }, + + /** + * Return WebGL uniform locations for this filter's shader. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {WebGLShaderProgram} program This filter's compiled shader program. + */ + getUniformLocations: function(gl, program) { + return { + uGamma: gl.getUniformLocation(program, 'uGamma'), + }; + }, + + /** + * Send data from this filter to its shader program's uniforms. + * + * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. + * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects + */ + sendUniformData: function(gl, uniformLocations) { + gl.uniform3fv(uniformLocations.uGamma, this.gamma); + }, + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Gamma} Instance of fabric.Image.filters.Gamma + */ + fabric.Image.filters.Gamma.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * A container class that knows how to apply a sequence of filters to an input image. + */ + filters.Composed = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Composed.prototype */ { + + type: 'Composed', + + /** + * A non sparse array of filters to apply + */ + subFilters: [], + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + this.callSuper('initialize', options); + // create a new array instead mutating the prototype with push + this.subFilters = this.subFilters.slice(0); + }, + + /** + * Apply this container's filters to the input image provided. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be applied. + */ + applyTo: function(options) { + options.passes += this.subFilters.length - 1; + this.subFilters.forEach(function(filter) { + filter.applyTo(options); + }); + }, + + /** + * Serialize this filter into JSON. + * + * @returns {Object} A JSON representation of this filter. + */ + toObject: function() { + return fabric.util.object.extend(this.callSuper('toObject'), { + subFilters: this.subFilters.map(function(filter) { return filter.toObject(); }), + }); + }, + + isNeutralState: function() { + return !this.subFilters.some(function(filter) { return !filter.isNeutralState(); }); + } + }); + + /** + * Deserialize a JSON definition of a ComposedFilter into a concrete instance. + */ + fabric.Image.filters.Composed.fromObject = function(object, callback) { + var filters = object.subFilters || [], + subFilters = filters.map(function(filter) { + return new fabric.Image.filters[filter.type](filter); + }), + instance = new fabric.Image.filters.Composed({ subFilters: subFilters }); + callback && callback(instance); + return instance; + }; +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * HueRotation filter class + * @class fabric.Image.filters.HueRotation + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.HueRotation#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.HueRotation({ + * rotation: -0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.HueRotation = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.HueRotation.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'HueRotation', + + /** + * HueRotation value, from -1 to 1. + * the unit is radians + * @param {Number} myParameter + * @default + */ + rotation: 0, + + /** + * Describe the property that is the filter parameter + * @param {String} m + * @default + */ + mainParameter: 'rotation', + + calculateMatrix: function() { + var rad = this.rotation * Math.PI, cos = fabric.util.cos(rad), sin = fabric.util.sin(rad), + aThird = 1 / 3, aThirdSqtSin = Math.sqrt(aThird) * sin, OneMinusCos = 1 - cos; + this.matrix = [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ]; + this.matrix[0] = cos + OneMinusCos / 3; + this.matrix[1] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[2] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[5] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[6] = cos + aThird * OneMinusCos; + this.matrix[7] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[10] = aThird * OneMinusCos - aThirdSqtSin; + this.matrix[11] = aThird * OneMinusCos + aThirdSqtSin; + this.matrix[12] = cos + aThird * OneMinusCos; + }, + + /** + * HueRotation isNeutralState implementation + * Used only in image applyFilters to discard filters that will not have an effect + * on the image + * @param {Object} options + **/ + isNeutralState: function(options) { + this.calculateMatrix(); + return filters.BaseFilter.prototype.isNeutralState.call(this, options); + }, + + /** + * Apply this filter to the input image data provided. + * + * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. + * + * @param {Object} options + * @param {Number} options.passes The number of filters remaining to be executed + * @param {Boolean} options.webgl Whether to use webgl to render the filter. + * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. + * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. + * @param {WebGLRenderingContext} options.context The GL context used for rendering. + * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. + */ + applyTo: function(options) { + this.calculateMatrix(); + filters.BaseFilter.prototype.applyTo.call(this, options); + }, + + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.HueRotation} Instance of fabric.Image.filters.HueRotation + */ + fabric.Image.filters.HueRotation.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + clone = fabric.util.object.clone; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + + /** + * Text class + * @class fabric.Text + * @extends fabric.Object + * @return {fabric.Text} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} + * @see {@link fabric.Text#initialize} for constructor definition + */ + fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { + + /** + * Properties which when set cause object to change dimensions + * @type Array + * @private + */ + _dimensionAffectingProps: [ + 'fontSize', + 'fontWeight', + 'fontFamily', + 'fontStyle', + 'lineHeight', + 'text', + 'charSpacing', + 'textAlign', + 'styles', + ], + + /** + * @private + */ + _reNewline: /\r?\n/, + + /** + * Use this regular expression to filter for whitespaces that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpacesAndTabs: /[ \t\r]/g, + + /** + * Use this regular expression to filter for whitespace that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpaceAndTab: /[ \t\r]/, + + /** + * Use this regular expression to filter consecutive groups of non spaces. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reWords: /\S+/g, + + /** + * Type of an object + * @type String + * @default + */ + type: 'text', + + /** + * Font size (in pixels) + * @type Number + * @default + */ + fontSize: 40, + + /** + * Font weight (e.g. bold, normal, 400, 600, 800) + * @type {(Number|String)} + * @default + */ + fontWeight: 'normal', + + /** + * Font family + * @type String + * @default + */ + fontFamily: 'Times New Roman', + + /** + * Text decoration underline. + * @type Boolean + * @default + */ + underline: false, + + /** + * Text decoration overline. + * @type Boolean + * @default + */ + overline: false, + + /** + * Text decoration linethrough. + * @type Boolean + * @default + */ + linethrough: false, + + /** + * Text alignment. Possible values: "left", "center", "right", "justify", + * "justify-left", "justify-center" or "justify-right". + * @type String + * @default + */ + textAlign: 'left', + + /** + * Font style . Possible values: "", "normal", "italic" or "oblique". + * @type String + * @default + */ + fontStyle: 'normal', + + /** + * Line height + * @type Number + * @default + */ + lineHeight: 1.16, + + /** + * Superscript schema object (minimum overlap) + * @type {Object} + * @default + */ + superscript: { + size: 0.60, // fontSize factor + baseline: -0.35 // baseline-shift factor (upwards) + }, + + /** + * Subscript schema object (minimum overlap) + * @type {Object} + * @default + */ + subscript: { + size: 0.60, // fontSize factor + baseline: 0.11 // baseline-shift factor (downwards) + }, + + /** + * Background color of text lines + * @type String + * @default + */ + textBackgroundColor: '', + + /** + * List of properties to consider when checking if + * state of an object is changed ({@link fabric.Object#hasStateChanged}) + * as well as for history (undo/redo) purposes + * @type Array + */ + stateProperties: fabric.Object.prototype.stateProperties.concat('fontFamily', + 'fontWeight', + 'fontSize', + 'text', + 'underline', + 'overline', + 'linethrough', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'textBackgroundColor', + 'charSpacing', + 'styles'), + + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: fabric.Object.prototype.cacheProperties.concat('fontFamily', + 'fontWeight', + 'fontSize', + 'text', + 'underline', + 'overline', + 'linethrough', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'textBackgroundColor', + 'charSpacing', + 'styles'), + + /** + * When defined, an object is rendered via stroke and this property specifies its color. + * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 + * @type String + * @default + */ + stroke: null, + + /** + * Shadow object representing shadow of this shape. + * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 + * @type fabric.Shadow + * @default + */ + shadow: null, + + /** + * @private + */ + _fontSizeFraction: 0.222, + + /** + * @private + */ + offsets: { + underline: 0.10, + linethrough: -0.315, + overline: -0.88 + }, + + /** + * Text Line proportion to font Size (in pixels) + * @type Number + * @default + */ + _fontSizeMult: 1.13, + + /** + * additional space between characters + * expressed in thousands of em unit + * @type Number + * @default + */ + charSpacing: 0, + + /** + * Object containing character styles - top-level properties -> line numbers, + * 2nd-level properties - charater numbers + * @type Object + * @default + */ + styles: null, + + /** + * Reference to a context to measure text char or couple of chars + * the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas + * once created it will be referenced on fabric._measuringContext to avoide creating a canvas for every + * text object created. + * @type {CanvasRenderingContext2D} + * @default + */ + _measuringContext: null, + + /** + * Baseline shift, stlyes only, keep at 0 for the main text object + * @type {Number} + * @default + */ + deltaY: 0, + + /** + * Array of properties that define a style unit (of 'styles'). + * @type {Array} + * @default + */ + _styleProperties: [ + 'stroke', + 'strokeWidth', + 'fill', + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'underline', + 'overline', + 'linethrough', + 'deltaY', + 'textBackgroundColor', + ], + + /** + * contains characters bounding boxes + */ + __charBounds: [], + + /** + * use this size when measuring text. To avoid IE11 rounding errors + * @type {Number} + * @default + * @readonly + * @private + */ + CACHE_FONT_SIZE: 400, + + /** + * contains the min text width to avoid getting 0 + * @type {Number} + * @default + */ + MIN_TEXT_WIDTH: 2, + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.text = text; + this.__skipDimension = true; + this.callSuper('initialize', options); + this.__skipDimension = false; + this.initDimensions(); + this.setCoords(); + this.setupState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Return a contex for measurement of text string. + * if created it gets stored for reuse + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + getMeasuringContext: function() { + // if we did not return we have to measure something. + if (!fabric._measuringContext) { + fabric._measuringContext = this.canvas && this.canvas.contextCache || + fabric.util.createCanvasElement().getContext('2d'); + } + return fabric._measuringContext; + }, + + /** + * @private + * Divides text into lines of text and lines of graphemes. + */ + _splitText: function() { + var newLines = this._splitTextIntoLines(this.text); + this.textLines = newLines.lines; + this._textLines = newLines.graphemeLines; + this._unwrappedTextLines = newLines._unwrappedLines; + this._text = newLines.graphemeText; + return newLines; + }, + + /** + * Initialize or update text dimensions. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + */ + initDimensions: function() { + if (this.__skipDimension) { + return; + } + this._splitText(); + this._clearCache(); + this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH; + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + this.height = this.calcTextHeight(); + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Enlarge space boxes and shift the others + */ + enlargeSpaces: function() { + var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) { + continue; + } + accumulatedSpace = 0; + line = this._textLines[i]; + currentLineWidth = this.getLineWidth(i); + if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { + numberOfSpaces = spaces.length; + diffSpace = (this.width - currentLineWidth) / numberOfSpaces; + for (var j = 0, jlen = line.length; j <= jlen; j++) { + charBound = this.__charBounds[i][j]; + if (this._reSpaceAndTab.test(line[j])) { + charBound.width += diffSpace; + charBound.kernedWidth += diffSpace; + charBound.left += accumulatedSpace; + accumulatedSpace += diffSpace; + } + else { + charBound.left += accumulatedSpace; + } + } + } + } + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + return lineIndex === this._textLines.length - 1; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * It return always for text and Itext. + * @return Number + */ + missingNewlineOffset: function() { + return 1; + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * 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 + * + *