diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1afd550dd..9f034de65bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [3.3.0] +- Differently support multi mouse events, fix multi touch on various browser [#5785](https://github.com/fabricjs/fabric.js/pull/5785) +- Word boundary search update on grapheme clusters [#5788](https://github.com/fabricjs/fabric.js/pull/5788) +- Enable deps free version [#5786](https://github.com/fabricjs/fabric.js/pull/5786) +- Remove variables named as reserved words [#5782](https://github.com/fabricjs/fabric.js/pull/5782) + ## [3.2.0] - Fix: Better handling of upperCanvas in toCanvasElement. [#5736](https://github.com/fabricjs/fabric.js/pull/5736) - Add: Pass raw event information to brushes [#5687](https://github.com/fabricjs/fabric.js/pull/5687) diff --git a/HEADER.js b/HEADER.js index b2cee5a0699..ede6c4a5210 100644 --- a/HEADER.js +++ b/HEADER.js @@ -1,6 +1,6 @@ /*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: '3.2.0' }; +var fabric = fabric || { version: '3.3.0' }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } diff --git a/dist/fabric.js b/dist/fabric.js index 69d6a80801e..b232fcd53c3 100644 --- a/dist/fabric.js +++ b/dist/fabric.js @@ -1,7 +1,7 @@ /* build: `node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs` */ /*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: '3.2.0' }; +var fabric = fabric || { version: '3.3.0' }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } @@ -2142,170 +2142,8 @@ fabric.CommonMethods = { (function () { - - var unknown = 'unknown'; - - /* EVENT HANDLING */ - - function areHostMethods(object) { - var methodNames = Array.prototype.slice.call(arguments, 1), - t, i, len = methodNames.length; - for (i = 0; i < len; i++) { - t = typeof object[methodNames[i]]; - if (!(/^(?:function|object|unknown)$/).test(t)) { - return false; - } - } - return true; - } - - /** @ignore */ - var getElement, - setElement, - getUniqueId = (function () { - var uid = 0; - return function (element) { - return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); - }; - })(); - - (function () { - var elements = { }; - /** @ignore */ - getElement = function (uid) { - return elements[uid]; - }; - /** @ignore */ - setElement = function (uid, element) { - elements[uid] = element; - }; - })(); - - function createListener(uid, handler) { - return { - handler: handler, - wrappedHandler: createWrappedHandler(uid, handler) - }; - } - - function createWrappedHandler(uid, handler) { - return function (e) { - handler.call(getElement(uid), e || fabric.window.event); - }; - } - - function createDispatcher(uid, eventName) { - return function (e) { - if (handlers[uid] && handlers[uid][eventName]) { - var handlersForEvent = handlers[uid][eventName]; - for (var i = 0, len = handlersForEvent.length; i < len; i++) { - handlersForEvent[i].call(this, e || fabric.window.event); - } - } - }; - } - - var shouldUseAddListenerRemoveListener = ( - areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && - areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), - - shouldUseAttachEventDetachEvent = ( - areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && - areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), - - // IE branch - listeners = { }, - - // DOM L0 branch - handlers = { }, - - addListener, removeListener; - - if (shouldUseAddListenerRemoveListener) { - /** @ignore */ - addListener = function (element, eventName, handler, options) { - // since ie10 or ie9 can use addEventListener but they do not support options, i need to check - element && element.addEventListener(eventName, handler, shouldUseAttachEventDetachEvent ? false : options); - }; - /** @ignore */ - removeListener = function (element, eventName, handler, options) { - element && element.removeEventListener(eventName, handler, shouldUseAttachEventDetachEvent ? false : options); - }; - } - - else if (shouldUseAttachEventDetachEvent) { - /** @ignore */ - addListener = function (element, eventName, handler) { - if (!element) { - return; - } - var uid = getUniqueId(element); - setElement(uid, element); - if (!listeners[uid]) { - listeners[uid] = { }; - } - if (!listeners[uid][eventName]) { - listeners[uid][eventName] = []; - - } - var listener = createListener(uid, handler); - listeners[uid][eventName].push(listener); - element.attachEvent('on' + eventName, listener.wrappedHandler); - }; - /** @ignore */ - removeListener = function (element, eventName, handler) { - if (!element) { - return; - } - var uid = getUniqueId(element), listener; - if (listeners[uid] && listeners[uid][eventName]) { - for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) { - listener = listeners[uid][eventName][i]; - if (listener && listener.handler === handler) { - element.detachEvent('on' + eventName, listener.wrappedHandler); - listeners[uid][eventName][i] = null; - } - } - } - }; - } - else { - /** @ignore */ - addListener = function (element, eventName, handler) { - if (!element) { - return; - } - var uid = getUniqueId(element); - if (!handlers[uid]) { - handlers[uid] = { }; - } - if (!handlers[uid][eventName]) { - handlers[uid][eventName] = []; - var existingHandler = element['on' + eventName]; - if (existingHandler) { - handlers[uid][eventName].push(existingHandler); - } - element['on' + eventName] = createDispatcher(uid, eventName); - } - handlers[uid][eventName].push(handler); - }; - /** @ignore */ - removeListener = function (element, eventName, handler) { - if (!element) { - return; - } - var uid = getUniqueId(element); - if (handlers[uid] && handlers[uid][eventName]) { - var handlersForEvent = handlers[uid][eventName]; - for (var i = 0, len = handlersForEvent.length; i < len; i++) { - if (handlersForEvent[i] === handler) { - handlersForEvent.splice(i, 1); - } - } - } - }; - } - + // 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 @@ -2314,7 +2152,9 @@ fabric.CommonMethods = { * @param {String} eventName * @param {Function} handler */ - fabric.util.addListener = addListener; + fabric.util.addListener = function(element, eventName, handler, options) { + element && element.addEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; /** * Removes an event listener from an element @@ -2324,60 +2164,27 @@ fabric.CommonMethods = { * @param {String} eventName * @param {Function} handler */ - fabric.util.removeListener = removeListener; - - /** - * Cross-browser wrapper for getting event's coordinates - * @memberOf fabric.util - * @param {Event} event Event object - */ - function getPointer(event) { - event || (event = fabric.window.event); - - var element = event.target || - (typeof event.srcElement !== unknown ? event.srcElement : null), - - scroll = fabric.util.getScrollLeftTop(element); - return { - x: pointerX(event) + scroll.left, - y: pointerY(event) + scroll.top - }; - } - - var pointerX = function(event) { - return event.clientX; - }, - - pointerY = function(event) { - return event.clientY; - }; - - function _getPointer(event, pageProp, clientProp) { - var touchProp = event.type === 'touchend' ? 'changedTouches' : 'touches'; - var pointer, eventTouchProp = event[touchProp]; - - if (eventTouchProp && eventTouchProp[0]) { - pointer = eventTouchProp[0][clientProp]; - } + fabric.util.removeListener = function(element, eventName, handler, options) { + element && element.removeEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; - if (typeof pointer === 'undefined') { - pointer = event[clientProp]; + function getTouchInfo(event) { + var touchProp = event.changedTouches; + if (touchProp && touchProp[0]) { + return touchProp[0]; } - - return pointer; + return event; } - if (fabric.isTouchSupported) { - pointerX = function(event) { - return _getPointer(event, 'pageX', 'clientX'); - }; - pointerY = function(event) { - return _getPointer(event, 'pageY', 'clientY'); + 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 }; - } - - fabric.util.getPointer = getPointer; - + }; })(); @@ -8655,7 +8462,10 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * Inovoked on mouse down * @param {Object} pointer */ - onMouseDown: function(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) @@ -8667,7 +8477,10 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * Inovoked on mouse move * @param {Object} pointer */ - onMouseMove: function(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 @@ -8693,9 +8506,13 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype /** * Invoked on mouse up */ - onMouseUp: function() { + onMouseUp: function(options) { + if (!this.canvas._isMainEvent(options.e)) { + return true; + } this.oldEnd = undefined; this._finalizeAndAddPath(); + return false; }, /** @@ -10728,7 +10545,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab height: height + 'px', left: 0, top: 0, - 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' + 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', + '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' }); element.width = width; element.height = height; @@ -11042,11 +10860,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab fabric.Canvas[prop] = fabric.StaticCanvas[prop]; } } - - if (fabric.isTouchSupported) { - /** @ignore */ - fabric.Canvas.prototype._setCursorFromEvent = function() { }; - } })(); @@ -11068,7 +10881,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab addEventOptions = { passive: false }; function checkClick(e, value) { - return 'which' in e ? e.which === value : e.button === value - 1; + return e.button && (e.button === value - 1); } fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { @@ -11088,6 +10901,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab '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 @@ -11101,9 +10921,17 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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.enablePointerEvents ? 'pointer' : 'mouse'; + eventTypePrefix = this._getEventPrefix(); functor(fabric.window, 'resize', this._onResize); functor(canvasElement, eventTypePrefix + 'down', this._onMouseDown); functor(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); @@ -11112,12 +10940,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab functor(canvasElement, 'wheel', this._onMouseWheel); functor(canvasElement, 'contextmenu', this._onContextMenu); functor(canvasElement, 'dblclick', this._onDoubleClick); - functor(canvasElement, 'touchstart', this._onMouseDown, addEventOptions); - functor(canvasElement, 'touchmove', this._onMouseMove, addEventOptions); 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); @@ -11133,9 +10962,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab removeListeners: function() { this.addOrRemove(removeListener, 'remove'); // if you dispose on a mouseDown, before mouse up, you need to clean document to... - var eventTypePrefix = this.enablePointerEvents ? 'pointer' : 'mouse'; + var eventTypePrefix = this._getEventPrefix(); removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); - removeListener(fabric.document, 'touchend', this._onMouseUp, addEventOptions); + removeListener(fabric.document, 'touchend', this._onTouchEnd, addEventOptions); removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); }, @@ -11149,8 +10978,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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); @@ -11290,29 +11121,105 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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 */ - _onMouseDown: function (e) { + _onTouchStart: function(e) { + e.preventDefault(); + if (this.mainTouchId === null) { + this.mainTouchId = this.getPointerId(e); + } this.__onMouseDown(e); this._resetTransformEventData(); - addListener(fabric.document, 'touchend', this._onMouseUp, addEventOptions); + 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.enablePointerEvents ? 'pointer' : 'mouse'; + eventTypePrefix = this._getEventPrefix(); removeListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - removeListener(canvasElement, 'touchmove', this._onMouseMove, addEventOptions); + addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); + addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + }, - if (e.type === 'touchstart') { - // Unbind mousedown to prevent double triggers from touch devices - removeListener(canvasElement, eventTypePrefix + 'down', this._onMouseDown); + /** + * @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; } - else { - addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); - addListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); + 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); }, /** @@ -11323,24 +11230,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.__onMouseUp(e); this._resetTransformEventData(); var canvasElement = this.upperCanvasEl, - eventTypePrefix = this.enablePointerEvents ? 'pointer' : 'mouse'; - - removeListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); - removeListener(fabric.document, 'touchend', this._onMouseUp, addEventOptions); - - removeListener(fabric.document, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - removeListener(fabric.document, 'touchmove', this._onMouseMove, addEventOptions); - - addListener(canvasElement, eventTypePrefix + 'move', this._onMouseMove, addEventOptions); - addListener(canvasElement, 'touchmove', this._onMouseMove, addEventOptions); - - if (e.type === 'touchend') { - // Wait 400ms before rebinding mousedown to prevent double triggers - // from touch devices - var _this = this; - setTimeout(function() { - addListener(_this.upperCanvasEl, eventTypePrefix + 'down', _this._onMouseDown); - }, 400); + 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); } }, @@ -11420,6 +11314,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return; } + if (!this._isMainEvent(e)) { + return; + } if (transform) { this._finalizeCurrentTransform(e); shouldRender = transform.actionPerformed; @@ -11608,12 +11505,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mouseup */ _onMouseUpInDrawingMode: function(e) { - this._isCurrentlyDrawing = false; if (this.clipTo) { this.contextTop.restore(); } var pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); + this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); this._handleEvent(e, 'up'); }, @@ -11649,6 +11545,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab return; } + if (!this._isMainEvent(e)) { + return; + } + // ignore if some object is being transformed at this moment if (this._currentTransform) { return; @@ -11748,7 +11648,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._onMouseMoveInDrawingMode(e); return; } - if (typeof e.touches !== 'undefined' && e.touches.length > 1) { + + if (!this._isMainEvent(e)) { return; } @@ -11980,7 +11881,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.setCursor(this.defaultCursor); return false; } - var hoverCursor = target.hoverCursor || this.hoverCursor, activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? this._activeObject : null, @@ -25193,7 +25093,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * possibly overridden to accommodate different measure logic or * to hook some external lib for character measurement * @private - * @param {String} char to be measured + * @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 @@ -25239,12 +25139,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Computes height of character at given position - * @param {Number} line the line number - * @param {Number} char the character number + * @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'); + getHeightOfChar: function(line, _char) { + return this.getValueOfPropertyAt(line, _char, 'fontSize'); }, /** @@ -27071,7 +26971,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { index++; } } - while (/\S/.test(this._text[index]) && index < this.text.length) { + while (/\S/.test(this._text[index]) && index < this._text.length) { offset++; index++; } @@ -27103,7 +27003,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { findLineBoundaryRight: function(startFrom) { var offset = 0, index = startFrom; - while (!/\n/.test(this._text[index]) && index < this.text.length) { + while (!/\n/.test(this._text[index]) && index < this._text.length) { offset++; index++; } @@ -27118,13 +27018,13 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @return {Number} Index of the beginning or end of a word */ searchWordBoundary: function(selectionStart, direction) { - var index = this._reSpace.test(this.text.charAt(selectionStart)) ? selectionStart - 1 : selectionStart, - _char = this.text.charAt(index), + 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) { + while (!reNonWord.test(_char) && index > 0 && index < this._text.length) { index += direction; - _char = this.text.charAt(index); + _char = this._text[index]; } if (reNonWord.test(_char) && _char !== '\n') { index += direction === 1 ? 0 : 1; diff --git a/package.json b/package.json index e619ca55e15..3602667eff1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fabric", "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.", "homepage": "http://fabricjs.com/", - "version": "3.2.0", + "version": "3.3.0", "authors": "Juriy Zaytsev ", "contributors": [ {