diff --git a/src/pretix/static/fabric/fabric.js b/src/pretix/static/fabric/fabric.js index 0e5f8a735..faee7fc67 100644 --- a/src/pretix/static/fabric/fabric.js +++ b/src/pretix/static/fabric/fabric.js @@ -1,52 +1,48 @@ -/* build: `node build.js modules=ALL exclude=json,gestures minifier=uglifyjs` */ - /*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ -/* -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +/* build: `node build.js modules=ALL exclude=gestures,accessors,erasing requirejs minifier=uglifyjs` */ +/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - */ - -var fabric = fabric || { version: "1.7.11" }; +var fabric = fabric || { version: '5.3.0' }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } - +/* _AMD_START_ */ +else if (typeof define === 'function' && define.amd) { + define([], function() { return fabric; }); +} +/* _AMD_END_ */ if (typeof document !== 'undefined' && typeof window !== 'undefined') { - fabric.document = document; + if (document instanceof (typeof HTMLDocument !== 'undefined' ? HTMLDocument : Document)) { + fabric.document = document; + } + else { + fabric.document = document.implementation.createHTMLDocument(''); + } fabric.window = window; - // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) - window.fabric = fabric; } else { // assume we're running under node.js when document/window are not present - fabric.document = require("jsdom") - .jsdom( - decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E") - ); - - if (fabric.document.createWindow) { - fabric.window = fabric.document.createWindow(); - } else { - fabric.window = fabric.document.parentWindow; - } + var jsdom = require('jsdom'); + var virtualWindow = new jsdom.JSDOM( + decodeURIComponent('%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E'), + { + features: { + FetchExternalResources: ['img'] + }, + resources: 'usable' + }).window; + fabric.document = virtualWindow.document; + fabric.jsdomImplForWrapper = require('jsdom/lib/jsdom/living/generated/utils').implForWrapper; + fabric.nodeCanvas = require('jsdom/lib/jsdom/utils').Canvas; + fabric.window = virtualWindow; + DOMParser = fabric.window.DOMParser; } /** * True when in environment that supports touch events * @type boolean */ -fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; +fabric.isTouchSupported = 'ontouchstart' in fabric.window || 'ontouchstart' in fabric.document || + (fabric.window && fabric.window.navigator && fabric.window.navigator.maxTouchPoints > 0); /** * True when in environment that's probably Node.js @@ -61,14 +57,15 @@ fabric.isLikelyNode = typeof Buffer !== 'undefined' && * @type array */ fabric.SHARED_ATTRIBUTES = [ - "display", - "transform", - "fill", "fill-opacity", "fill-rule", - "opacity", - "stroke", "stroke-dasharray", "stroke-linecap", - "stroke-linejoin", "stroke-miterlimit", - "stroke-opacity", "stroke-width", - "id" + '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_ */ @@ -76,15 +73,71 @@ fabric.SHARED_ATTRIBUTES = [ * 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+)(?:e[-+]?\\d+)?)'; +fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)'; +fabric.commaWsp = '(?:\\s+,?\\s*|,\\s*)'; +fabric.rePathCommand = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/ig; +fabric.reNonWord = /[ \n\.,;!\?\-]/; fabric.fontPaths = { }; fabric.iMatrix = [1, 0, 0, 1, 0, 0]; +fabric.svgNS = 'http://www.w3.org/2000/svg'; + +/** + * Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine. + * @since 1.7.14 + * @type Number + * @default + */ +fabric.perfLimitSizeTotal = 2097152; + +/** + * Pixel limit for cache canvases width or height. IE fixes the maximum at 5000 + * @since 1.7.14 + * @type Number + * @default + */ +fabric.maxCacheSideLimit = 4096; + +/** + * Lowest pixel limit for cache canvases, set at 256PX + * @since 1.7.14 + * @type Number + * @default + */ +fabric.minCacheSideLimit = 256; /** * Cache Object for widths of chars in text rendering. */ fabric.charWidthsCache = { }; +/** + * if webgl is enabled and available, textureSize will determine the size + * of the canvas backend + * @since 2.0.0 + * @type Number + * @default + */ +fabric.textureSize = 2048; + +/** + * When 'true', style information is not retained when copy/pasting text, making + * pasted text use destination style. + * Defaults to 'false'. + * @type Boolean + * @default + */ +fabric.disableStyleCopyPaste = false; + +/** + * Enable webgl for filtering picture is available + * A filtering backend will be initialized, this will both take memory and + * time since a default 2048x2048 canvas will be created for the gl context + * @since 2.0.0 + * @type Boolean + * @default + */ +fabric.enableGLFiltering = true; + /** * Device Pixel Ratio * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html @@ -93,6 +146,68 @@ 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 bezier 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() { @@ -117,7 +232,6 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio || /** * 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}) @@ -125,7 +239,7 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio || * @return {Self} thisArg * @chainable */ - function observe(eventName, handler) { + function on(eventName, handler) { if (!this.__eventListeners) { this.__eventListeners = { }; } @@ -144,10 +258,30 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio || return this; } + function _once(eventName, handler) { + var _handler = function () { + handler.apply(this, arguments); + this.off(eventName, _handler); + }.bind(this); + this.on(eventName, _handler); + } + + function once(eventName, handler) { + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + _once.call(this, prop, eventName[prop]); + } + } + else { + _once.call(this, eventName, 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}) @@ -155,9 +289,9 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio || * @return {Self} thisArg * @chainable */ - function stopObserving(eventName, handler) { + function off(eventName, handler) { if (!this.__eventListeners) { - return; + return this; } // remove all key/value pairs (event name -> event handler) @@ -180,9 +314,7 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio || /** * 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 @@ -190,12 +322,12 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio || */ function fire(eventName, options) { if (!this.__eventListeners) { - return; + return this; } var listenersForEvent = this.__eventListeners[eventName]; if (!listenersForEvent) { - return; + return this; } for (var i = 0, len = listenersForEvent.length; i < len; i++) { @@ -213,13 +345,10 @@ fabric.devicePixelRatio = fabric.window.devicePixelRatio || * @see {@link http://fabricjs.com/events|Events demo} */ fabric.Observable = { - observe: observe, - stopObserving: stopObserving, fire: fire, - - on: observe, - off: stopObserving, - trigger: fire + on: on, + once: once, + off: off, }; })(); @@ -236,6 +365,9 @@ fabric.Collection = { * (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 @@ -247,13 +379,16 @@ fabric.Collection = { this._onObjectAdded(arguments[i]); } } - this.renderOnAddRemove && this.renderAll(); + 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 @@ -261,7 +396,7 @@ fabric.Collection = { * @chainable */ insertAt: function (object, index, nonSplicing) { - var objects = this.getObjects(); + var objects = this._objects; if (nonSplicing) { objects[index] = object; } @@ -269,7 +404,7 @@ fabric.Collection = { objects.splice(index, 0, object); } this._onObjectAdded && this._onObjectAdded(object); - this.renderOnAddRemove && this.renderAll(); + this.renderOnAddRemove && this.requestRenderAll(); return this; }, @@ -280,7 +415,7 @@ fabric.Collection = { * @chainable */ remove: function() { - var objects = this.getObjects(), + var objects = this._objects, index, somethingRemoved = false; for (var i = 0, length = arguments.length; i < length; i++) { @@ -294,7 +429,7 @@ fabric.Collection = { } } - this.renderOnAddRemove && somethingRemoved && this.renderAll(); + this.renderOnAddRemove && somethingRemoved && this.requestRenderAll(); return this; }, @@ -321,12 +456,13 @@ fabric.Collection = { /** * 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; + return this._objects.concat(); } return this._objects.filter(function(o) { return o.type === type; @@ -339,7 +475,7 @@ fabric.Collection = { * @return {Self} thisArg */ item: function (index) { - return this.getObjects()[index]; + return this._objects[index]; }, /** @@ -347,7 +483,7 @@ fabric.Collection = { * @return {Boolean} true if collection is empty */ isEmpty: function () { - return this.getObjects().length === 0; + return this._objects.length === 0; }, /** @@ -355,16 +491,25 @@ fabric.Collection = { * @return {Number} Collection size */ size: function() { - return this.getObjects().length; + return this._objects.length; }, /** * Returns true if collection contains an object * @param {Object} object Object to check against + * @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects` * @return {Boolean} `true` if collection contains an object */ - contains: function(object) { - return this.getObjects().indexOf(object) > -1; + contains: function (object, deep) { + if (this._objects.indexOf(object) > -1) { + return true; + } + else if (deep) { + return this._objects.some(function (obj) { + return typeof obj.contains === 'function' && obj.contains(object, true); + }); + } + return false; }, /** @@ -372,7 +517,7 @@ fabric.Collection = { * @return {Number} complexity */ complexity: function () { - return this.getObjects().reduce(function (memo, current) { + return this._objects.reduce(function (memo, current) { memo += current.complexity ? current.complexity() : 0; return memo; }, 0); @@ -421,21 +566,6 @@ fabric.CommonMethods = { } }, - /** - * @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 */ @@ -457,12 +587,7 @@ fabric.CommonMethods = { this._setObject(key); } else { - if (typeof value === 'function' && key !== 'clipTo') { - this._set(key, value(this.get(key))); - } - else { - this._set(key, value); - } + this._set(key, value); } return this; }, @@ -501,14 +626,57 @@ fabric.CommonMethods = { var sqrt = Math.sqrt, atan2 = Math.atan2, pow = Math.pow, - abs = Math.abs, - PiBy180 = Math.PI / 180; + 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` @@ -570,8 +738,8 @@ fabric.CommonMethods = { * @return {fabric.Point} The new rotated point */ rotatePoint: function(point, origin, radians) { - point.subtractEquals(origin); - var v = fabric.util.rotateVector(point, radians); + var newPoint = new fabric.Point(point.x - origin.x, point.y - origin.y), + v = fabric.util.rotateVector(newPoint, radians); return new fabric.Point(v.x, v.y).addEquals(origin); }, @@ -584,8 +752,8 @@ fabric.CommonMethods = { * @return {Object} The new rotated point */ rotateVector: function(vector, radians) { - var sin = Math.sin(radians), - cos = Math.cos(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 { @@ -594,6 +762,135 @@ fabric.CommonMethods = { }; }, + /** + * Creates a vetor from points represented as a point + * @static + * @memberOf fabric.util + * + * @typedef {Object} Point + * @property {number} x + * @property {number} y + * + * @param {Point} from + * @param {Point} to + * @returns {Point} vector + */ + createVector: function (from, to) { + return new fabric.Point(to.x - from.x, to.y - from.y); + }, + + /** + * Calculates angle between 2 vectors using dot product + * @static + * @memberOf fabric.util + * @param {Point} a + * @param {Point} b + * @returns the angle in radian between the vectors + */ + calcAngleBetweenVectors: function (a, b) { + return Math.acos((a.x * b.x + a.y * b.y) / (Math.hypot(a.x, a.y) * Math.hypot(b.x, b.y))); + }, + + /** + * @static + * @memberOf fabric.util + * @param {Point} v + * @returns {Point} vector representing the unit vector of pointing to the direction of `v` + */ + getHatVector: function (v) { + return new fabric.Point(v.x, v.y).multiply(1 / Math.hypot(v.x, v.y)); + }, + + /** + * @static + * @memberOf fabric.util + * @param {Point} A + * @param {Point} B + * @param {Point} C + * @returns {{ vector: Point, angle: number }} vector representing the bisector of A and A's angle + */ + getBisector: function (A, B, C) { + var AB = fabric.util.createVector(A, B), AC = fabric.util.createVector(A, C); + var alpha = fabric.util.calcAngleBetweenVectors(AB, AC); + // check if alpha is relative to AB->BC + var ro = fabric.util.calcAngleBetweenVectors(fabric.util.rotateVector(AB, alpha), AC); + var phi = alpha * (ro === 0 ? 1 : -1) / 2; + return { + vector: fabric.util.getHatVector(fabric.util.rotateVector(AB, phi)), + angle: alpha + }; + }, + + /** + * Project stroke width on points returning 2 projections for each point as follows: + * - `miter`: 2 points corresponding to the outer boundary and the inner boundary of stroke. + * - `bevel`: 2 points corresponding to the bevel boundaries, tangent to the bisector. + * - `round`: same as `bevel` + * Used to calculate object's bounding box + * @static + * @memberOf fabric.util + * @param {Point[]} points + * @param {Object} options + * @param {number} options.strokeWidth + * @param {'miter'|'bevel'|'round'} options.strokeLineJoin + * @param {number} options.strokeMiterLimit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit + * @param {boolean} options.strokeUniform + * @param {number} options.scaleX + * @param {number} options.scaleY + * @param {boolean} [openPath] whether the shape is open or not, affects the calculations of the first and last points + * @returns {fabric.Point[]} array of size 2n/4n of all suspected points + */ + projectStrokeOnPoints: function (points, options, openPath) { + var coords = [], s = options.strokeWidth / 2, + strokeUniformScalar = options.strokeUniform ? + new fabric.Point(1 / options.scaleX, 1 / options.scaleY) : new fabric.Point(1, 1), + getStrokeHatVector = function (v) { + var scalar = s / (Math.hypot(v.x, v.y)); + return new fabric.Point(v.x * scalar * strokeUniformScalar.x, v.y * scalar * strokeUniformScalar.y); + }; + if (points.length <= 1) {return coords;} + points.forEach(function (p, index) { + var A = new fabric.Point(p.x, p.y), B, C; + if (index === 0) { + C = points[index + 1]; + B = openPath ? getStrokeHatVector(fabric.util.createVector(C, A)).addEquals(A) : points[points.length - 1]; + } + else if (index === points.length - 1) { + B = points[index - 1]; + C = openPath ? getStrokeHatVector(fabric.util.createVector(B, A)).addEquals(A) : points[0]; + } + else { + B = points[index - 1]; + C = points[index + 1]; + } + var bisector = fabric.util.getBisector(A, B, C), + bisectorVector = bisector.vector, + alpha = bisector.angle, + scalar, + miterVector; + if (options.strokeLineJoin === 'miter') { + scalar = -s / Math.sin(alpha / 2); + miterVector = new fabric.Point( + bisectorVector.x * scalar * strokeUniformScalar.x, + bisectorVector.y * scalar * strokeUniformScalar.y + ); + if (Math.hypot(miterVector.x, miterVector.y) / s <= options.strokeMiterLimit) { + coords.push(A.add(miterVector)); + coords.push(A.subtract(miterVector)); + return; + } + } + scalar = -s * Math.SQRT2; + miterVector = new fabric.Point( + bisectorVector.x * scalar * strokeUniformScalar.x, + bisectorVector.y * scalar * strokeUniformScalar.y + ); + coords.push(A.add(miterVector)); + coords.push(A.subtract(miterVector)); + }); + return coords; + }, + /** * Apply transform t to point p * @static @@ -619,17 +916,23 @@ fabric.CommonMethods = { /** * 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) { + 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 = Math.abs(minX - maxX), + 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 = Math.abs(minY - maxY); + height = maxY - minY; return { left: minX, @@ -727,6 +1030,33 @@ fabric.CommonMethods = { 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 @@ -766,11 +1096,12 @@ fabric.CommonMethods = { var img = fabric.util.createImage(); /** @ignore */ - img.onload = function () { - callback && callback.call(context, img); + var onLoadCallback = function () { + callback && callback.call(context, img, false); img = img.onload = img.onerror = null; }; + img.onload = onLoadCallback; /** @ignore */ img.onerror = function() { fabric.log('Error loading ' + img.src); @@ -782,13 +1113,50 @@ fabric.CommonMethods = { // 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) { + // crossOrigin null is the same as not set. + if (url.indexOf('data') !== 0 && + crossOrigin !== undefined && + crossOrigin !== null) { 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 @@ -802,16 +1170,18 @@ fabric.CommonMethods = { enlivenObjects: function(objects, callback, namespace, reviver) { objects = objects || []; - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - callback && callback(enlivenedObjects); - } - } - var enlivenedObjects = [], numLoadedObjects = 0, - numTotalObjects = objects.length, - forceAsync = true; + 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); @@ -829,7 +1199,26 @@ fabric.CommonMethods = { error || (enlivenedObjects[index] = obj); reviver && reviver(o, obj, error); onLoaded(); - }, forceAsync); + }); + }); + }, + + /** + * Creates corresponding fabric instances residing in an object, e.g. `clipPath` + * @see {@link fabric.Object.ENLIVEN_PROPS} + * @param {Object} object + * @param {Object} [context] assign enlived props to this object (pass null to skip this) + * @param {(objects:fabric.Object[]) => void} callback + */ + enlivenObjectEnlivables: function (object, context, callback) { + var enlivenProps = fabric.Object.ENLIVEN_PROPS.filter(function (key) { return !!object[key]; }); + fabric.util.enlivenObjects(enlivenProps.map(function (key) { return object[key]; }), function (enlivedProps) { + var objects = {}; + enlivenProps.forEach(function (key, index) { + objects[key] = enlivedProps[index]; + context && (context[key] = enlivedProps[index]); + }); + callback && callback(objects); }); }, @@ -837,10 +1226,8 @@ fabric.CommonMethods = { * Create and wait for loading of patterns * @static * @memberOf fabric.util - * @param {Array} objects Objects to enliven + * @param {Array} patterns 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. */ enlivenPatterns: function(patterns, callback) { @@ -882,13 +1269,29 @@ fabric.CommonMethods = { * @param {Array} elements SVG elements to group * @param {Object} [options] Options object * @param {String} path Value to set sourcePath to - * @return {fabric.Object|fabric.PathGroup} + * @return {fabric.Object|fabric.Group} */ groupSVGElements: function(elements, options, path) { var object; - - object = new fabric.PathGroup(elements, options); - + if (elements && elements.length === 1) { + if (typeof path !== 'undefined') { + elements[0].sourcePath = path; + } + 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; } @@ -901,10 +1304,10 @@ fabric.CommonMethods = { * @memberOf fabric.util * @param {Object} source Source object * @param {Object} destination Destination object - * @return {Array} properties Propertie names to include + * @return {Array} properties Properties names to include */ populateWithProperties: function(source, destination, properties) { - if (properties && Object.prototype.toString.call(properties) === '[object Array]') { + if (properties && Array.isArray(properties)) { for (var i = 0, len = properties.length; i < len; i++) { if (properties[i] in source) { destination[properties[i]] = source[properties[i]]; @@ -914,61 +1317,41 @@ fabric.CommonMethods = { }, /** - * 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 + * Creates canvas element + * @static + * @memberOf fabric.util + * @return {CanvasElement} initialized canvas element */ - 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(); + createCanvasElement: function() { + return fabric.document.createElement('canvas'); }, /** - * Creates canvas element and initializes it via excanvas if necessary + * 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 - * @param {CanvasElement} [canvasEl] optional canvas element to initialize; - * when not given, element is created implicitly * @return {CanvasElement} initialized canvas element */ - createCanvasElement: function(canvasEl) { - canvasEl || (canvasEl = fabric.document.createElement('canvas')); - /* eslint-disable camelcase */ - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - /* eslint-enable camelcase */ - return canvasEl; + 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); }, /** @@ -978,53 +1361,7 @@ fabric.CommonMethods = { * @return {HTMLImageElement} HTML image element */ createImage: function() { - return fabric.isLikelyNode - ? new (require('canvas').Image)() - : fabric.document.createElement('img'); - }, - - /** - * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array - * @static - * @memberOf fabric.util - * @param {Object} klass "Class" to create accessors for - */ - createAccessors: function(klass) { - var proto = klass.prototype, i, propName, - capitalizedPropName, setterName, getterName; - - for (i = proto.stateProperties.length; i--; ) { - - propName = proto.stateProperties[i]; - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1); - setterName = 'set' + capitalizedPropName; - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - }, - - /** - * @static - * @memberOf fabric.util - * @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(); + return fabric.document.createElement('img'); }, /** @@ -1049,7 +1386,7 @@ fabric.CommonMethods = { }, /** - * Decomposes standard 2x2 matrix into transform componentes + * Decomposes standard 2x3 matrix into transform components * @static * @memberOf fabric.util * @param {Array} a transformMatrix @@ -1059,10 +1396,10 @@ fabric.CommonMethods = { 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, + 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, + angle: angle / PiBy180, scaleX: scaleX, scaleY: scaleY, skewX: skewX / PiBy180, @@ -1072,12 +1409,107 @@ fabric.CommonMethods = { }; }, - customTransformMatrix: function(scaleX, scaleY, skewX) { - var skewMatrixX = [1, 0, abs(Math.tan(skewX * PiBy180)), 1], - scaleMatrix = [abs(scaleX), 0, 0, abs(scaleY)]; - return fabric.util.multiplyTransformMatrices(scaleMatrix, skewMatrixX, true); + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] angle in degrees + * @return {Number[]} transform matrix + */ + calcRotateMatrix: function(options) { + if (!options.angle) { + return fabric.iMatrix.concat(); + } + var theta = fabric.util.degreesToRadians(options.angle), + cos = fabric.util.cos(theta), + sin = fabric.util.sin(theta); + return [cos, sin, -sin, cos, 0, 0]; }, + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet. + * is called DimensionsTransformMatrix because those properties are the one that influence + * the size of the resulting box of the object. + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewY] + * @return {Number[]} transform matrix + */ + calcDimensionsMatrix: function(options) { + var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX, + scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY, + scaleMatrix = [ + options.flipX ? -scaleX : scaleX, + 0, + 0, + options.flipY ? -scaleY : scaleY, + 0, + 0], + multiply = fabric.util.multiplyTransformMatrices, + degreesToRadians = fabric.util.degreesToRadians; + if (options.skewX) { + scaleMatrix = multiply( + scaleMatrix, + [1, 0, Math.tan(degreesToRadians(options.skewX)), 1], + true); + } + if (options.skewY) { + scaleMatrix = multiply( + scaleMatrix, + [1, Math.tan(degreesToRadians(options.skewY)), 0, 1], + true); + } + return scaleMatrix; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @param {Number} [options.translateX] + * @param {Number} [options.translateY] + * @return {Number[]} transform matrix + */ + composeMatrix: function(options) { + var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0], + multiply = fabric.util.multiplyTransformMatrices; + if (options.angle) { + matrix = multiply(matrix, fabric.util.calcRotateMatrix(options)); + } + if (options.scaleX !== 1 || options.scaleY !== 1 || + options.skewX || options.skewY || options.flipX || options.flipY) { + matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options)); + } + return matrix; + }, + + /** + * 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; @@ -1085,16 +1517,28 @@ fabric.CommonMethods = { target.skewY = 0; target.flipX = false; target.flipY = false; - target.setAngle(0); + target.rotate(0); }, /** - * Returns string representation of function body - * @param {Function} fn Function to get body of - * @return {String} Function body + * Extract Object transform values + * @static + * @memberOf fabric.util + * @param {fabric.Object} target object to read from + * @return {Object} Components of transform */ - getFunctionBody: function(fn) { - return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; + 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 + }; }, /** @@ -1172,43 +1616,387 @@ fabric.CommonMethods = { }, /** - * Clear char widths cache for a font family. + * 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)); + }, + + /** + * Finds the scale for the object source to fit inside the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to fit into destination + */ + findScaleToFit: function(source, destination) { + return Math.min(destination.width / source.width, destination.height / source.height); + }, + + /** + * Finds the scale for the object source to cover entirely the object destination, + * keeping aspect ratio intact. + * respect the total allowed area for the cache. + * @memberOf fabric.util + * @param {Object | fabric.Object} source + * @param {Number} source.height natural unscaled height of the object + * @param {Number} source.width natural unscaled width of the object + * @param {Object | fabric.Object} destination + * @param {Number} destination.height natural unscaled height of the object + * @param {Number} destination.width natural unscaled width of the object + * @return {Number} scale factor to apply to source to cover destination + */ + 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} transform 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(' ') + ')'; + }, + + /** + * given an object and a transform, apply the inverse transform to the object, + * this is equivalent to remove from that object that transformation, so that + * added in a space with the removed transform, the object will be the same as before. + * Removing from an object a transform that scale by 2 is like scaling it by 1/2. + * Removing from an object a transfrom that rotate by 30deg is like rotating by 30deg + * in the opposite direction. + * This util is used to add objects inside transformed groups or nested groups. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + removeTransformFromObject: function(object, transform) { + var inverted = fabric.util.invertTransform(transform), + finalTransform = fabric.util.multiplyTransformMatrices(inverted, object.calcOwnMatrix()); + fabric.util.applyTransformToObject(object, finalTransform); + }, + + /** + * given an object and a transform, apply the transform to the object. + * this is equivalent to change the space where the object is drawn. + * Adding to an object a transform that scale by 2 is like scaling it by 2. + * This is used when removing an object from an active selection for example. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + addTransformToObject: function(object, transform) { + fabric.util.applyTransformToObject( + object, + fabric.util.multiplyTransformMatrices(transform, object.calcOwnMatrix()) + ); + }, + + /** + * discard an object transform state and apply the one from the matrix. + * @memberOf fabric.util + * @param {fabric.Object} object the object you want to transform + * @param {Array} transform the destination transform + */ + applyTransformToObject: function(object, transform) { + var options = fabric.util.qrDecompose(transform), + 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'); + }, + + /** + * given a width and height, return the size of the bounding box + * that can contains the box with width/height with applied transform + * described in options. + * Use to calculate the boxes around objects for controls. + * @memberOf fabric.util + * @param {Number} width + * @param {Number} height + * @param {Object} options + * @param {Number} options.scaleX + * @param {Number} options.scaleY + * @param {Number} options.skewX + * @param {Number} options.skewY + * @return {Object.x} width of containing + * @return {Object.y} height of containing + */ + sizeAfterTransform: function(width, height, options) { + var dimX = width / 2, dimY = height / 2, + points = [ + { + x: -dimX, + y: -dimY + }, + { + x: dimX, + y: -dimY + }, + { + x: -dimX, + y: dimY + }, + { + x: dimX, + y: dimY + }], + transformMatrix = fabric.util.calcDimensionsMatrix(options), + bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); + return { + x: bbox.width, + y: bbox.height, + }; + }, + + /** + * Merges 2 clip paths into one visually equal clip path + * + * **IMPORTANT**:\ + * Does **NOT** clone the arguments, clone them proir if necessary. + * + * Creates a wrapper (group) that contains one clip path and is clipped by the other so content is kept where both overlap. + * Use this method if both the clip paths may have nested clip paths of their own, so assigning one to the other's clip path property is not possible. + * + * In order to handle the `inverted` property we follow logic described in the following cases:\ + * **(1)** both clip paths are inverted - the clip paths pass the inverted prop to the wrapper and loose it themselves.\ + * **(2)** one is inverted and the other isn't - the wrapper shouldn't become inverted and the inverted clip path must clip the non inverted one to produce an identical visual effect.\ + * **(3)** both clip paths are not inverted - wrapper and clip paths remain unchanged. + * + * @memberOf fabric.util + * @param {fabric.Object} c1 + * @param {fabric.Object} c2 + * @returns {fabric.Object} merged clip path + */ + mergeClipPaths: function (c1, c2) { + var a = c1, b = c2; + if (a.inverted && !b.inverted) { + // case (2) + a = c2; + b = c1; + } + // `b` becomes `a`'s clip path so we transform `b` to `a` coordinate plane + fabric.util.applyTransformToObject( + b, + fabric.util.multiplyTransformMatrices( + fabric.util.invertTransform(a.calcTransformMatrix()), + b.calcTransformMatrix() + ) + ); + // assign the `inverted` prop to the wrapping group + var inverted = a.inverted && b.inverted; + if (inverted) { + // case (1) + a.inverted = b.inverted = false; + } + return new fabric.Group([a], { clipPath: b, inverted: inverted }); + }, + + /** + * @memberOf fabric.util + * @param {Object} prevStyle first style to compare + * @param {Object} thisStyle second style to compare + * @param {boolean} forTextSpans whether to check overline, underline, and line-through properties + * @return {boolean} true if the style changed + */ + hasStyleChanged: function(prevStyle, thisStyle, forTextSpans) { + forTextSpans = forTextSpans || false; + 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.textBackgroundColor !== thisStyle.textBackgroundColor || + prevStyle.deltaY !== thisStyle.deltaY) || + (forTextSpans && + (prevStyle.overline !== thisStyle.overline || + prevStyle.underline !== thisStyle.underline || + prevStyle.linethrough !== thisStyle.linethrough)); + }, + + /** + * Returns the array form of a text object's inline styles property with styles grouped in ranges + * rather than per character. This format is less verbose, and is better suited for storage + * so it is used in serialization (not during runtime). + * @memberOf fabric.util + * @param {object} styles per character styles for a text object + * @param {String} text the text string that the styles are applied to + * @return {{start: number, end: number, style: object}[]} + */ + stylesToArray: function(styles, text) { + // clone style structure to prevent mutation + var styles = fabric.util.object.clone(styles, true), + textLines = text.split('\n'), + charIndex = -1, prevStyle = {}, stylesArray = []; + //loop through each textLine + for (var i = 0; i < textLines.length; i++) { + if (!styles[i]) { + //no styles exist for this line, so add the line's length to the charIndex total + charIndex += textLines[i].length; + continue; + } + //loop through each character of the current line + for (var c = 0; c < textLines[i].length; c++) { + charIndex++; + var thisStyle = styles[i][c]; + //check if style exists for this character + if (thisStyle && Object.keys(thisStyle).length > 0) { + var styleChanged = fabric.util.hasStyleChanged(prevStyle, thisStyle, true); + if (styleChanged) { + stylesArray.push({ + start: charIndex, + end: charIndex + 1, + style: thisStyle + }); + } + else { + //if style is the same as previous character, increase end index + stylesArray[stylesArray.length - 1].end++; + } + } + prevStyle = thisStyle || {}; + } + } + return stylesArray; + }, + + /** + * Returns the object form of the styles property with styles that are assigned per + * character rather than grouped by range. This format is more verbose, and is + * only used during runtime (not for serialization/storage) + * @memberOf fabric.util + * @param {Array} styles the serialized form of a text object's styles + * @param {String} text the text string that the styles are applied to + * @return {Object} + */ + stylesFromArray: function(styles, text) { + if (!Array.isArray(styles)) { + return styles; + } + var textLines = text.split('\n'), + charIndex = -1, styleIndex = 0, stylesObject = {}; + //loop through each textLine + for (var i = 0; i < textLines.length; i++) { + //loop through each character of the current line + for (var c = 0; c < textLines[i].length; c++) { + charIndex++; + //check if there's a style collection that includes the current character + if (styles[styleIndex] + && styles[styleIndex].start <= charIndex + && charIndex < styles[styleIndex].end) { + //create object for line index if it doesn't exist + stylesObject[i] = stylesObject[i] || {}; + //assign a style at this character's index + stylesObject[i][c] = Object.assign({}, styles[styleIndex].style); + //if character is at the end of the current style collection, move to the next + if (charIndex === styles[styleIndex].end - 1) { + styleIndex++; + } + } + } + } + return stylesObject; } }; - })(typeof exports !== 'undefined' ? exports : this); (function() { + var _join = Array.prototype.join, + commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }, + repeatedCommands = { + m: 'l', + M: 'L' + }; + 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); - var arcToSegmentsCache = { }, - segmentToBezierCache = { }, - boundsOfCurveCache = { }, - _join = Array.prototype.join; + return ['C', + cp1X, cp1Y, + cp2X, cp2Y, + toX, toY + ]; + } /* 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 (arcToSegmentsCache[argsString]) { - return arcToSegmentsCache[argsString]; - } - var PI = Math.PI, th = rotateX * PI / 180, - sinTh = Math.sin(th), - cosTh = Math.cos(th), + sinTh = fabric.util.sin(th), + cosTh = fabric.util.cos(th), fromX = 0, fromY = 0; rx = Math.abs(rx); @@ -1252,40 +2040,14 @@ fabric.CommonMethods = { 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]; + fromX = result[i][5]; + fromY = result[i][6]; mTheta = th3; th3 += mDelta; } - arcToSegmentsCache[argsString] = result; return result; } - function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) { - var argsString2 = _join.call(arguments); - if (segmentToBezierCache[argsString2]) { - return segmentToBezierCache[argsString2]; - } - - var costh2 = Math.cos(th2), - sinth2 = Math.sin(th2), - costh3 = Math.cos(th3), - sinth3 = Math.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); - - segmentToBezierCache[argsString2] = [ - cp1X, cp1Y, - cp2X, cp2Y, - toX, toY - ]; - return segmentToBezierCache[argsString2]; - } - /* * Private */ @@ -1300,62 +2062,6 @@ fabric.CommonMethods = { } } - /** - * 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 @@ -1364,14 +2070,18 @@ fabric.CommonMethods = { * @param {Number} y1 * @param {Number} x2 secondo control point * @param {Number} y2 - * @param {Number} x3 end of beizer + * @param {Number} x3 end of bezier * @param {Number} y3 */ // taken from http://jsbin.com/ivomiq/56/edit no credits available for that. + // TODO: can we normalize this with the starting points set at 0 and then translated the bbox? function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) { - var argsString = _join.call(arguments); - if (boundsOfCurveCache[argsString]) { - return boundsOfCurveCache[argsString]; + var argsString; + if (fabric.cachesBoundsOfCurve) { + argsString = _join.call(arguments); + if (fabric.boundsOfCurveCache[argsString]) { + return fabric.boundsOfCurveCache[argsString]; + } } var sqrt = Math.sqrt, @@ -1441,12 +2151,636 @@ fabric.CommonMethods = { y: max.apply(null, bounds[1]) } ]; - boundsOfCurveCache[argsString] = result; + if (fabric.cachesBoundsOfCurve) { + fabric.boundsOfCurveCache[argsString] = result; + } return result; } - fabric.util.getBoundsOfCurve = getBoundsOfCurve; + /** + * Converts arc to a bunch of bezier curves + * @param {Number} fx starting point x + * @param {Number} fy starting point y + * @param {Array} coords Arc command + */ + function fromArcToBeziers(fx, fy, coords) { + var rx = coords[1], + ry = coords[2], + rot = coords[3], + large = coords[4], + sweep = coords[5], + tx = coords[6], + ty = coords[7], + segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + for (var i = 0, len = segsNorm.length; i < len; i++) { + segsNorm[i][1] += fx; + segsNorm[i][2] += fy; + segsNorm[i][3] += fx; + segsNorm[i][4] += fy; + segsNorm[i][5] += fx; + segsNorm[i][6] += fy; + } + return segsNorm; + }; + + /** + * This function take a parsed SVG path and make it simpler for fabricJS logic. + * simplification consist of: only UPPERCASE absolute commands ( relative converted to absolute ) + * S converted in C, T converted in Q, A converted in C. + * @param {Array} path the array of commands of a parsed svg path for fabric.Path + * @return {Array} the simplified array of commands of a parsed svg path for fabric.Path + */ + function makePathSimpler(path) { + // x and y represent the last point of the path. the previous command point. + // we add them to each relative command to make it an absolute comment. + // we also swap the v V h H with L, because are easier to transform. + var x = 0, y = 0, len = path.length, + // x1 and y1 represent the last point of the subpath. the subpath is started with + // m or M command. When a z or Z command is drawn, x and y need to be resetted to + // the last x1 and y1. + x1 = 0, y1 = 0, current, i, converted, + // previous will host the letter of the previous command, to handle S and T. + // controlX and controlY will host the previous reflected control point + destinationPath = [], previous, controlX, controlY; + for (i = 0; i < len; ++i) { + converted = false; + current = path[i].slice(0); + switch (current[0]) { // first letter + case 'l': // lineto, relative + current[0] = 'L'; + current[1] += x; + current[2] += y; + // falls through + case 'L': + x = current[1]; + y = current[2]; + break; + case 'h': // horizontal lineto, relative + current[1] += x; + // falls through + case 'H': + current[0] = 'L'; + current[2] = y; + x = current[1]; + break; + case 'v': // vertical lineto, relative + current[1] += y; + // falls through + case 'V': + current[0] = 'L'; + y = current[1]; + current[1] = x; + current[2] = y; + break; + case 'm': // moveTo, relative + current[0] = 'M'; + current[1] += x; + current[2] += y; + // falls through + case 'M': + x = current[1]; + y = current[2]; + x1 = current[1]; + y1 = current[2]; + break; + case 'c': // bezierCurveTo, relative + current[0] = 'C'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + current[5] += x; + current[6] += y; + // falls through + case 'C': + controlX = current[3]; + controlY = current[4]; + x = current[5]; + y = current[6]; + break; + case 's': // shorthand cubic bezierCurveTo, relative + current[0] = 'S'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'S': + // would be sScC but since we are swapping sSc for C, we check just that. + if (previous === 'C') { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // 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; + } + x = current[3]; + y = current[4]; + current[0] = 'C'; + current[5] = current[3]; + current[6] = current[4]; + current[3] = current[1]; + current[4] = current[2]; + current[1] = controlX; + current[2] = controlY; + // current[3] and current[4] are NOW the second control point. + // we keep it for the next reflection. + controlX = current[3]; + controlY = current[4]; + break; + case 'q': // quadraticCurveTo, relative + current[0] = 'Q'; + current[1] += x; + current[2] += y; + current[3] += x; + current[4] += y; + // falls through + case 'Q': + controlX = current[1]; + controlY = current[2]; + x = current[3]; + y = current[4]; + break; + case 't': // shorthand quadraticCurveTo, relative + current[0] = 'T'; + current[1] += x; + current[2] += y; + // falls through + case 'T': + if (previous === 'Q') { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + else { + // 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; + } + current[0] = 'Q'; + x = current[1]; + y = current[2]; + current[1] = controlX; + current[2] = controlY; + current[3] = x; + current[4] = y; + break; + case 'a': + current[0] = 'A'; + current[6] += x; + current[7] += y; + // falls through + case 'A': + converted = true; + destinationPath = destinationPath.concat(fromArcToBeziers(x, y, current)); + x = current[6]; + y = current[7]; + break; + case 'z': + case 'Z': + x = x1; + y = y1; + break; + default: + } + if (!converted) { + destinationPath.push(current); + } + previous = current[0]; + } + return destinationPath; + }; + + /** + * Calc length from point x1,y1 to x2,y2 + * @param {Number} x1 starting point x + * @param {Number} y1 starting point y + * @param {Number} x2 starting point x + * @param {Number} y2 starting point y + * @return {Number} length of segment + */ + function calcLineLength(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + + // functions for the Cubic beizer + // taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350 + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + + function getPointOnCubicBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function(pct) { + var c1 = CB1(pct), c2 = CB2(pct), c3 = CB3(pct), c4 = CB4(pct); + return { + x: p4x * c1 + p3x * c2 + p2x * c3 + p1x * c4, + y: p4y * c1 + p3y * c2 + p2y * c3 + p1y * c4 + }; + }; + } + + function getTangentCubicIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (3 * invT * invT * (p2x - p1x)) + (6 * invT * pct * (p3x - p2x)) + + (3 * pct * pct * (p4x - p3x)), + tangentY = (3 * invT * invT * (p2y - p1y)) + (6 * invT * pct * (p3y - p2y)) + + (3 * pct * pct * (p4y - p3y)); + return Math.atan2(tangentY, tangentX); + }; + } + + function QB1(t) { + return t * t; + } + + function QB2(t) { + return 2 * t * (1 - t); + } + + function QB3(t) { + return (1 - t) * (1 - t); + } + + function getPointOnQuadraticBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function(pct) { + var c1 = QB1(pct), c2 = QB2(pct), c3 = QB3(pct); + return { + x: p3x * c1 + p2x * c2 + p1x * c3, + y: p3y * c1 + p2y * c2 + p1y * c3 + }; + }; + } + + function getTangentQuadraticIterator(p1x, p1y, p2x, p2y, p3x, p3y) { + return function (pct) { + var invT = 1 - pct, + tangentX = (2 * invT * (p2x - p1x)) + (2 * pct * (p3x - p2x)), + tangentY = (2 * invT * (p2y - p1y)) + (2 * pct * (p3y - p2y)); + return Math.atan2(tangentY, tangentX); + }; + } + + + // this will run over a path segment ( a cubic or quadratic segment) and approximate it + // with 100 segemnts. This will good enough to calculate the length of the curve + function pathIterator(iterator, x1, y1) { + var tempP = { x: x1, y: y1 }, p, tmpLen = 0, perc; + for (perc = 1; perc <= 100; perc += 1) { + p = iterator(perc / 100); + tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y); + tempP = p; + } + return tmpLen; + } + + /** + * Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1 + * that correspond to that pixels run over the path. + * The percentage will be then used to find the correct point on the canvas for the path. + * @param {Array} segInfo fabricJS collection of information on a parsed path + * @param {Number} distance from starting point, in pixels. + * @return {Object} info object with x and y ( the point on canvas ) and angle, the tangent on that point; + */ + function findPercentageForDistance(segInfo, distance) { + var perc = 0, tmpLen = 0, iterator = segInfo.iterator, tempP = { x: segInfo.x, y: segInfo.y }, + p, nextLen, nextStep = 0.01, angleFinder = segInfo.angleFinder, lastPerc; + // nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100 + // the path + while (tmpLen < distance && nextStep > 0.0001) { + p = iterator(perc); + lastPerc = perc; + nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y); + // compare tmpLen each cycle with distance, decide next perc to test. + if ((nextLen + tmpLen) > distance) { + // we discard this step and we make smaller steps. + perc -= nextStep; + nextStep /= 2; + } + else { + tempP = p; + perc += nextStep; + tmpLen += nextLen; + } + } + p.angle = angleFinder(lastPerc); + return p; + } + + /** + * Run over a parsed and simplifed path and extrac some informations. + * informations are length of each command and starting point + * @param {Array} path fabricJS parsed path commands + * @return {Array} path commands informations + */ + function getPathSegmentsInfo(path) { + var totalLength = 0, len = path.length, current, + //x2 and y2 are the coords of segment start + //x1 and y1 are the coords of the current point + x1 = 0, y1 = 0, x2 = 0, y2 = 0, info = [], iterator, tempInfo, angleFinder; + for (var i = 0; i < len; i++) { + current = path[i]; + tempInfo = { + x: x1, + y: y1, + command: current[0], + }; + switch (current[0]) { //first letter + case 'M': + tempInfo.length = 0; + x2 = x1 = current[1]; + y2 = y1 = current[2]; + break; + case 'L': + tempInfo.length = calcLineLength(x1, y1, current[1], current[2]); + x1 = current[1]; + y1 = current[2]; + break; + case 'C': + iterator = getPointOnCubicBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + angleFinder = getTangentCubicIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[5]; + y1 = current[6]; + break; + case 'Q': + iterator = getPointOnQuadraticBezierIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + angleFinder = getTangentQuadraticIterator( + x1, + y1, + current[1], + current[2], + current[3], + current[4] + ); + tempInfo.iterator = iterator; + tempInfo.angleFinder = angleFinder; + tempInfo.length = pathIterator(iterator, x1, y1); + x1 = current[3]; + y1 = current[4]; + break; + case 'Z': + case 'z': + // we add those in order to ease calculations later + tempInfo.destX = x2; + tempInfo.destY = y2; + tempInfo.length = calcLineLength(x1, y1, x2, y2); + x1 = x2; + y1 = y2; + break; + } + totalLength += tempInfo.length; + info.push(tempInfo); + } + info.push({ length: totalLength, x: x1, y: y1 }); + return info; + } + + function getPointOnPath(path, distance, infos) { + if (!infos) { + infos = getPathSegmentsInfo(path); + } + var i = 0; + while ((distance - infos[i].length > 0) && i < (infos.length - 2)) { + distance -= infos[i].length; + i++; + } + // var distance = infos[infos.length - 1] * perc; + var segInfo = infos[i], segPercent = distance / segInfo.length, + command = segInfo.command, segment = path[i], info; + + switch (command) { + case 'M': + return { x: segInfo.x, y: segInfo.y, angle: 0 }; + case 'Z': + case 'z': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segInfo.destX, segInfo.destY), + segPercent + ); + info.angle = Math.atan2(segInfo.destY - segInfo.y, segInfo.destX - segInfo.x); + return info; + case 'L': + info = new fabric.Point(segInfo.x, segInfo.y).lerp( + new fabric.Point(segment[1], segment[2]), + segPercent + ); + info.angle = Math.atan2(segment[2] - segInfo.y, segment[1] - segInfo.x); + return info; + case 'C': + return findPercentageForDistance(segInfo, distance); + case 'Q': + return findPercentageForDistance(segInfo, distance); + } + } + + /** + * + * @param {string} pathString + * @return {(string|number)[][]} An array of SVG path commands + * @example Usage + * parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [ + * ['M', 3, 4], + * ['Q', 3, 5, 2, 1, 4, 0], + * ['Q', 9, 12, 2, 1, 4, 0], + * ]; + * + */ + function parsePath(pathString) { + var result = [], + coords = [], + currentPath, + parsed, + re = fabric.rePathCommand, + rNumber = '[-+]?(?:\\d*\\.\\d+|\\d+\\.?)(?:[eE][-+]?\\d+)?\\s*', + rNumberCommaWsp = '(' + rNumber + ')' + fabric.commaWsp, + rFlagCommaWsp = '([01])' + fabric.commaWsp + '?', + rArcSeq = rNumberCommaWsp + '?' + rNumberCommaWsp + '?' + rNumberCommaWsp + rFlagCommaWsp + rFlagCommaWsp + + rNumberCommaWsp + '?(' + rNumber + ')', + regArcArgumentSequence = new RegExp(rArcSeq, 'g'), + match, + coordsStr, + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + path; + if (!pathString || !pathString.match) { + return result; + } + path = pathString.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + for (var i = 0, coordsParsed, len = path.length; i < len; i++) { + currentPath = path[i]; + + coordsStr = currentPath.slice(1).trim(); + coords.length = 0; + + var command = currentPath.charAt(0); + coordsParsed = [command]; + + if (command.toLowerCase() === 'a') { + // arcs have special flags that apparently don't require spaces so handle special + for (var args; (args = regArcArgumentSequence.exec(coordsStr));) { + for (var j = 1; j < args.length; j++) { + coords.push(args[j]); + } + } + } + else { + while ((match = re.exec(coordsStr))) { + coords.push(match[0]); + } + } + + for (var j = 0, jlen = coords.length; j < jlen; j++) { + parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + coordsParsed.push(parsed); + } + } + + var 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; + }; + + /** + * + * Converts points to a smooth SVG path + * @param {{ x: number,y: number }[]} points Array of points + * @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value. + * @return {(string|number)[][]} An array of SVG path commands + */ + function getSmoothPathFromPoints(points, correction) { + var path = [], i, + 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; + correction = correction || 0; + + 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 * correction, p1.y - multSignY * correction]); + 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 * correction, p1.y + multSignY * correction]); + return path; + } + /** + * Transform a path by transforming each segment. + * it has to be a simplified path or it won't work. + * WARNING: this depends from pathOffset for correct operation + * @param {Array} path fabricJS parsed and simplified path commands + * @param {Array} transform matrix that represent the transformation + * @param {Object} [pathOffset] the fabric.Path pathOffset + * @param {Number} pathOffset.x + * @param {Number} pathOffset.y + * @returns {Array} the transformed path + */ + function transformPath(path, transform, pathOffset) { + if (pathOffset) { + transform = fabric.util.multiplyTransformMatrices( + transform, + [1, 0, 0, 1, -pathOffset.x, -pathOffset.y] + ); + } + return path.map(function(pathSegment) { + var newSegment = pathSegment.slice(0), point = {}; + for (var i = 1; i < pathSegment.length - 1; i += 2) { + point.x = pathSegment[i]; + point.y = pathSegment[i + 1]; + point = fabric.util.transformPoint(point, transform); + newSegment[i] = point.x; + newSegment[i + 1] = point.y; + } + return newSegment; + }); + } + + /** + * Join path commands to go back to svg format + * @param {Array} pathData fabricJS parsed path commands + * @return {String} joined path 'M 0 0 L 20 30' + */ + fabric.util.joinPath = function(pathData) { + return pathData.map(function (segment) { return segment.join(' '); }).join(' '); + }; + fabric.util.parsePath = parsePath; + fabric.util.makePathSimpler = makePathSimpler; + fabric.util.getSmoothPathFromPoints = getSmoothPathFromPoints; + fabric.util.getPathSegmentsInfo = getPathSegmentsInfo; + fabric.util.getBoundsOfCurve = getBoundsOfCurve; + fabric.util.getPointOnPath = getPointOnPath; + fabric.util.transformPath = transformPath; })(); @@ -1454,172 +2788,6 @@ fabric.CommonMethods = { var slice = Array.prototype.slice; - /* _ES5_COMPAT_START_ */ - - if (!Array.prototype.indexOf) { - /** - * Finds index of an element in an array - * @param {*} searchElement - * @return {Number} - */ - Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { - if (this === void 0 || this === null) { - throw new TypeError(); - } - var t = Object(this), len = t.length >>> 0; - if (len === 0) { - return -1; - } - var n = 0; - if (arguments.length > 0) { - n = Number(arguments[1]); - if (n !== n) { // shortcut for verifying if it's NaN - n = 0; - } - else if (n !== 0 && n !== Number.POSITIVE_INFINITY && n !== Number.NEGATIVE_INFINITY) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); - for (; k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - return -1; - }; - } - - if (!Array.prototype.forEach) { - /** - * Iterates an array, invoking callback for each element - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Array} - */ - Array.prototype.forEach = function(fn, context) { - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this) { - fn.call(context, this[i], i, this); - } - } - }; - } - - if (!Array.prototype.map) { - /** - * Returns a result of iterating over an array, invoking callback for each element - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Array} - */ - Array.prototype.map = function(fn, context) { - var result = []; - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this) { - result[i] = fn.call(context, this[i], i, this); - } - } - return result; - }; - } - - if (!Array.prototype.every) { - /** - * Returns true if a callback returns truthy value for all elements in an array - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Boolean} - */ - Array.prototype.every = function(fn, context) { - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this && !fn.call(context, this[i], i, this)) { - return false; - } - } - return true; - }; - } - - if (!Array.prototype.some) { - /** - * Returns true if a callback returns truthy value for at least one element in an array - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Boolean} - */ - Array.prototype.some = function(fn, context) { - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this && fn.call(context, this[i], i, this)) { - return true; - } - } - return false; - }; - } - - if (!Array.prototype.filter) { - /** - * Returns the result of iterating over elements in an array - * @param {Function} fn Callback to invoke for each element - * @param {Object} [context] Context to invoke callback in - * @return {Array} - */ - Array.prototype.filter = function(fn, context) { - var result = [], val; - for (var i = 0, len = this.length >>> 0; i < len; i++) { - if (i in this) { - val = this[i]; // in case fn mutates this - if (fn.call(context, val, i, this)) { - result.push(val); - } - } - } - return result; - }; - } - - if (!Array.prototype.reduce) { - /** - * Returns "folded" (reduced) result of iterating over elements in an array - * @param {Function} fn Callback to invoke for each element - * @return {*} - */ - Array.prototype.reduce = function(fn /*, initial*/) { - var len = this.length >>> 0, - i = 0, - rv; - - if (arguments.length > 1) { - rv = arguments[1]; - } - else { - do { - if (i in this) { - rv = this[i++]; - break; - } - // if array contains no values, no initial value to return - if (++i >= len) { - throw new TypeError(); - } - } - while (true); - } - for (; i < len; i++) { - if (i in this) { - rv = fn.call(null, rv, this[i], i, this); - } - } - return rv; - }; - } - - /* _ES5_COMPAT_END_ */ - /** * Invokes method on all items in a given array * @memberOf fabric.util.array @@ -1715,10 +2883,14 @@ fabric.CommonMethods = { (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 and group properties in deep cloning. * @memberOf fabric.util.object * @param {Object} destination Where to copy to * @param {Object} source Where to copy from + * @param {Boolean} [deep] Whether to extend nested objects * @return {Object} */ @@ -1739,7 +2911,12 @@ fabric.CommonMethods = { } else if (source && typeof source === 'object') { for (var property in source) { - if (source.hasOwnProperty(property)) { + if (property === 'canvas' || property === 'group') { + // we do not want to clone this props at all. + // we want to keep the keys in the copy + destination[property] = null; + } + else if (source.hasOwnProperty(property)) { destination[property] = extend({ }, source[property], deep); } } @@ -1759,10 +2936,14 @@ fabric.CommonMethods = { /** * Creates an empty object and copies all enumerable properties of another object to it + * This method is mostly for internal use, and not intended for duplicating shapes in canvas. * @memberOf fabric.util.object * @param {Object} object Object to clone + * @param {Boolean} [deep] Whether to clone nested objects * @return {Object} */ + + //TODO: this function return an empty object if you try to clone null function clone(object, deep) { return extend({ }, object, deep); } @@ -1772,26 +2953,12 @@ fabric.CommonMethods = { extend: extend, clone: clone }; - + fabric.util.object.extend(fabric.util, fabric.Observable); })(); (function() { - /* _ES5_COMPAT_START_ */ - if (!String.prototype.trim) { - /** - * Trims a string (removing whitespace from the beginning and the end) - * @function external:String#trim - * @see String#trim on MDN - */ - String.prototype.trim = function () { - // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now - return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); - }; - } - /* _ES5_COMPAT_END_ */ - /** * Camelizes a string * @memberOf fabric.util.string @@ -1826,12 +2993,69 @@ fabric.CommonMethods = { */ function escapeXml(string) { return string.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .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 @@ -1839,50 +3063,12 @@ fabric.CommonMethods = { fabric.util.string = { camelize: camelize, capitalize: capitalize, - escapeXml: escapeXml + escapeXml: escapeXml, + graphemeSplit: graphemeSplit }; })(); -/* _ES5_COMPAT_START_ */ -(function() { - - var slice = Array.prototype.slice, - apply = Function.prototype.apply, - Dummy = function() { }; - - if (!Function.prototype.bind) { - /** - * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) - * @see Function#bind on MDN - * @param {Object} thisArg Object to bind function to - * @param {Any[]} Values to pass to a bound function - * @return {Function} - */ - Function.prototype.bind = function(thisArg) { - var _this = this, args = slice.call(arguments, 1), bound; - if (args.length) { - bound = function() { - return apply.call(_this, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); - }; - } - else { - /** @ignore */ - bound = function() { - return apply.call(_this, this instanceof Dummy ? this : thisArg, arguments); - }; - } - Dummy.prototype = this.prototype; - bound.prototype = new Dummy(); - - return bound; - }; - } - -})(); -/* _ES5_COMPAT_END_ */ - - (function() { var slice = Array.prototype.slice, emptyFunction = function() { }, @@ -2001,158 +3187,9 @@ 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.addEventListener(eventName, handler, shouldUseAttachEventDetachEvent ? false : options); - }; - /** @ignore */ - removeListener = function (element, eventName, handler, options) { - element.removeEventListener(eventName, handler, shouldUseAttachEventDetachEvent ? false : options); - }; - } - - else if (shouldUseAttachEventDetachEvent) { - /** @ignore */ - addListener = function (element, eventName, handler) { - 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) { - 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) { - 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) { - 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 ie11 can use addEventListener but they do not support options, i need to check + var couldUseAttachEvent = !!fabric.document.createElement('div').attachEvent, + touchEvents = ['touchstart', 'touchmove', 'touchend']; /** * Adds an event listener to an element * @function @@ -2161,7 +3198,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 @@ -2171,60 +3210,31 @@ fabric.CommonMethods = { * @param {String} eventName * @param {Function} handler */ - fabric.util.removeListener = removeListener; + fabric.util.removeListener = function(element, eventName, handler, options) { + element && element.removeEventListener(eventName, handler, couldUseAttachEvent ? false : options); + }; - /** - * 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); + 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: pointerX(event) + scroll.left, - y: pointerY(event) + scroll.top + x: _evt.clientX + scroll.left, + y: _evt.clientY + scroll.top }; - } - - var pointerX = function(event) { - // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) - // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] - // need to investigate later - return (typeof event.clientX !== unknown ? event.clientX : 0); - }, - - pointerY = function(event) { - return (typeof event.clientY !== unknown ? event.clientY : 0); - }; - - function _getPointer(event, pageProp, clientProp) { - var touchProp = event.type === 'touchend' ? 'changedTouches' : 'touches'; - - return (event[touchProp] && event[touchProp][0] - ? (event[touchProp][0][pageProp] - (event[touchProp][0][pageProp] - event[touchProp][0][clientProp])) - || event[clientProp] - : event[clientProp]); - } - - if (fabric.isTouchSupported) { - pointerX = function(event) { - return _getPointer(event, 'pageX', 'clientX'); - }; - pointerY = function(event) { - return _getPointer(event, 'pageY', 'clientY'); - }; - } - - fabric.util.getPointer = getPointer; - - fabric.util.object.extend(fabric.util, fabric.Observable); + }; + fabric.util.isTouchEvent = function(event) { + return touchEvents.indexOf(event.type) > -1 || event.pointerType === 'touch'; + }; })(); @@ -2256,7 +3266,7 @@ fabric.CommonMethods = { var normalizedProperty = (property === 'float' || property === 'cssFloat') ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') : property; - elementStyle[normalizedProperty] = styles[property]; + elementStyle.setProperty(normalizedProperty, styles[property]); } } return element; @@ -2427,8 +3437,7 @@ fabric.CommonMethods = { top += element.scrollTop || 0; } - if (element.nodeType === 1 && - fabric.util.getElementStyle(element, 'position') === 'fixed') { + if (element.nodeType === 1 && element.style.position === 'fixed') { break; } } @@ -2555,49 +3564,50 @@ fabric.CommonMethods = { fabric.util.makeElementSelectable = makeElementSelectable; })(); - (function() { + function getNodeCanvas(element) { + var impl = fabric.jsdomImplForWrapper(element); + return impl._canvas || impl._image; + }; - /** - * 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); + 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.getScript = getScript; - })(); + function setImageSmoothing(ctx, value) { + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled + || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; + ctx.imageSmoothingEnabled = value; + } + /** + * setImageSmoothing sets the context imageSmoothingEnabled property. + * Used by canvas and by ImageObject. + * @memberOf fabric.util + * @since 4.0.0 + * @param {HTMLRenderingContext2D} ctx to set on + * @param {Boolean} value true or false + */ + fabric.util.setImageSmoothing = setImageSmoothing; fabric.util.getById = getById; fabric.util.toArray = toArray; - fabric.util.makeElement = makeElement; fabric.util.addClass = addClass; + fabric.util.makeElement = makeElement; fabric.util.wrapElement = wrapElement; fabric.util.getScrollLeftTop = getScrollLeftTop; fabric.util.getElementOffset = getElementOffset; - fabric.util.getElementStyle = getElementStyle; + fabric.util.getNodeCanvas = getNodeCanvas; + fabric.util.cleanUpJsdomNode = cleanUpJsdomNode; })(); @@ -2608,24 +3618,6 @@ fabric.CommonMethods = { return url + (/\?/.test(url) ? '&' : '?') + param; } - var makeXHR = (function() { - var factories = [ - function() { return new ActiveXObject('Microsoft.XMLHTTP'); }, - function() { return new ActiveXObject('Msxml2.XMLHTTP'); }, - function() { return new ActiveXObject('Msxml2.XMLHTTP.3.0'); }, - function() { return new XMLHttpRequest(); } - ]; - for (var i = factories.length; i--; ) { - try { - var req = factories[i](); - if (req) { - return factories[i]; - } - } - catch (err) { } - } - })(); - function emptyFn() { } /** @@ -2640,12 +3632,11 @@ fabric.CommonMethods = { * @return {XMLHttpRequest} request */ function request(url, options) { - options || (options = { }); var method = options.method ? options.method.toUpperCase() : 'GET', onComplete = options.onComplete || function() { }, - xhr = makeXHR(), + xhr = new fabric.window.XMLHttpRequest(), body = options.body || options.parameters; /** @ignore */ @@ -2681,78 +3672,236 @@ fabric.CommonMethods = { * Wrapper around `console.log` (when available) * @param {*} [values] Values to log */ -fabric.log = function() { }; +fabric.log = console.log; /** * Wrapper around `console.warn` (when available) * @param {*} [values] Values to log as a warning */ -fabric.warn = function() { }; +fabric.warn = console.warn; -/* eslint-disable */ -if (typeof console !== 'undefined') { - ['log', 'warn'].forEach(function(methodName) { +(function () { - if (typeof console[methodName] !== 'undefined' && - typeof console[methodName].apply === 'function') { + var extend = fabric.util.object.extend, + clone = fabric.util.object.clone; - fabric[methodName] = function() { - return console[methodName].apply(console, arguments); - }; + /** + * @typedef {Object} AnimationOptions + * Animation of a value or list of values. + * When using lists, think of something like this: + * fabric.util.animate({ + * startValue: [1, 2, 3], + * endValue: [2, 4, 6], + * onChange: function([a, b, c]) { + * canvas.zoomToPoint({x: b, y: c}, a) + * canvas.renderAll() + * } + * }); + * @example + * @property {Function} [onChange] Callback; invoked on every value change + * @property {Function} [onComplete] Callback; invoked when value change is completed + * @example + * // Note: startValue, endValue, and byValue must match the type + * var animationOptions = { startValue: 0, endValue: 1, byValue: 0.25 } + * var animationOptions = { startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] } + * @property {number | number[]} [startValue=0] Starting value + * @property {number | number[]} [endValue=100] Ending value + * @property {number | number[]} [byValue=100] Value to modify the property by + * @property {Function} [easing] Easing function + * @property {Number} [duration=500] Duration of change (in ms) + * @property {Function} [abort] Additional function with logic. If returns true, animation aborts. + * + * @typedef {() => void} CancelFunction + * + * @typedef {Object} AnimationCurrentState + * @property {number | number[]} currentValue value in range [`startValue`, `endValue`] + * @property {number} completionRate value in range [0, 1] + * @property {number} durationRate value in range [0, 1] + * + * @typedef {(AnimationOptions & AnimationCurrentState & { cancel: CancelFunction }} AnimationContext + */ + + /** + * Array holding all running animations + * @memberof fabric + * @type {AnimationContext[]} + */ + var RUNNING_ANIMATIONS = []; + fabric.util.object.extend(RUNNING_ANIMATIONS, { + + /** + * cancel all running animations at the next requestAnimFrame + * @returns {AnimationContext[]} + */ + cancelAll: function () { + var animations = this.splice(0); + animations.forEach(function (animation) { + animation.cancel(); + }); + return animations; + }, + + /** + * cancel all running animations attached to canvas at the next requestAnimFrame + * @param {fabric.Canvas} canvas + * @returns {AnimationContext[]} + */ + cancelByCanvas: function (canvas) { + if (!canvas) { + return []; + } + var cancelled = this.filter(function (animation) { + return typeof animation.target === 'object' && animation.target.canvas === canvas; + }); + cancelled.forEach(function (animation) { + animation.cancel(); + }); + return cancelled; + }, + + /** + * cancel all running animations for target at the next requestAnimFrame + * @param {*} target + * @returns {AnimationContext[]} + */ + cancelByTarget: function (target) { + var cancelled = this.findAnimationsByTarget(target); + cancelled.forEach(function (animation) { + animation.cancel(); + }); + return cancelled; + }, + + /** + * + * @param {CancelFunction} cancelFunc the function returned by animate + * @returns {number} + */ + findAnimationIndex: function (cancelFunc) { + return this.indexOf(this.findAnimation(cancelFunc)); + }, + + /** + * + * @param {CancelFunction} cancelFunc the function returned by animate + * @returns {AnimationContext | undefined} animation's options object + */ + findAnimation: function (cancelFunc) { + return this.find(function (animation) { + return animation.cancel === cancelFunc; + }); + }, + + /** + * + * @param {*} target the object that is assigned to the target property of the animation context + * @returns {AnimationContext[]} array of animation options object associated with target + */ + findAnimationsByTarget: function (target) { + if (!target) { + return []; + } + return this.filter(function (animation) { + return animation.target === target; + }); } }); -} -/* eslint-enable */ + function noop() { + return false; + } -(function() { + 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 {AnimationOptions} [options] Animation options + * @example + * // Note: startValue, endValue, and byValue must match the type + * fabric.util.animate({ startValue: 0, endValue: 1, byValue: 0.25 }) + * fabric.util.animate({ startValue: [0, 1], endValue: [1, 2], byValue: [0.25, 0.25] }) + * @returns {CancelFunction} cancel function */ function animate(options) { + options || (options = {}); + var cancel = false, + context, + removeFromRegistry = function () { + var index = fabric.runningAnimations.indexOf(context); + return index > -1 && fabric.runningAnimations.splice(index, 1)[0]; + }; + + context = extend(clone(options), { + cancel: function () { + cancel = true; + return removeFromRegistry(); + }, + currentValue: 'startValue' in options ? options.startValue : 0, + completionRate: 0, + durationRate: 0 + }); + fabric.runningAnimations.push(context); requestAnimFrame(function(timestamp) { - options || (options = { }); - var start = timestamp || +new Date(), duration = options.duration || 500, finish = start + duration, time, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;}, + onChange = options.onChange || noop, + abort = options.abort || noop, + onComplete = options.onComplete || noop, + easing = options.easing || defaultEasing, + isMany = 'startValue' in options ? options.startValue.length > 0 : false, startValue = 'startValue' in options ? options.startValue : 0, endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || endValue - startValue; + byValue = options.byValue || (isMany ? startValue.map(function(value, i) { + return endValue[i] - startValue[i]; + }) : endValue - startValue); options.onStart && options.onStart(); (function tick(ticktime) { time = ticktime || +new Date(); - var currentTime = time > finish ? duration : (time - start); - if (abort()) { - options.onComplete && options.onComplete(); + var currentTime = time > finish ? duration : (time - start), + timePerc = currentTime / duration, + current = isMany ? startValue.map(function(_value, i) { + return easing(currentTime, startValue[i], byValue[i], duration); + }) : easing(currentTime, startValue, byValue, duration), + valuePerc = isMany ? Math.abs((current[0] - startValue[0]) / byValue[0]) + : Math.abs((current - startValue) / byValue); + // update context + context.currentValue = isMany ? current.slice() : current; + context.completionRate = valuePerc; + context.durationRate = timePerc; + if (cancel) { + return; + } + if (abort(current, valuePerc, timePerc)) { + removeFromRegistry(); return; } - onChange(easing(currentTime, startValue, byValue, duration)); if (time > finish) { - options.onComplete && options.onComplete(); + // update context + context.currentValue = isMany ? endValue.slice() : endValue; + context.completionRate = 1; + context.durationRate = 1; + // execute callbacks + onChange(isMany ? endValue.slice() : endValue, 1, 1); + onComplete(endValue, 1, 1); + removeFromRegistry(); return; } - requestAnimFrame(tick); + else { + onChange(current, valuePerc, timePerc); + requestAnimFrame(tick); + } })(start); }); + return context.cancel; } var _requestAnimFrame = fabric.window.requestAnimationFrame || @@ -2761,9 +3910,11 @@ if (typeof console !== 'undefined') { fabric.window.oRequestAnimationFrame || fabric.window.msRequestAnimationFrame || function(callback) { - fabric.window.setTimeout(callback, 1000 / 60); + 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 @@ -2775,9 +3926,14 @@ if (typeof console !== 'undefined') { 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; + fabric.runningAnimations = RUNNING_ANIMATIONS; })(); @@ -2806,23 +3962,48 @@ if (typeof console !== 'undefined') { * @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. + * @returns {Function} abort function */ function animateColor(fromColor, toColor, duration, options) { var startColor = new fabric.Color(fromColor).getSource(), - endColor = new fabric.Color(toColor).getSource(); - + endColor = new fabric.Color(toColor).getSource(), + originalOnComplete = options.onComplete, + originalOnChange = options.onChange; options = options || {}; - fabric.util.animate(fabric.util.object.extend(options, { + return 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)); + ? options.colorEasing(currentTime, duration) + : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); return calculateColor(startValue, byValue, posValue); + }, + // has to take in account for color restoring; + onComplete: function(current, valuePerc, timePerc) { + if (originalOnComplete) { + return originalOnComplete( + calculateColor(endColor, endColor, 0), + valuePerc, + timePerc + ); + } + }, + onChange: function(current, valuePerc, timePerc) { + if (originalOnChange) { + if (Array.isArray(current)) { + return originalOnChange( + calculateColor(current, current, 0), + valuePerc, + timePerc + ); + } + originalOnChange(current, valuePerc, timePerc); + } } })); } @@ -3248,10 +4429,11 @@ if (typeof console !== 'undefined') { parseUnit = fabric.util.parseUnit, multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, - reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/i, - reViewBoxTagNames = /^(symbol|image|marker|pattern|view|svg)$/i, - reNotAllowedAncestors = /^(?:pattern|defs|symbol|metadata|clipPath|mask)$/i, - reAllowedParents = /^(symbol|g|a|svg)$/i, + 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', @@ -3268,24 +4450,39 @@ if (typeof console !== 'undefined') { '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': 'originX', - opacity: 'opacity' + 'text-anchor': 'textAnchor', + opacity: 'opacity', + 'clip-path': 'clipPath', + 'clip-rule': 'clipRule', + 'vector-effect': 'strokeUniform', + 'image-rendering': 'imageSmoothing', }, colorAttributes = { stroke: 'strokeOpacity', fill: 'fillOpacity' - }; + }, + + fSize = 'font-size', cPath = 'clip-path'; + + 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 @@ -3296,20 +4493,20 @@ if (typeof console !== 'undefined') { } function normalizeValue(attr, value, parentAttributes, fontSize) { - var isArray = Object.prototype.toString.call(value) === '[object Array]', - parsed; + var isArray = Array.isArray(value), parsed; if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; } + else if (attr === 'strokeUniform') { + return (value === 'non-scaling-stroke'); + } else if (attr === 'strokeDashArray') { if (value === 'none') { value = null; } else { - value = value.replace(/,/g, ' ').split(/\s+/).map(function(n) { - return parseFloat(n); - }); + value = value.replace(/,/g, ' ').split(/\s+/).map(parseFloat); } } else if (attr === 'transformMatrix') { @@ -3322,7 +4519,7 @@ if (typeof console !== 'undefined') { } } else if (attr === 'visible') { - value = (value === 'none' || value === 'hidden') ? false : true; + value = value !== 'none' && value !== 'hidden'; // display=none on parent element always takes precedence over child element if (parentAttributes && parentAttributes.visible === false) { value = false; @@ -3334,9 +4531,30 @@ if (typeof console !== 'undefined') { value *= parentAttributes.opacity; } } - else if (attr === 'originX' /* text-anchor */) { + 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' || attr === 'font') { + return value; + } + else if (attr === 'imageSmoothing') { + return (value === 'optimizeQuality'); + } else { parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); } @@ -3344,6 +4562,13 @@ if (typeof console !== 'undefined') { 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 @@ -3376,8 +4601,8 @@ if (typeof console !== 'undefined') { * @private */ function _getMultipleNodes(doc, nodeNames) { - var nodeName, nodeArray = [], nodeList; - for (var i = 0; i < nodeNames.length; i++) { + 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)); @@ -3395,7 +4620,7 @@ if (typeof console !== 'undefined') { */ fabric.parseTransformAttribute = (function() { function rotateMatrix(matrix, args) { - var cos = Math.cos(args[0]), sin = Math.sin(args[0]), + var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]), x = 0, y = 0; if (args.length === 3) { x = args[1]; @@ -3430,19 +4655,12 @@ if (typeof console !== 'undefined') { } // identity matrix - var iMatrix = [ - 1, // a - 0, // b - 0, // c - 1, // d - 0, // e - 0 // f - ], + var iMatrix = fabric.iMatrix, // == begin transform regexp number = fabric.reNum, - commaWsp = '(?:\\s+,?\\s*|,\\s*)', + commaWsp = fabric.commaWsp, skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', @@ -3622,7 +4840,7 @@ if (typeof console !== 'undefined') { function selectorMatches(element, selector) { var nodeName = element.nodeName, classNames = element.getAttribute('class'), - id = element.getAttribute('id'), matcher; + 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'); @@ -3633,7 +4851,7 @@ if (typeof console !== 'undefined') { } if (classNames && selector.length) { classNames = classNames.split(' '); - for (var i = classNames.length; i--;) { + for (i = classNames.length; i--;) { matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); selector = selector.replace(matcher, ''); } @@ -3643,7 +4861,7 @@ if (typeof console !== 'undefined') { /** * @private - * to support IE8 missing getElementById on SVGdocument + * to support IE8 missing getElementById on SVGdocument and on node xmlDOM */ function elementById(doc, id) { var el; @@ -3651,8 +4869,8 @@ if (typeof console !== 'undefined') { if (el) { return el; } - var node, i, nodelist = doc.getElementsByTagName('*'); - for (i = 0; i < nodelist.length; i++) { + 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; @@ -3665,22 +4883,32 @@ if (typeof console !== 'undefined') { */ 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').substr(1), + xlinkAttribute = el.getAttribute('xlink:href') || el.getAttribute('href'); + + if (xlinkAttribute === null) { + return; + } + + var xlink = xlinkAttribute.slice(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, l; + parentNode, + oldLength = nodelist.length, attr, + j, + attrs, + len, + namespace = fabric.svgNS; applyViewboxTransform(el2); if (/^svg$/i.test(el2.nodeName)) { - var el3 = el2.ownerDocument.createElement('g'); - for (j = 0, attrs = el2.attributes, l = attrs.length; j < l; j++) { + var el3 = el2.ownerDocument.createElementNS(namespace, 'g'); + for (j = 0, attrs = el2.attributes, len = attrs.length; j < len; j++) { attr = attrs.item(j); - el3.setAttribute(attr.nodeName, attr.nodeValue); + el3.setAttributeNS(namespace, attr.nodeName, attr.nodeValue); } // el2.firstChild != null while (el2.firstChild) { @@ -3689,9 +4917,10 @@ if (typeof console !== 'undefined') { el2 = el3; } - for (j = 0, attrs = el.attributes, l = attrs.length; j < l; j++) { + 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') { + if (attr.nodeName === 'x' || attr.nodeName === 'y' || + attr.nodeName === 'xlink:href' || attr.nodeName === 'href') { continue; } @@ -3730,7 +4959,9 @@ if (typeof console !== 'undefined') { * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements */ function applyViewboxTransform(element) { - + if (!fabric.svgViewBoxElementsRegEx.test(element.nodeName)) { + return {}; + } var viewBoxAttr = element.getAttribute('viewBox'), scaleX = 1, scaleY = 1, @@ -3742,16 +4973,25 @@ if (typeof console !== 'undefined') { x = element.getAttribute('x') || 0, y = element.getAttribute('y') || 0, preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', - missingViewBox = (!viewBoxAttr || !reViewBoxTagNames.test(element.nodeName) - || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), + missingViewBox = (!viewBoxAttr || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), toBeParsed = missingViewBox && missingDimAttr, - parsedDim = { }, translateMatrix = ''; + parsedDim = { }, translateMatrix = '', widthDiff = 0, heightDiff = 0; parsedDim.width = 0; parsedDim.height = 0; parsedDim.toBeParsed = toBeParsed; + if (missingViewBox) { + if (((x || y) && element.parentNode && element.parentNode.nodeName !== '#document')) { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + matrix = (element.getAttribute('transform') || '') + translateMatrix; + element.setAttribute('transform', matrix); + element.removeAttribute('x'); + element.removeAttribute('y'); + } + } + if (toBeParsed) { return parsedDim; } @@ -3759,14 +4999,17 @@ if (typeof console !== 'undefined') { if (missingViewBox) { parsedDim.width = parseUnit(widthAttr); parsedDim.height = parseUnit(heightAttr); + // set a transform for elements that have x y and are inner(only) SVGs 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); @@ -3782,14 +5025,34 @@ if (typeof console !== 'undefined') { preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); if (preserveAspectRatio.alignX !== 'none') { //translate all container for the effect of Mid, Min, Max - scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); + 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) { + if ((x || y) && element.parentNode.nodeName !== '#document') { translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; } @@ -3797,11 +5060,12 @@ if (typeof console !== 'undefined') { ' 0' + ' 0 ' + scaleY + ' ' + - (minX * scaleX) + ' ' + - (minY * scaleY) + ') '; - + (minX * scaleX + widthDiff) + ' ' + + (minY * scaleY + heightDiff) + ') '; + // seems unused. + // parsedDim.viewboxTransform = fabric.parseTransformAttribute(matrix); if (element.nodeName === 'svg') { - el = element.ownerDocument.createElement('g'); + el = element.ownerDocument.createElementNS(fabric.svgNS, 'g'); // element.firstChild != null while (element.firstChild) { el.appendChild(element.firstChild); @@ -3810,9 +5074,10 @@ if (typeof console !== 'undefined') { } else { el = element; + el.removeAttribute('x'); + el.removeAttribute('y'); matrix = el.getAttribute('transform') + matrix; } - el.setAttribute('transform', matrix); return parsedDim; } @@ -3846,7 +5111,7 @@ if (typeof console !== 'undefined') { parseUseDirectives(doc); - var svgUid = fabric.Object.__uid++, + var svgUid = fabric.Object.__uid++, i, len, options = applyViewboxTransform(doc), descendants = fabric.util.toArray(doc.getElementsByTagName('*')); options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; @@ -3857,7 +5122,7 @@ if (typeof console !== 'undefined') { // https://github.com/ajaxorg/node-o3-xml/issues/21 descendants = doc.selectNodes('//*[name(.)!="svg"]'); var arr = []; - for (var i = 0, len = descendants.length; i < len; i++) { + for (i = 0, len = descendants.length; i < len; i++) { arr[i] = descendants[i]; } descendants = arr; @@ -3865,25 +5130,58 @@ if (typeof console !== 'undefined') { var elements = descendants.filter(function(el) { applyViewboxTransform(el); - return reAllowedSVGTagNames.test(el.nodeName.replace('svg:', '')) && - !hasAncestorWithNodeName(el, reNotAllowedAncestors); // http://www.w3.org/TR/SVG/struct.html#DefsElement + 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) { + fabric.parseElements(elements, function(instances, elements) { if (callback) { - callback(instances, options); + 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).slice(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*(' + @@ -3945,27 +5243,14 @@ if (typeof console !== 'undefined') { 'svg:linearGradient', 'svg:radialGradient'], elList = _getMultipleNodes(doc, tagArray), - el, j = 0, id, xlink, - gradientDefs = { }, idsToXlinkMap = { }; - + el, j = 0, gradientDefs = { }; j = elList.length; - while (j--) { el = elList[j]; - xlink = el.getAttribute('xlink:href'); - id = el.getAttribute('id'); - if (xlink) { - idsToXlinkMap[id] = xlink.substr(1); - } - gradientDefs[id] = el; - } - - for (id in idsToXlinkMap) { - var el2 = gradientDefs[idsToXlinkMap[id]].cloneNode(true); - el = gradientDefs[id]; - while (el2.firstChild) { - el.appendChild(el2.firstChild); + if (el.getAttribute('xlink:href')) { + recursivelyParseGradientsXlink(doc, el); } + gradientDefs[el.getAttribute('id')] = el; } return gradientDefs; }, @@ -3987,17 +5272,15 @@ if (typeof console !== 'undefined') { var value, parentAttributes = { }, - fontSize; + 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 && reAllowedParents.test(element.parentNode.nodeName)) { + if (element.parentNode && fabric.svgValidParentsRegEx.test(element.parentNode.nodeName)) { parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); } - fontSize = (parentAttributes && parentAttributes.fontSize ) || - element.getAttribute('font-size') || fabric.Text.DEFAULT_SVG_FONT_SIZE; var ownAttributes = attributes.reduce(function(memo, attr) { value = element.getAttribute(attr); @@ -4008,8 +5291,22 @@ if (typeof console !== 'undefined') { }, { }); // 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))); + var cssAttrs = extend( + getGlobalStylesForElement(element, svgUid), + fabric.parseStyleAttribute(element) + ); + ownAttributes = extend( + ownAttributes, + cssAttrs + ); + if (cssAttrs[cPath]) { + element.setAttribute(cPath, cssAttrs[cPath]); + } + fontSize = parentFontSize = parentAttributes.fontSize || fabric.Text.DEFAULT_SVG_FONT_SIZE; + if (ownAttributes[fSize]) { + // looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers. + ownAttributes[fSize] = fontSize = parseUnit(ownAttributes[fSize], parentFontSize); + } var normalizedAttr, normalizedValue, normalizedStyle = {}; for (var attr in ownAttributes) { @@ -4021,7 +5318,7 @@ if (typeof console !== 'undefined') { fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); } var mergedAttrs = extend(parentAttributes, normalizedStyle); - return reAllowedParents.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); + return fabric.svgValidParentsRegEx.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); }, /** @@ -4082,9 +5379,7 @@ if (typeof console !== 'undefined') { points = points.split(/\s+/); var parsedPoints = [], i, len; - i = 0; - len = points.length; - for (; i < len; i += 2) { + for (i = 0, len = points.length; i < len; i += 2) { parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i + 1]) @@ -4108,34 +5403,38 @@ if (typeof console !== 'undefined') { * @return {Object} CSS rules of this document */ getCSSRules: function(doc) { - var styles = doc.getElementsByTagName('style'), + var styles = doc.getElementsByTagName('style'), i, len, allRules = { }, rules; // very crude parsing of style contents - for (var 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; + for (i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[i].textContent; // 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(); }); + // recovers all the rule in this form `body { style code... }` + // rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = styleContents.split('}'); + // remove empty rules. + rules = rules.filter(function(rule) { return rule.trim(); }); + // at this point we have hopefully an array of rules `body { style code... ` + // 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*/); + var match = rule.split('{'), + ruleObj = { }, declaration = match[1].trim(), + propertyValuePairs = declaration.split(';').filter(function(pair) { return pair.trim(); }); - for (var i = 0, len = propertyValuePairs.length; i < len; i++) { - var pair = propertyValuePairs[i].split(/\s*:\s*/), - property = pair[0], - value = pair[1]; + for (i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(':'), + property = pair[0].trim(), + value = pair[1].trim(); ruleObj[property] = value; } - rule = match[1]; + rule = match[0].trim(); rule.split(',').forEach(function(_rule) { _rule = _rule.replace(/^svg/i, '').trim(); if (_rule === '') { @@ -4174,18 +5473,13 @@ if (typeof console !== 'undefined') { 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) { - callback && callback(results, _options); + fabric.parseSVGDocument(xml.documentElement, function (results, _options, elements, allElements) { + callback && callback(results, _options, elements, allElements); }, reviver, options); } }, @@ -4200,23 +5494,10 @@ if (typeof console !== 'undefined') { * @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 DOMParser !== 'undefined') { - var parser = new 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) { - callback(results, _options); + var parser = new fabric.window.DOMParser(), + doc = parser.parseFromString(string.trim(), 'text/xml'); + fabric.parseSVGDocument(doc.documentElement, function (results, _options, elements, allElements) { + callback(results, _options, elements, allElements); }, reviver, options); } }); @@ -4224,95 +5505,158 @@ if (typeof console !== 'undefined') { })(typeof exports !== 'undefined' ? exports : this); -fabric.ElementsParser = function(elements, callback, options, reviver, parsingOptions) { +fabric.ElementsParser = function(elements, callback, options, reviver, parsingOptions, doc) { 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; + this.doc = doc; }; -fabric.ElementsParser.prototype.parse = function() { - this.instances = new Array(this.elements.length); - this.numElements = this.elements.length; - - this.createObjects(); -}; - -fabric.ElementsParser.prototype.createObjects = function() { - for (var i = 0, len = this.elements.length; i < len; i++) { - this.elements[i].setAttribute('svgUid', this.svgUid); - (function(_obj, i) { - setTimeout(function() { - _obj.createObject(_obj.elements[i], i); - }, 0); - })(this, i); - } -}; - -fabric.ElementsParser.prototype.createObject = function(el, index) { - var klass = fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))]; - if (klass && klass.fromElement) { - try { - this._createObject(klass, el, index); - } - catch (err) { - fabric.log(err); - } - } - else { - this.checkIfDone(); - } -}; - -fabric.ElementsParser.prototype._createObject = function(klass, el, index) { - if (klass.async) { - klass.fromElement(el, this.createCallback(index, el), this.options); - } - else { - var obj = klass.fromElement(el, this.options); - this.resolveGradient(obj, 'fill'); - this.resolveGradient(obj, 'stroke'); - this.reviver && this.reviver(el, obj); - this.instances[index] = obj; - this.checkIfDone(); - } -}; - -fabric.ElementsParser.prototype.createCallback = function(index, el) { - var _this = this; - return function(obj) { - _this.resolveGradient(obj, 'fill'); - _this.resolveGradient(obj, 'stroke'); - _this.reviver && _this.reviver(el, obj); - _this.instances[index] = obj; - _this.checkIfDone(); +(function(proto) { + proto.parse = function() { + this.instances = new Array(this.elements.length); + this.numElements = this.elements.length; + this.createObjects(); }; -}; -fabric.ElementsParser.prototype.resolveGradient = function(obj, property) { - - var instanceFillValue = obj.get(property); - if (!(/^url\(/).test(instanceFillValue)) { - return; - } - var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - if (fabric.gradientDefs[this.svgUid][gradientId]) { - obj.set(property, - fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][gradientId], obj)); - } -}; - -fabric.ElementsParser.prototype.checkIfDone = function() { - if (--this.numElements === 0) { - this.instances = this.instances.filter(function(el) { - // eslint-disable-next-line no-eq-null, eqeqeq - return el != null; + proto.createObjects = function() { + var _this = this; + this.elements.forEach(function(element, i) { + element.setAttribute('svgUid', _this.svgUid); + _this.createObject(element, i); }); - this.callback(this.instances); - } -}; + }; + + 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, el); + _this.reviver && _this.reviver(el, obj); + _this.instances[index] = obj; + _this.checkIfDone(); + }; + }; + + proto.extractPropertyDefinition = function(obj, property, storage) { + var value = obj[property], regex = this.regexUrl; + if (!regex.test(value)) { + return; + } + regex.lastIndex = 0; + var id = regex.exec(value)[1]; + regex.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, usingElement) { + var clipPath = this.extractPropertyDefinition(obj, 'clipPath', 'clipPaths'), + element, klass, objTransformInv, container, gTransform, options; + if (clipPath) { + container = []; + objTransformInv = fabric.util.invertTransform(obj.calcTransformMatrix()); + // move the clipPath tag as sibling to the real element that is using it + var clipPathTag = clipPath[0].parentNode; + var clipPathOwner = usingElement; + while (clipPathOwner.parentNode && clipPathOwner.getAttribute('clip-path') !== obj.clipPath) { + clipPathOwner = clipPathOwner.parentNode; + } + clipPathOwner.parentNode.appendChild(clipPathTag); + 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() + ); + if (clipPath.clipPath) { + this.resolveClipPath(clipPath, clipPathOwner); + } + 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; + } + else { + // if clip-path does not resolve to any element, delete the property. + delete obj.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) { @@ -4435,7 +5779,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { }, /** - * Miltiplies this point by a value and returns a new one + * Multiplies this point by a value and returns a new one * TODO: rename in scalarMultiply in 2.0 * @param {Number} scalar * @return {fabric.Point} @@ -4445,7 +5789,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { }, /** - * Miltiplies this point by a value + * Multiplies this point by a value * TODO: rename in scalarMultiplyEquals in 2.0 * @param {Number} scalar * @return {fabric.Point} thisArg @@ -4756,9 +6100,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { fabric.Intersection.intersectLinePolygon = function(a1, a2, points) { var result = new Intersection(), length = points.length, - b1, b2, inter; + b1, b2, inter, i; - for (var i = 0; i < length; i++) { + for (i = 0; i < length; i++) { b1 = points[i]; b2 = points[(i + 1) % length]; inter = Intersection.intersectLineLine(a1, a2, b1, b2); @@ -4780,9 +6124,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { var result = new Intersection(), - length = points1.length; + length = points1.length, i; - for (var i = 0; i < length; i++) { + for (i = 0; i < length; i++) { var a1 = points1[i], a2 = points1[(i + 1) % length], inter = Intersection.intersectLinePolygon(a1, a2, points2); @@ -5021,7 +6365,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { toHexa: function() { var source = this.getSource(), a; - a = source[3] * 255; + a = Math.round(source[3] * 255); a = a.toString(16); a = (a.length === 1) ? ('0' + a) : a; @@ -5091,9 +6435,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { alpha = this.getAlpha(), otherAlpha = 0.5, source = this.getSource(), - otherSource = otherColor.getSource(); + otherSource = otherColor.getSource(), i; - for (var i = 0; i < 3; i++) { + for (i = 0; i < 3; i++) { result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); } @@ -5109,8 +6453,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @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*)?\)$/; + // 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 )) @@ -5118,7 +6462,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @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*)?\)$/; + 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) @@ -5129,31 +6473,161 @@ fabric.ElementsParser.prototype.checkIfDone = function() { fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; /** - * Map of the 17 basic color names with HEX code + * Map of the 148 color names with HEX code * @static * @field * @memberOf fabric.Color - * @see: http://www.w3.org/TR/CSS2/syndata.html#color-units + * @see: https://www.w3.org/TR/css3-color/#svg-color */ fabric.Color.colorNameMap = { - aqua: '#00FFFF', - black: '#000000', - blue: '#0000FF', - fuchsia: '#FF00FF', - gray: '#808080', - grey: '#808080', - green: '#008000', - lime: '#00FF00', - maroon: '#800000', - navy: '#000080', - olive: '#808000', - orange: '#FFA500', - purple: '#800080', - red: '#FF0000', - silver: '#C0C0C0', - teal: '#008080', - white: '#FFFFFF', - yellow: '#FFFF00' + 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' }; /** @@ -5336,13 +6810,1197 @@ fabric.ElementsParser.prototype.checkIfDone = function() { })(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + scaleMap = ['e', 'se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'], + skewMap = ['ns', 'nesw', 'ew', 'nwse'], + controls = {}, + LEFT = 'left', TOP = 'top', RIGHT = 'right', BOTTOM = 'bottom', CENTER = 'center', + opposite = { + top: BOTTOM, + bottom: TOP, + left: RIGHT, + right: LEFT, + center: CENTER, + }, radiansToDegrees = fabric.util.radiansToDegrees, + sign = (Math.sign || function(x) { return ((x > 0) - (x < 0)) || +x; }); + + /** + * Combine control position and object angle to find the control direction compared + * to the object center. + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + * @param {fabric.Control} control the control class + * @return {Number} 0 - 7 a quadrant number + */ + function findCornerQuadrant(fabricObject, control) { + var cornerAngle = fabricObject.angle + radiansToDegrees(Math.atan2(control.y, control.x)) + 360; + return Math.round((cornerAngle % 360) / 45); + } + + function fireEvent(eventName, options) { + var target = options.transform.target, + canvas = target.canvas, + canvasOptions = fabric.util.object.clone(options); + canvasOptions.target = target; + canvas && canvas.fire('object:' + eventName, canvasOptions); + target.fire(eventName, options); + } + + /** + * Inspect event and fabricObject properties to understand if the scaling action + * @param {Event} eventData from the user action + * @param {fabric.Object} fabricObject the fabric object about to scale + * @return {Boolean} true if scale is proportional + */ + function scaleIsProportional(eventData, fabricObject) { + var canvas = fabricObject.canvas, uniScaleKey = canvas.uniScaleKey, + uniformIsToggled = eventData[uniScaleKey]; + return (canvas.uniformScaling && !uniformIsToggled) || + (!canvas.uniformScaling && uniformIsToggled); + } + + /** + * Checks if transform is centered + * @param {Object} transform transform data + * @return {Boolean} true if transform is centered + */ + function isTransformCentered(transform) { + return transform.originX === CENTER && transform.originY === CENTER; + } + + /** + * Inspect fabricObject to understand if the current scaling action is allowed + * @param {fabric.Object} fabricObject the fabric object about to scale + * @param {String} by 'x' or 'y' or '' + * @param {Boolean} scaleProportionally true if we are trying to scale proportionally + * @return {Boolean} true if scaling is not allowed at current conditions + */ + function scalingIsForbidden(fabricObject, by, scaleProportionally) { + var lockX = fabricObject.lockScalingX, lockY = fabricObject.lockScalingY; + if (lockX && lockY) { + return true; + } + if (!by && (lockX || lockY) && scaleProportionally) { + return true; + } + if (lockX && by === 'x') { + return true; + } + if (lockY && by === 'y') { + return true; + } + return false; + } + + /** + * return the correct cursor style for the scale action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed', + scaleProportionally = scaleIsProportional(eventData, fabricObject), + by = ''; + if (control.x !== 0 && control.y === 0) { + by = 'x'; + } + else if (control.x === 0 && control.y !== 0) { + by = 'y'; + } + if (scalingIsForbidden(fabricObject, by, scaleProportionally)) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control); + return scaleMap[n] + '-resize'; + } + + /** + * return the correct cursor style for the skew action + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function skewCursorStyleHandler(eventData, control, fabricObject) { + var notAllowed = 'not-allowed'; + if (control.x !== 0 && fabricObject.lockSkewingY) { + return notAllowed; + } + if (control.y !== 0 && fabricObject.lockSkewingX) { + return notAllowed; + } + var n = findCornerQuadrant(fabricObject, control) % 4; + return skewMap[n] + '-resize'; + } + + /** + * Combine skew and scale style handlers to cover fabric standard use case + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function scaleSkewCursorStyleHandler(eventData, control, fabricObject) { + if (eventData[fabricObject.canvas.altActionKey]) { + return controls.skewCursorStyleHandler(eventData, control, fabricObject); + } + return controls.scaleCursorStyleHandler(eventData, control, fabricObject); + } + + /** + * Inspect event, control and fabricObject to return the correct action name + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} an action name + */ + function scaleOrSkewActionName(eventData, control, fabricObject) { + var isAlternative = eventData[fabricObject.canvas.altActionKey]; + if (control.x === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewX' : 'scaleY'; + } + if (control.y === 0) { + // then is scaleY or skewX + return isAlternative ? 'skewY' : 'scaleX'; + } + } + + /** + * Find the correct style for the control that is used for rotation. + * this function is very simple and it just take care of not-allowed or standard cursor + * @param {Event} eventData the javascript event that is causing the scale + * @param {fabric.Control} control the control that is interested in the action + * @param {fabric.Object} fabricObject the fabric object that is interested in the action + * @return {String} a valid css string for the cursor + */ + function rotationStyleHandler(eventData, control, fabricObject) { + if (fabricObject.lockRotation) { + return 'not-allowed'; + } + return control.cursorStyle; + } + + function commonEventInfo(eventData, transform, x, y) { + return { + e: eventData, + transform: transform, + pointer: { + x: x, + y: y, + } + }; + } + + /** + * Wrap an action handler with saving/restoring object position on the transform. + * this is the code that permits to objects to keep their position while transforming. + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFixedAnchor(actionHandler) { + return function(eventData, transform, x, y) { + var target = transform.target, centerPoint = target.getCenterPoint(), + constraint = target.translateToOriginPoint(centerPoint, transform.originX, transform.originY), + actionPerformed = actionHandler(eventData, transform, x, y); + target.setPositionByOrigin(constraint, transform.originX, transform.originY); + return actionPerformed; + }; + } + + /** + * Wrap an action handler with firing an event if the action is performed + * @param {Function} actionHandler the function to wrap + * @return {Function} a function with an action handler signature + */ + function wrapWithFireEvent(eventName, actionHandler) { + return function(eventData, transform, x, y) { + var actionPerformed = actionHandler(eventData, transform, x, y); + if (actionPerformed) { + fireEvent(eventName, commonEventInfo(eventData, transform, x, y)); + } + return actionPerformed; + }; + } + + /** + * Transforms a point described by x and y in a distance from the top left corner of the object + * bounding box. + * @param {Object} transform + * @param {String} originX + * @param {String} originY + * @param {number} x + * @param {number} y + * @return {Fabric.Point} the normalized point + */ + function getLocalPoint(transform, originX, originY, x, y) { + var target = transform.target, + control = target.controls[transform.corner], + zoom = target.canvas.getZoom(), + padding = target.padding / zoom, + localPoint = target.toLocalPoint(new fabric.Point(x, y), originX, originY); + if (localPoint.x >= padding) { + localPoint.x -= padding; + } + if (localPoint.x <= -padding) { + localPoint.x += padding; + } + if (localPoint.y >= padding) { + localPoint.y -= padding; + } + if (localPoint.y <= padding) { + localPoint.y += padding; + } + localPoint.x -= control.offsetX; + localPoint.y -= control.offsetY; + return localPoint; + } + + /** + * Detect if the fabric object is flipped on one side. + * @param {fabric.Object} target + * @return {Boolean} true if one flip, but not two. + */ + function targetHasOneFlip(target) { + return target.flipX !== target.flipY; + } + + /** + * Utility function to compensate the scale factor when skew is applied on both axes + * @private + */ + function compensateScaleForSkew(target, oppositeSkew, scaleToCompensate, axis, reference) { + if (target[oppositeSkew] !== 0) { + var newDim = target._getTransformedDimensions()[axis]; + var newValue = reference / newDim * target[scaleToCompensate]; + target.set(scaleToCompensate, newValue); + } + } + + /** + * Action handler for skewing on the X axis + * @private + */ + function skewObjectX(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(0, target.skewY), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.x * 2) - dimNoSkew.x, + currentSkew = target.skewX, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleX), (dimNoSkew.y / target.scaleY)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().y; + target.set('skewX', newSkew); + compensateScaleForSkew(target, 'skewY', 'scaleY', 'y', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Action handler for skewing on the Y axis + * @private + */ + function skewObjectY(eventData, transform, x, y) { + var target = transform.target, + // find how big the object would be, if there was no skewX. takes in account scaling + dimNoSkew = target._getTransformedDimensions(target.skewX, 0), + localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + // the mouse is in the center of the object, and we want it to stay there. + // so the object will grow twice as much as the mouse. + // this makes the skew growth to localPoint * 2 - dimNoSkew. + totalSkewSize = Math.abs(localPoint.y * 2) - dimNoSkew.y, + currentSkew = target.skewY, newSkew; + if (totalSkewSize < 2) { + // let's make it easy to go back to position 0. + newSkew = 0; + } + else { + newSkew = radiansToDegrees( + Math.atan2((totalSkewSize / target.scaleY), (dimNoSkew.x / target.scaleX)) + ); + // now we have to find the sign of the skew. + // it mostly depend on the origin of transformation. + if (transform.originX === LEFT && transform.originY === BOTTOM) { + newSkew = -newSkew; + } + if (transform.originX === RIGHT && transform.originY === TOP) { + newSkew = -newSkew; + } + if (targetHasOneFlip(target)) { + newSkew = -newSkew; + } + } + var hasSkewed = currentSkew !== newSkew; + if (hasSkewed) { + var dimBeforeSkewing = target._getTransformedDimensions().x; + target.set('skewY', newSkew); + compensateScaleForSkew(target, 'skewX', 'scaleX', 'x', dimBeforeSkewing); + } + return hasSkewed; + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerX(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewX > 0 and originY bottom we anchor on right + // if skewX > 0 and originY top we anchor on left + // if skewX < 0 and originY bottom we anchor on left + // if skewX < 0 and originY top we anchor on right + // if skewX is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewX, originX, originY = transform.originY; + if (target.lockSkewingX) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.x > 0) { + // we are pulling right, anchor left; + originX = LEFT; + } + else { + // we are pulling right, anchor right + originX = RIGHT; + } + } + else { + if (currentSkew > 0) { + originX = originY === TOP ? LEFT : RIGHT; + } + if (currentSkew < 0) { + originX = originY === TOP ? RIGHT : LEFT; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originX = originX === LEFT ? RIGHT : LEFT; + } + } + + // once we have the origin, we find the anchor point + transform.originX = originX; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectX)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Wrapped Action handler for skewing on the Y axis, takes care of the + * skew direction and determine the correct transform origin for the anchor point + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function skewHandlerY(eventData, transform, x, y) { + // step1 figure out and change transform origin. + // if skewY > 0 and originX left we anchor on top + // if skewY > 0 and originX right we anchor on bottom + // if skewY < 0 and originX left we anchor on bottom + // if skewY < 0 and originX right we anchor on top + // if skewY is 0, we look for mouse position to understand where are we going. + var target = transform.target, currentSkew = target.skewY, originY, originX = transform.originX; + if (target.lockSkewingY) { + return false; + } + if (currentSkew === 0) { + var localPointFromCenter = getLocalPoint(transform, CENTER, CENTER, x, y); + if (localPointFromCenter.y > 0) { + // we are pulling down, anchor up; + originY = TOP; + } + else { + // we are pulling up, anchor down + originY = BOTTOM; + } + } + else { + if (currentSkew > 0) { + originY = originX === LEFT ? TOP : BOTTOM; + } + if (currentSkew < 0) { + originY = originX === LEFT ? BOTTOM : TOP; + } + // is the object flipped on one side only? swap the origin. + if (targetHasOneFlip(target)) { + originY = originY === TOP ? BOTTOM : TOP; + } + } + + // once we have the origin, we find the anchor point + transform.originY = originY; + var finalHandler = wrapWithFireEvent('skewing', wrapWithFixedAnchor(skewObjectY)); + return finalHandler(eventData, transform, x, y); + } + + /** + * Action handler for rotation and snapping, without anchor point. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + * @private + */ + function rotationWithSnapping(eventData, transform, x, y) { + var t = transform, + target = t.target, + pivotPoint = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY); + + if (target.lockRotation) { + return false; + } + + var lastAngle = Math.atan2(t.ey - pivotPoint.y, t.ex - pivotPoint.x), + curAngle = Math.atan2(y - pivotPoint.y, x - pivotPoint.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; + + hasRotated = target.angle !== angle; + target.angle = angle; + return hasRotated; + } + + /** + * Basic scaling logic, reused with different constrain for scaling X,Y, freely or equally. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @param {Object} options additional information for scaling + * @param {String} options.by 'x', 'y', 'equally' or '' to indicate type of scaling + * @return {Boolean} true if some change happened + * @private + */ + function scaleObject(eventData, transform, x, y, options) { + options = options || {}; + var target = transform.target, + lockScalingX = target.lockScalingX, lockScalingY = target.lockScalingY, + by = options.by, newPoint, scaleX, scaleY, dim, + scaleProportionally = scaleIsProportional(eventData, target), + forbidScaling = scalingIsForbidden(target, by, scaleProportionally), + signX, signY, gestureScale = transform.gestureScale; + + if (forbidScaling) { + return false; + } + if (gestureScale) { + scaleX = transform.scaleX * gestureScale; + scaleY = transform.scaleY * gestureScale; + } + else { + newPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y); + // use of sign: We use sign to detect change of direction of an action. sign usually change when + // we cross the origin point with the mouse. So a scale flip for example. There is an issue when scaling + // by center and scaling using one middle control ( default: mr, mt, ml, mb), the mouse movement can easily + // cross many time the origin point and flip the object. so we need a way to filter out the noise. + // This ternary here should be ok to filter out X scaling when we want Y only and vice versa. + signX = by !== 'y' ? sign(newPoint.x) : 1; + signY = by !== 'x' ? sign(newPoint.y) : 1; + if (!transform.signX) { + transform.signX = signX; + } + if (!transform.signY) { + transform.signY = signY; + } + + if (target.lockScalingFlip && + (transform.signX !== signX || transform.signY !== signY) + ) { + return false; + } + + dim = target._getTransformedDimensions(); + // missing detection of flip and logic to switch the origin + if (scaleProportionally && !by) { + // uniform scaling + var distance = Math.abs(newPoint.x) + Math.abs(newPoint.y), + original = transform.original, + originalDistance = Math.abs(dim.x * original.scaleX / target.scaleX) + + Math.abs(dim.y * original.scaleY / target.scaleY), + scale = distance / originalDistance; + scaleX = original.scaleX * scale; + scaleY = original.scaleY * scale; + } + else { + scaleX = Math.abs(newPoint.x * target.scaleX / dim.x); + scaleY = Math.abs(newPoint.y * target.scaleY / dim.y); + } + // if we are scaling by center, we need to double the scale + if (isTransformCentered(transform)) { + scaleX *= 2; + scaleY *= 2; + } + if (transform.signX !== signX && by !== 'y') { + transform.originX = opposite[transform.originX]; + scaleX *= -1; + transform.signX = signX; + } + if (transform.signY !== signY && by !== 'x') { + transform.originY = opposite[transform.originY]; + scaleY *= -1; + transform.signY = signY; + } + } + // minScale is taken are in the setter. + var oldScaleX = target.scaleX, oldScaleY = target.scaleY; + if (!by) { + !lockScalingX && target.set('scaleX', scaleX); + !lockScalingY && target.set('scaleY', scaleY); + } + else { + // forbidden cases already handled on top here. + by === 'x' && target.set('scaleX', scaleX); + by === 'y' && target.set('scaleY', scaleY); + } + return oldScaleX !== target.scaleX || oldScaleY !== target.scaleY; + } + + /** + * Generic scaling logic, to scale from corners either equally or freely. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectFromCorner(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y); + } + + /** + * Scaling logic for the X axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectX(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'x' }); + } + + /** + * Scaling logic for the Y axis. + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scaleObjectY(eventData, transform, x, y) { + return scaleObject(eventData, transform, x, y , { by: 'y' }); + } + + /** + * Composed action handler to either scale Y or skew X + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingYOrSkewingX(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerX(eventData, transform, x, y); + } + return controls.scalingY(eventData, transform, x, y); + } + + /** + * Composed action handler to either scale X or skew Y + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function scalingXOrSkewingY(eventData, transform, x, y) { + // ok some safety needed here. + if (eventData[transform.target.canvas.altActionKey]) { + return controls.skewHandlerY(eventData, transform, x, y); + } + return controls.scalingX(eventData, transform, x, y); + } + + /** + * Action handler to change textbox width + * Needs to be wrapped with `wrapWithFixedAnchor` to be effective + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if some change happened + */ + function changeWidth(eventData, transform, x, y) { + var target = transform.target, localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y), + strokePadding = target.strokeWidth / (target.strokeUniform ? target.scaleX : 1), + multiplier = isTransformCentered(transform) ? 2 : 1, + oldWidth = target.width, + newWidth = Math.abs(localPoint.x * multiplier / target.scaleX) - strokePadding; + target.set('width', Math.max(newWidth, 0)); + return oldWidth !== newWidth; + } + + /** + * Action handler + * @private + * @param {Event} eventData javascript event that is doing the transform + * @param {Object} transform javascript object containing a series of information around the current transform + * @param {number} x current mouse x position, canvas normalized + * @param {number} y current mouse y position, canvas normalized + * @return {Boolean} true if the translation occurred + */ + function dragHandler(eventData, transform, x, y) { + var 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); + if (moveX || moveY) { + fireEvent('moving', commonEventInfo(eventData, transform, x, y)); + } + return moveX || moveY; + } + + controls.scaleCursorStyleHandler = scaleCursorStyleHandler; + controls.skewCursorStyleHandler = skewCursorStyleHandler; + controls.scaleSkewCursorStyleHandler = scaleSkewCursorStyleHandler; + controls.rotationWithSnapping = wrapWithFireEvent('rotating', wrapWithFixedAnchor(rotationWithSnapping)); + controls.scalingEqually = wrapWithFireEvent('scaling', wrapWithFixedAnchor( scaleObjectFromCorner)); + controls.scalingX = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectX)); + controls.scalingY = wrapWithFireEvent('scaling', wrapWithFixedAnchor(scaleObjectY)); + controls.scalingYOrSkewingX = scalingYOrSkewingX; + controls.scalingXOrSkewingY = scalingXOrSkewingY; + controls.changeWidth = wrapWithFireEvent('resizing', wrapWithFixedAnchor(changeWidth)); + controls.skewHandlerX = skewHandlerX; + controls.skewHandlerY = skewHandlerY; + controls.dragHandler = dragHandler; + controls.scaleOrSkewActionName = scaleOrSkewActionName; + controls.rotationStyleHandler = rotationStyleHandler; + controls.fireEvent = fireEvent; + controls.wrapWithFixedAnchor = wrapWithFixedAnchor; + controls.wrapWithFireEvent = wrapWithFireEvent; + controls.getLocalPoint = getLocalPoint; + fabric.controlsUtils = controls; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + degreesToRadians = fabric.util.degreesToRadians, + controls = fabric.controlsUtils; + + /** + * Render a round control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderCircleControl (ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && (styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor), + myLeft = left, + myTop = top, size; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // as soon as fabric react v5, remove ie11, use proper ellipse code. + if (xSize > ySize) { + size = xSize; + ctx.scale(1.0, ySize / xSize); + myTop = top * xSize / ySize; + } + else if (ySize > xSize) { + size = ySize; + ctx.scale(xSize / ySize, 1.0); + myLeft = left * ySize / xSize; + } + else { + size = xSize; + } + // this is still wrong + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(myLeft, myTop, size / 2, 0, 2 * Math.PI, false); + ctx[methodName](); + if (stroke) { + ctx.stroke(); + } + ctx.restore(); + } + + /** + * Render a square control, as per fabric features. + * This function is written to respect object properties like transparentCorners, cornerSize + * cornerColor, cornerStrokeColor + * plus the addition of offsetY and offsetX. + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Number} left x coordinate where the control center should be + * @param {Number} top y coordinate where the control center should be + * @param {Object} styleOverride override for fabric.Object controls style + * @param {fabric.Object} fabricObject the fabric object for which we are rendering controls + */ + function renderSquareControl(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + var xSize = this.sizeX || styleOverride.cornerSize || fabricObject.cornerSize, + ySize = this.sizeY || styleOverride.cornerSize || fabricObject.cornerSize, + transparentCorners = typeof styleOverride.transparentCorners !== 'undefined' ? + styleOverride.transparentCorners : fabricObject.transparentCorners, + methodName = transparentCorners ? 'stroke' : 'fill', + stroke = !transparentCorners && ( + styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor + ), xSizeBy2 = xSize / 2, ySizeBy2 = ySize / 2; + ctx.save(); + ctx.fillStyle = styleOverride.cornerColor || fabricObject.cornerColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || fabricObject.cornerStrokeColor; + // this is still wrong + ctx.lineWidth = 1; + ctx.translate(left, top); + ctx.rotate(degreesToRadians(fabricObject.angle)); + // this does not work, and fixed with ( && ) does not make sense. + // to have real transparent corners we need the controls on upperCanvas + // transparentCorners || ctx.clearRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + ctx[methodName + 'Rect'](-xSizeBy2, -ySizeBy2, xSize, ySize); + if (stroke) { + ctx.strokeRect(-xSizeBy2, -ySizeBy2, xSize, ySize); + } + ctx.restore(); + } + + controls.renderCircleControl = renderCircleControl; + controls.renderSquareControl = renderSquareControl; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }); + + function Control(options) { + for (var i in options) { + this[i] = options[i]; + } + } + + fabric.Control = Control; + + fabric.Control.prototype = /** @lends fabric.Control.prototype */ { + + /** + * keep track of control visibility. + * mainly for backward compatibility. + * if you do not want to see a control, you can remove it + * from the controlset. + * @type {Boolean} + * @default true + */ + visible: true, + + /** + * Name of the action that the control will likely execute. + * This is optional. FabricJS uses to identify what the user is doing for some + * extra optimizations. If you are writing a custom control and you want to know + * somewhere else in the code what is going on, you can use this string here. + * you can also provide a custom getActionName if your control run multiple actions + * depending on some external state. + * default to scale since is the most common, used on 4 corners by default + * @type {String} + * @default 'scale' + */ + actionName: 'scale', + + /** + * Drawing angle of the control. + * NOT used for now, but name marked as needed for internal logic + * example: to reuse the same drawing function for different rotated controls + * @type {Number} + * @default 0 + */ + angle: 0, + + /** + * Relative position of the control. X + * 0,0 is the center of the Object, while -0.5 (left) or 0.5 (right) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + x: 0, + + /** + * Relative position of the control. Y + * 0,0 is the center of the Object, while -0.5 (top) or 0.5 (bottom) are the extremities + * of the bounding box. + * @type {Number} + * @default 0 + */ + y: 0, + + /** + * Horizontal offset of the control from the defined position. In pixels + * Positive offset moves the control to the right, negative to the left. + * It used when you want to have position of control that does not scale with + * the bounding box. Example: rotation control is placed at x:0, y: 0.5 on + * the boundindbox, with an offset of 30 pixels vertically. Those 30 pixels will + * stay 30 pixels no matter how the object is big. Another example is having 2 + * controls in the corner, that stay in the same position when the object scale. + * of the bounding box. + * @type {Number} + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset of the control from the defined position. In pixels + * Positive offset moves the control to the bottom, negative to the top. + * @type {Number} + * @default 0 + */ + offsetY: 0, + + /** + * Sets the length of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeX: null, + + /** + * Sets the height of the control. If null, defaults to object's cornerSize. + * Expects both sizeX and sizeY to be set when set. + * @type {?Number} + * @default null + */ + sizeY: null, + + /** + * Sets the length of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeX: null, + + /** + * Sets the height of the touch area of the control. If null, defaults to object's touchCornerSize. + * Expects both touchSizeX and touchSizeY to be set when set. + * @type {?Number} + * @default null + */ + touchSizeY: null, + + /** + * Css cursor style to display when the control is hovered. + * if the method `cursorStyleHandler` is provided, this property is ignored. + * @type {String} + * @default 'crosshair' + */ + cursorStyle: 'crosshair', + + /** + * If controls has an offsetY or offsetX, draw a line that connects + * the control to the bounding box + * @type {Boolean} + * @default false + */ + withConnection: false, + + /** + * The control actionHandler, provide one to handle action ( control being moved ) + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + actionHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control handler for mouse down, provide one to handle mouse down on control + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseDownHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * The control mouseUpHandler, provide one to handle an effect on mouse up. + * @param {Event} eventData the native mouse event + * @param {Object} transformData properties of the current transform + * @param {Number} x x position of the cursor + * @param {Number} y y position of the cursor + * @return {Boolean} true if the action/event modified the object + */ + mouseUpHandler: function(/* eventData, transformData, x, y */) { }, + + /** + * Returns control actionHandler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getActionHandler: function(/* eventData, fabricObject, control */) { + return this.actionHandler; + }, + + /** + * Returns control mouseDown handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseDownHandler: function(/* eventData, fabricObject, control */) { + return this.mouseDownHandler; + }, + + /** + * Returns control mouseUp handler + * @param {Event} eventData the native mouse event + * @param {fabric.Object} fabricObject on which the control is displayed + * @param {fabric.Control} control control for which the action handler is being asked + * @return {Function} the action handler + */ + getMouseUpHandler: function(/* eventData, fabricObject, control */) { + return this.mouseUpHandler; + }, + + /** + * Returns control cursorStyle for css using cursorStyle. If you need a more elaborate + * function you can pass one in the constructor + * the cursorStyle property + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + cursorStyleHandler: function(eventData, control /* fabricObject */) { + return control.cursorStyle; + }, + + /** + * Returns the action name. The basic implementation just return the actionName property. + * @param {Event} eventData the native mouse event + * @param {fabric.Control} control the current control ( likely this) + * @param {fabric.Object} object on which the control is displayed + * @return {String} + */ + getActionName: function(eventData, control /* fabricObject */) { + return control.actionName; + }, + + /** + * Returns controls visibility + * @param {fabric.Object} object on which the control is displayed + * @param {String} controlKey key where the control is memorized on the + * @return {Boolean} + */ + getVisibility: function(fabricObject, controlKey) { + var objectVisibility = fabricObject._controlsVisibility; + if (objectVisibility && typeof objectVisibility[controlKey] !== 'undefined') { + return objectVisibility[controlKey]; + } + return this.visible; + }, + + /** + * Sets controls visibility + * @param {Boolean} visibility for the object + * @return {Void} + */ + setVisibility: function(visibility /* name, fabricObject */) { + this.visible = visibility; + }, + + + positionHandler: function(dim, finalMatrix /*, fabricObject, currentControl */) { + var point = fabric.util.transformPoint({ + x: this.x * dim.x + this.offsetX, + y: this.y * dim.y + this.offsetY }, finalMatrix); + return point; + }, + + /** + * Returns the coords for this control based on object values. + * @param {Number} objectAngle angle from the fabric object holding the control + * @param {Number} objectCornerSize cornerSize from the fabric object holding the control (or touchCornerSize if + * isTouch is true) + * @param {Number} centerX x coordinate where the control center should be + * @param {Number} centerY y coordinate where the control center should be + * @param {boolean} isTouch true if touch corner, false if normal corner + */ + calcCornerCoords: function(objectAngle, objectCornerSize, centerX, centerY, isTouch) { + var cosHalfOffset, + sinHalfOffset, + cosHalfOffsetComp, + sinHalfOffsetComp, + xSize = (isTouch) ? this.touchSizeX : this.sizeX, + ySize = (isTouch) ? this.touchSizeY : this.sizeY; + if (xSize && ySize && xSize !== ySize) { + // handle rectangular corners + var controlTriangleAngle = Math.atan2(ySize, xSize); + var cornerHypotenuse = Math.sqrt(xSize * xSize + ySize * ySize) / 2; + var newTheta = controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + var newThetaComp = Math.PI / 2 - controlTriangleAngle - fabric.util.degreesToRadians(objectAngle); + cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta); + // use complementary angle for two corners + cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newThetaComp); + sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newThetaComp); + } + else { + // handle square corners + // use default object corner size unless size is defined + var cornerSize = (xSize && ySize) ? xSize : objectCornerSize; + /* 0.7071067812 stands for sqrt(2)/2 */ + cornerHypotenuse = cornerSize * 0.7071067812; + // complementary angles are equal since they're both 45 degrees + var newTheta = fabric.util.degreesToRadians(45 - objectAngle); + cosHalfOffset = cosHalfOffsetComp = cornerHypotenuse * fabric.util.cos(newTheta); + sinHalfOffset = sinHalfOffsetComp = cornerHypotenuse * fabric.util.sin(newTheta); + } + + return { + tl: { + x: centerX - sinHalfOffsetComp, + y: centerY - cosHalfOffsetComp, + }, + tr: { + x: centerX + cosHalfOffset, + y: centerY - sinHalfOffset, + }, + bl: { + x: centerX - cosHalfOffset, + y: centerY + sinHalfOffset, + }, + br: { + x: centerX + sinHalfOffsetComp, + y: centerY + cosHalfOffsetComp, + }, + }; + }, + + /** + * Render function for the control. + * When this function runs the context is unscaled. unrotate. Just retina scaled. + * all the functions will have to translate to the point left,top before starting Drawing + * if they want to draw a control where the position is detected. + * left and top are the result of the positionHandler function + * @param {RenderingContext2D} ctx the context where the control will be drawn + * @param {Number} left position of the canvas where we are about to render the control. + * @param {Number} top position of the canvas where we are about to render the control. + * @param {Object} styleOverride + * @param {fabric.Object} fabricObject the object where the control is about to be rendered + */ + render: function(ctx, left, top, styleOverride, fabricObject) { + styleOverride = styleOverride || {}; + switch (styleOverride.cornerStyle || fabricObject.cornerStyle) { + case 'circle': + fabric.controlsUtils.renderCircleControl.call(this, ctx, left, top, styleOverride, fabricObject); + break; + default: + fabric.controlsUtils.renderSquareControl.call(this, ctx, left, top, styleOverride, fabricObject); + } + }, + }; + +})(typeof exports !== 'undefined' ? exports : this); + + (function() { /* _FROM_SVG_START_ */ - function getColorStop(el) { + function getColorStop(el, multiplier) { var style = el.getAttribute('style'), offset = el.getAttribute('offset') || 0, - color, colorAlpha, opacity; + color, colorAlpha, opacity, i; // convert percents to absolute values offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); @@ -5354,7 +8012,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { keyValuePairs.pop(); } - for (var i = keyValuePairs.length; i--; ) { + for (i = keyValuePairs.length; i--; ) { var split = keyValuePairs[i].split(/\s*:\s*/), key = split[0].trim(), @@ -5379,7 +8037,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { color = new fabric.Color(color); colorAlpha = color.getAlpha(); opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); - opacity *= colorAlpha; + opacity *= colorAlpha * multiplier; return { offset: offset, @@ -5433,18 +8091,68 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ 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 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. + * allowed values pixels or percentage. + * @type String + * @default 'pixels' + */ + gradientUnits: 'pixels', + + /** + * Gradient type linear or radial + * @type String + * @default 'pixels' + */ + type: 'linear', + /** * Constructor - * @param {Object} [options] Options object with type, coords, gradientUnits and colorStops + * @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 {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 = { }; + var coords, _this = this; - this.id = fabric.Object.__uid++; - this.type = options.type || 'linear'; + // 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, @@ -5457,13 +8165,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { coords.r1 = options.coords.r1 || 0; coords.r2 = options.coords.r2 || 0; } + this.coords = coords; this.colorStops = options.colorStops.slice(); - if (options.gradientTransform) { - this.gradientTransform = options.gradientTransform; - } - this.offsetX = options.offsetX || this.offsetX; - this.offsetY = options.offsetY || this.offsetY; }, /** @@ -5495,6 +8199,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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); @@ -5508,31 +8213,41 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @param {Object} object Object to create a gradient for * @return {String} SVG representation of an gradient (linear/radial) */ - toSVG: function(object) { - var coords = clone(this.coords, true), + 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; + 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 (!(object.group && object.group.type === 'path-group')) { - for (var prop in coords) { - if (prop === 'x1' || prop === 'x2') { - coords[prop] += this.offsetX - object.width / 2; - } - else if (prop === 'y1' || prop === 'y2') { - coords[prop] += this.offsetY - object.height / 2; - } - } + if (gradientUnits === 'objectBoundingBox') { + offsetX /= object.width; + offsetY /= object.height; + } + else { + offsetX += object.width / 2; + offsetY += object.height / 2; + } + if (object.type === 'path' && this.gradientUnits !== 'percentage') { + offsetX -= object.pathOffset.x; + offsetY -= object.pathOffset.y; } + + transform[4] -= offsetX; + transform[5] -= offsetY; + commonAttributes = 'id="SVGID_' + this.id + - '" gradientUnits="userSpaceOnUse"'; - if (this.gradientTransform) { - commonAttributes += ' gradientTransform="matrix(' + this.gradientTransform.join(' ') + ')" '; - } + '" gradientUnits="' + gradientUnits + '"'; + commonAttributes += ' gradientTransform="' + (withViewport ? + options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; + if (this.type === 'linear') { markup = [ '\n' ); } @@ -5598,27 +8313,15 @@ fabric.ElementsParser.prototype.checkIfDone = function() { /** * Returns an instance of CanvasGradient * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Object} object * @return {CanvasGradient} */ - toLive: function(ctx, object) { - var gradient, prop, coords = fabric.util.object.clone(this.coords); + toLive: function(ctx) { + var gradient, coords = fabric.util.object.clone(this.coords), i, len; if (!this.type) { return; } - if (object.group && object.group.type === 'path-group') { - for (prop in coords) { - if (prop === 'x1' || prop === 'x2') { - coords[prop] += -this.offsetX + object.width / 2; - } - else if (prop === 'y1' || prop === 'y2') { - coords[prop] += -this.offsetY + object.height / 2; - } - } - } - if (this.type === 'linear') { gradient = ctx.createLinearGradient( coords.x1, coords.y1, coords.x2, coords.y2); @@ -5628,7 +8331,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); } - for (var i = 0, len = this.colorStops.length; i < len; i++) { + 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; @@ -5652,12 +8355,18 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @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 gradients + * 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) { - + fromElement: function(el, instance, opacityAttr, svgOptions) { /** * @example: * @@ -5691,106 +8400,88 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * */ + 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') || 'objectBoundingBox', - gradientTransform = el.getAttribute('gradientTransform'), + gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? + 'pixels' : 'percentage', + gradientTransform = el.getAttribute('gradientTransform') || '', colorStops = [], - coords, ellipseMatrix; - + coords, i, offsetX = 0, offsetY = 0, + transformMatrix; if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { type = 'linear'; + coords = getLinearCoords(el); } else { type = 'radial'; - } - - if (type === 'linear') { - coords = getLinearCoords(el); - } - else if (type === 'radial') { coords = getRadialCoords(el); } - for (var i = colorStopEls.length; i--; ) { - colorStops.push(getColorStop(colorStopEls[i])); + for (i = colorStopEls.length; i--; ) { + colorStops.push(getColorStop(colorStopEls[i], multiplier)); } - ellipseMatrix = _convertPercentUnitsToValues(instance, coords, gradientUnits); + 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, - offsetX: -instance.left, - offsetY: -instance.top + gradientUnits: gradientUnits, + gradientTransform: transformMatrix, + offsetX: offsetX, + offsetY: offsetY, }); - if (gradientTransform || ellipseMatrix !== '') { - gradient.gradientTransform = fabric.parseTransformAttribute((gradientTransform || '') + ellipseMatrix); - } return gradient; - }, - /* _FROM_SVG_END_ */ - - /** - * Returns {@link fabric.Gradient} instance from its object representation - * @static - * @memberOf fabric.Gradient - * @param {Object} obj - * @param {Object} [options] Options object - */ - forObject: function(obj, options) { - options || (options = { }); - _convertPercentUnitsToValues(obj, options.coords, 'userSpaceOnUse'); - return new fabric.Gradient(options); } + /* _FROM_SVG_END_ */ }); /** * @private */ - function _convertPercentUnitsToValues(object, options, gradientUnits) { - var propValue, addFactor = 0, multFactor = 1, ellipseMatrix = ''; - for (var prop in options) { - if (options[prop] === 'Infinity') { - options[prop] = 1; + 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 (options[prop] === '-Infinity') { - options[prop] = 0; - } - propValue = parseFloat(options[prop], 10); - if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { - multFactor = 0.01; + else if (propValue === '-Infinity') { + finalValue = 0; } else { - multFactor = 1; + 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; + } + } + } } - if (prop === 'x1' || prop === 'x2' || prop === 'r2') { - multFactor *= gradientUnits === 'objectBoundingBox' ? object.width : 1; - addFactor = gradientUnits === 'objectBoundingBox' ? object.left || 0 : 0; - } - else if (prop === 'y1' || prop === 'y2') { - multFactor *= gradientUnits === 'objectBoundingBox' ? object.height : 1; - addFactor = gradientUnits === 'objectBoundingBox' ? object.top || 0 : 0; - } - options[prop] = propValue * multFactor + addFactor; - } - if (object.type === 'ellipse' && - options.r2 !== null && - gradientUnits === 'objectBoundingBox' && - object.rx !== object.ry) { - - var scaleFactor = object.ry / object.rx; - ellipseMatrix = ' scale(1, ' + scaleFactor + ')'; - if (options.y1) { - options.y1 /= scaleFactor; - } - if (options.y2) { - options.y2 /= scaleFactor; - } - } - return ellipseMatrix; + options[prop] = finalValue; + }); } })(); @@ -5833,6 +8524,21 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ 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 @@ -5848,19 +8554,14 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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) { + fabric.util.loadImage(options.source, function(img, isError) { _this.source = img; - callback && callback(_this); - }); + callback && callback(_this, isError); + }, null, this.crossOrigin); } }, @@ -5873,12 +8574,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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') { + if (typeof this.source.src === 'string') { source = this.source.src; } // element @@ -5890,8 +8587,10 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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); @@ -5913,9 +8612,16 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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; @@ -5950,8 +8656,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @return {CanvasPattern} */ toLive: function(ctx) { - var source = typeof this.source === 'function' ? this.source() : this.source; - + var source = this.source; // if the image failed to load, return, and allow rest to continue loading if (!source) { return ''; @@ -6033,9 +8738,18 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ 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, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)") + * @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) { @@ -6063,9 +8777,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { return { color: color.trim(), - offsetX: parseInt(offsetsAndBlur[1], 10) || 0, - offsetY: parseInt(offsetsAndBlur[2], 10) || 0, - blur: parseInt(offsetsAndBlur[3], 10) || 0 + offsetX: parseFloat(offsetsAndBlur[1], 10) || 0, + offsetY: parseFloat(offsetsAndBlur[2], 10) || 0, + blur: parseFloat(offsetsAndBlur[3], 10) || 0 }; }, @@ -6089,7 +8803,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { offset = fabric.util.rotateVector( { x: this.offsetX, y: this.offsetY }, fabric.util.degreesToRadians(-object.angle)), - BLUR_BOX = 20; + BLUR_BOX = 20, color = new fabric.Color(this.color); if (object.width && object.height) { //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion @@ -6103,6 +8817,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { if (object.flipY) { offset.y *= -1; } + return ( '\n' + @@ -6110,7 +8825,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { toFixed(this.blur ? this.blur / 2 : 0, NUM_FRACTION_DIGITS) + '">\n' + '\t\n' + - '\t\n' + + '\t\n' + '\t\n' + '\t\n' + '\t\t\n' + @@ -6131,12 +8846,13 @@ fabric.ElementsParser.prototype.checkIfDone = function() { blur: this.blur, offsetX: this.offsetX, offsetY: this.offsetY, - affectStroke: this.affectStroke + affectStroke: this.affectStroke, + nonScaling: this.nonScaling }; } var obj = { }, proto = fabric.Shadow.prototype; - ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke'].forEach(function(prop) { + ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke', 'nonScaling'].forEach(function(prop) { if (this[prop] !== proto[prop]) { obj[prop] = this[prop]; } @@ -6153,7 +8869,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @memberOf fabric.Shadow */ // eslint-disable-next-line max-len - fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/; + fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/; })(typeof exports !== 'undefined' ? exports : this); @@ -6174,6 +8890,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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'); @@ -6200,7 +8918,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ initialize: function(el, options) { options || (options = { }); - + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); this._initStatic(el, options); }, @@ -6214,10 +8933,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { /** * 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 */ @@ -6234,10 +8952,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { /** * 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 */ @@ -6245,6 +8962,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { /** * Indicates whether toObject/toDatalessObject should include default values + * if set to false, takes precedence over the object value. * @type Boolean * @default */ @@ -6258,22 +8976,17 @@ fabric.ElementsParser.prototype.checkIfDone = function() { stateful: false, /** - * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove} should also re-render canvas. - * Disabling this option could give a great performance boost when adding/removing a lot of objects to/from canvas at once - * (followed by a manual rendering after addition/deletion) + * 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. See clipping canvas area in {@link https://github.com/kangax/fabric.js/wiki/FAQ} - * @type Function - * @default - */ - clipTo: null, - /** * Indicates whether object controls (borders/controls) are rendered above overlay image * @type Boolean @@ -6296,8 +9009,12 @@ fabric.ElementsParser.prototype.checkIfDone = function() { imageSmoothingEnabled: true, /** - * The transformation (in the format of Canvas transform) which focuses the viewport + * The transformation (a Canvas 2D API transform matrix) which focuses the viewport * @type Array + * @example Default transform + * canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; + * @example Scale by 70% and translate toward bottom-right by 50, without skewing + * canvas.viewportTransform = [0.7, 0, 0, 0.7, 50, 50]; * @default */ viewportTransform: fabric.iMatrix.concat(), @@ -6318,15 +9035,10 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ overlayVpt: true, - /** - * Callback; invoked right before object is about to be scaled/rotated - */ - onBeforeScaleRotate: function () { - /* NOOP */ - }, - /** * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens + * @type Boolean + * @default */ enableRetinaScaling: true, @@ -6348,8 +9060,19 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * 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: false, + 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 @@ -6357,11 +9080,10 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @param {Object} [options] Options object */ _initStatic: function(el, options) { - var cb = fabric.StaticCanvas.prototype.renderAll.bind(this); + var cb = this.requestRenderAllBound; this._objects = []; this._createLowerCanvas(el); this._initOptions(options); - this._setImageSmoothing(); // only initialize retina scaling once if (!this.interactive) { this._initRetinaScaling(); @@ -6386,7 +9108,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ _isRetinaScaling: function() { - return (fabric.devicePixelRatio !== 1 && this.enableRetinaScaling); + return (fabric.devicePixelRatio > 1 && this.enableRetinaScaling); }, /** @@ -6394,7 +9116,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @return {Number} retinaScaling if applied, otherwise 1; */ getRetinaScaling: function() { - return this._isRetinaScaling() ? fabric.devicePixelRatio : 1; + return this._isRetinaScaling() ? Math.max(1, fabric.devicePixelRatio) : 1; }, /** @@ -6404,12 +9126,20 @@ fabric.ElementsParser.prototype.checkIfDone = 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); + var scaleRatio = fabric.devicePixelRatio; + this.__initRetinaScaling(scaleRatio, this.lowerCanvasEl, this.contextContainer); + if (this.upperCanvasEl) { + this.__initRetinaScaling(scaleRatio, this.upperCanvasEl, this.contextTop); + } }, + __initRetinaScaling: function(scaleRatio, canvas, context) { + canvas.setAttribute('width', this.width * scaleRatio); + canvas.setAttribute('height', this.height * scaleRatio); + context.scale(scaleRatio, scaleRatio); + }, + + /** * Calculates canvas element offset relative to the document * This method is also attached as "resize" event handler of window @@ -6445,7 +9175,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * 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) { + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img, isError) { * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); * }); @@ -6479,7 +9209,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @param {Object} [options] Optional options to set for the {@link fabric.Image|background image}. * @return {fabric.Canvas} thisArg * @chainable - * @see {@link http://jsfiddle.net/fabricjs/YH9yD/|jsFiddle demo} + * @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 @@ -6496,7 +9226,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * 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) { + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img, isError) { * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); * }); @@ -6519,14 +9249,15 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * crossOrigin: 'anonymous' * }); */ + // TODO: fix stretched examples setBackgroundImage: function (image, callback, options) { return this.__setBgOverlayImage('backgroundImage', image, callback, options); }, /** - * Sets {@link fabric.StaticCanvas#overlayColor|background color} for this canvas - * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set background color to - * @param {Function} callback Callback to invoke when background color is set + * 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} @@ -6573,37 +9304,30 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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 {Function} callback Callback to invoke when image is loaded and set as background or overlay. The first argument is the created image, the second argument is a flag indicating whether an error occurred or not. * @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) { - img && (this[property] = new fabric.Image(img, options)); - callback && callback(img); + fabric.util.loadImage(image, function(img, isError) { + if (img) { + var instance = new fabric.Image(img, options); + this[property] = instance; + instance.canvas = this; + } + callback && callback(img, isError); }, this, options && options.crossOrigin); } else { options && image.setOptions(options); this[property] = image; - callback && callback(image); + image && (image.canvas = this); + callback && callback(image, false); } return this; @@ -6626,14 +9350,14 @@ fabric.ElementsParser.prototype.checkIfDone = function() { /** * @private */ - _createCanvasElement: function(canvasEl) { - var element = fabric.util.createCanvasElement(canvasEl); - if (!element.style) { - element.style = { }; - } + _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; } @@ -6645,20 +9369,21 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @param {Object} [options] Options object */ _initOptions: function (options) { + var lowerCanvasEl = this.lowerCanvasEl; this._setOptions(options); - this.width = this.width || parseInt(this.lowerCanvasEl.width, 10) || 0; - this.height = this.height || parseInt(this.lowerCanvasEl.height, 10) || 0; + this.width = this.width || parseInt(lowerCanvasEl.width, 10) || 0; + this.height = this.height || parseInt(lowerCanvasEl.height, 10) || 0; if (!this.lowerCanvasEl.style) { return; } - this.lowerCanvasEl.width = this.width; - this.lowerCanvasEl.height = this.height; + lowerCanvasEl.width = this.width; + lowerCanvasEl.height = this.height; - this.lowerCanvasEl.style.width = this.width + 'px'; - this.lowerCanvasEl.style.height = this.height + 'px'; + lowerCanvasEl.style.width = this.width + 'px'; + lowerCanvasEl.style.height = this.height + 'px'; this.viewportTransform = this.viewportTransform.slice(); }, @@ -6669,10 +9394,16 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @param {HTMLElement} [canvasEl] */ _createLowerCanvas: function (canvasEl) { - this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(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'); - + this._originalCanvasStyle = this.lowerCanvasEl.style; if (this.interactive) { this._applyCanvasStyle(this.lowerCanvasEl); } @@ -6744,18 +9475,21 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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.contextTop); + } this._initRetinaScaling(); - this._setImageSmoothing(); this.calcOffset(); if (!options.cssOnly) { - this.renderAll(); + this.requestRenderAll(); } return this; @@ -6816,28 +9550,40 @@ fabric.ElementsParser.prototype.checkIfDone = function() { }, /** - * Sets viewport transform of this canvas instance - * @param {Array} vpt the transform in the form of context.transform + * Sets viewport transformation of this canvas instance + * @param {Array} vpt a Canvas 2D API transform matrix * @return {fabric.Canvas} instance * @chainable true */ setViewportTransform: function (vpt) { - var activeGroup = this._activeGroup, object, ingoreVpt = false, skipAbsolute = true; + var activeObject = this._activeObject, + backgroundObject = this.backgroundImage, + overlayObject = this.overlayImage, + object, i, len; this.viewportTransform = vpt; - for (var i = 0, len = this._objects.length; i < len; i++) { + for (i = 0, len = this._objects.length; i < len; i++) { object = this._objects[i]; - object.group || object.setCoords(ingoreVpt, skipAbsolute); + object.group || object.setCoords(true); } - if (activeGroup) { - activeGroup.setCoords(ingoreVpt, skipAbsolute); + if (activeObject) { + activeObject.setCoords(); + } + if (backgroundObject) { + backgroundObject.setCoords(true); + } + if (overlayObject) { + overlayObject.setCoords(true); } this.calcViewportBoundaries(); - this.renderAll(); + this.renderOnAddRemove && this.requestRenderAll(); return this; }, /** - * Sets zoom level of this canvas instance, zoom centered around point + * Sets zoom level of this canvas instance, the zoom centered around point + * meaning that following zoom to point with the same point will have the visual + * effect of the zoom originating from that point. The point won't move. + * It has nothing to do with canvas center or visual center of the viewport. * @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 @@ -6947,7 +9693,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @chainable */ clear: function () { - this._objects.length = 0; + this.remove.apply(this, this.getObjects()); this.backgroundImage = null; this.overlayImage = null; this.backgroundColor = ''; @@ -6959,7 +9705,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { } this.clearContext(this.contextContainer); this.fire('canvas:cleared'); - this.renderAll(); + this.renderOnAddRemove && this.requestRenderAll(); return this; }, @@ -6974,6 +9720,35 @@ fabric.ElementsParser.prototype.checkIfDone = function() { 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 @@ -6982,7 +9757,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @chainable */ calcViewportBoundaries: function() { - var points = { }, width = this.getWidth(), height = this.getHeight(), + 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); @@ -6992,6 +9767,13 @@ fabric.ElementsParser.prototype.checkIfDone = function() { return points; }, + cancelRequestedRender: function() { + if (this.isRendering) { + fabric.util.cancelAnimFrame(this.isRendering); + this.isRendering = 0; + } + }, + /** * Renders background, objects, overlay and controls. * @param {CanvasRenderingContext2D} ctx @@ -7000,30 +9782,52 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @chainable */ renderCanvas: function(ctx, objects) { + var v = this.viewportTransform, path = this.clipPath; + this.cancelRequestedRender(); this.calcViewportBoundaries(); this.clearContext(ctx); - this.fire('before:render'); - if (this.clipTo) { - fabric.util.clipContext(this, ctx); - } + fabric.util.setImageSmoothing(ctx, this.imageSmoothingEnabled); + this.fire('before:render', { ctx: ctx, }); this._renderBackground(ctx); ctx.save(); //apply viewport transform once for all rendering process - ctx.transform.apply(ctx, this.viewportTransform); + 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'); + 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(); }, /** @@ -7032,7 +9836,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @param {Array} objects to render */ _renderObjects: function(ctx, objects) { - for (var i = 0, length = objects.length; i < length; ++i) { + var i, len; + for (i = 0, len = objects.length; i < len; ++i) { objects[i] && objects[i].render(ctx); } }, @@ -7043,26 +9848,38 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @param {string} property 'background' or 'overlay' */ _renderBackgroundOrOverlay: function(ctx, property) { - var object = this[property + 'Color']; - if (object) { - ctx.fillStyle = object.toLive - ? object.toLive(ctx, this) - : object; - - ctx.fillRect( - object.offsetX || 0, - object.offsetY || 0, - this.width, - this.height); + 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], v[5]); + } + ctx.transform(1, 0, 0, 1, fill.offsetX || 0, 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(); } - object = this[property + 'Image']; if (object) { - if (this[property + 'Vpt']) { - ctx.save(); - ctx.transform.apply(ctx, this.viewportTransform); + ctx.save(); + if (needsVpt) { + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); } object.render(ctx); - this[property + 'Vpt'] && ctx.restore(); + ctx.restore(); } }, @@ -7086,64 +9903,66 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * 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 + * @deprecated migrate to `getCenterPoint` */ getCenter: function () { return { - top: this.getHeight() / 2, - left: this.getWidth() / 2 + top: this.height / 2, + left: this.width / 2 }; }, + /** + * Returns coordinates of a center of canvas. + * @return {fabric.Point} + */ + getCenterPoint: function () { + return new fabric.Point(this.width / 2, this.height / 2); + }, + /** * Centers object horizontally in the canvas - * You might need to call `setCoords` on an object after centering, to update controls area. * @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)); + return this._centerObject(object, new fabric.Point(this.getCenterPoint().x, object.getCenterPoint().y)); }, /** * Centers object vertically in the canvas - * You might need to call `setCoords` on an object after centering, to update controls area. * @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)); + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenterPoint().y)); }, /** * Centers object vertically and horizontally in the canvas - * You might need to call `setCoords` on an object after centering, to update controls area. * @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)); + var center = this.getCenterPoint(); + return this._centerObject(object, center); }, /** * Centers object vertically and horizontally in the viewport - * You might need to call `setCoords` on an object after centering, to update controls area. * @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 - * You might need to call `setCoords` on an object after centering, to update controls area. * @param {fabric.Object} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable @@ -7156,7 +9975,6 @@ fabric.ElementsParser.prototype.checkIfDone = function() { /** * Centers object Vertically in the viewport, object.top is unchanged - * You might need to call `setCoords` on an object after centering, to update controls area. * @param {fabric.Object} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable @@ -7173,9 +9991,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @chainable */ getVpCenter: function() { - var center = this.getCenter(), + var center = this.getCenterPoint(), iVpt = invertTransform(this.viewportTransform); - return transformPoint({ x: center.left, y: center.top }, iVpt); + return transformPoint(center, iVpt); }, /** @@ -7187,12 +10005,13 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ _centerObject: function(object, center) { object.setPositionByOrigin(center, 'center', 'center'); - this.renderAll(); + object.setCoords(); + this.renderOnAddRemove && this.requestRenderAll(); return this; }, /** - * Returs dataless JSON representation of canvas + * Returns dataless JSON representation of canvas * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {String} json string */ @@ -7223,10 +10042,13 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ _toObjectMethod: function (methodName, propertiesToInclude) { - var data = { - objects: this._toObjects(methodName, propertiesToInclude) + var clipPath = this.clipPath, data = { + version: fabric.version, + objects: this._toObjects(methodName, propertiesToInclude), }; - + if (clipPath && !clipPath.excludeFromExport) { + data.clipPath = this._toObject(this.clipPath, methodName, propertiesToInclude); + } extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude)); fabric.util.populateWithProperties(this, data, propertiesToInclude); @@ -7238,7 +10060,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ _toObjects: function(methodName, propertiesToInclude) { - return this.getObjects().filter(function(object) { + return this._objects.filter(function(object) { return !object.excludeFromExport; }).map(function(instance) { return this._toObject(instance, methodName, propertiesToInclude); @@ -7267,24 +10089,32 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ __serializeBgOverlay: function(methodName, propertiesToInclude) { - var data = { }; + var data = {}, bgImage = this.backgroundImage, overlayImage = this.overlayImage, + bgColor = this.backgroundColor, overlayColor = this.overlayColor; - if (this.backgroundColor) { - data.background = this.backgroundColor.toObject - ? this.backgroundColor.toObject(propertiesToInclude) - : this.backgroundColor; + if (bgColor && bgColor.toObject) { + if (!bgColor.excludeFromExport) { + data.background = bgColor.toObject(propertiesToInclude); + } + } + else if (bgColor) { + data.background = bgColor; } - if (this.overlayColor) { - data.overlay = this.overlayColor.toObject - ? this.overlayColor.toObject(propertiesToInclude) - : this.overlayColor; + if (overlayColor && overlayColor.toObject) { + if (!overlayColor.excludeFromExport) { + data.overlay = overlayColor.toObject(propertiesToInclude); + } } - if (this.backgroundImage) { - data.backgroundImage = this._toObject(this.backgroundImage, methodName, propertiesToInclude); + else if (overlayColor) { + data.overlay = overlayColor; } - if (this.overlayImage) { - data.overlayImage = this._toObject(this.overlayImage, methodName, propertiesToInclude); + + if (bgImage && !bgImage.excludeFromExport) { + data.backgroundImage = this._toObject(bgImage, methodName, propertiesToInclude); + } + if (overlayImage && !overlayImage.excludeFromExport) { + data.overlayImage = this._toObject(overlayImage, methodName, propertiesToInclude); } return data; @@ -7305,7 +10135,7 @@ fabric.ElementsParser.prototype.checkIfDone = 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.x] x-coordinate 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 @@ -7338,18 +10168,21 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ toSVG: function(options, reviver) { options || (options = { }); - + options.reviver = reviver; var markup = []; this._setSVGPreamble(markup, options); this._setSVGHeader(markup, options); - - this._setSVGBgOverlayColor(markup, 'backgroundColor'); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'background'); this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); - this._setSVGObjects(markup, reviver); - - this._setSVGBgOverlayColor(markup, 'overlayColor'); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'overlay'); this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); markup.push(''); @@ -7366,8 +10199,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { } markup.push( '\n', - '\n' + '\n' ); }, @@ -7400,31 +10233,51 @@ fabric.ElementsParser.prototype.checkIfDone = function() { markup.push( '\n', + 'xmlns="http://www.w3.org/2000/svg" ', + 'xmlns:xlink="http://www.w3.org/1999/xlink" ', + 'version="1.1" ', + 'width="', width, '" ', + 'height="', height, '" ', + viewBox, + 'xml:space="preserve">\n', 'Created with Fabric.js ', fabric.version, '\n', '\n', - this.createSVGFontFacesMarkup(), - this.createSVGRefElementsMarkup(), + 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 = ['backgroundColor', 'overlayColor'].map(function(prop) { - var fill = _this[prop]; + markup = ['background', 'overlay'].map(function(prop) { + var fill = _this[prop + 'Color']; if (fill && fill.toLive) { - return fill.toSVG(_this, false); + 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(''); @@ -7439,10 +10292,17 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ createSVGFontFacesMarkup: function() { var markup = '', fontList = { }, obj, fontFamily, - style, row, rowIndex, _char, charIndex, - fontPaths = fabric.fontPaths, objects = this.getObjects(); + style, row, rowIndex, _char, charIndex, i, len, + fontPaths = fabric.fontPaths, objects = []; - for (var i = 0, len = objects.length; i < len; i++) { + this._objects.forEach(function add(object) { + objects.push(object); + if (object._objects) { + object._objects.forEach(add); + } + }); + + 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]) { @@ -7491,8 +10351,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ _setSVGObjects: function(markup, reviver) { - var instance; - for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { + var instance, i, len, objects = this._objects; + for (i = 0, len = objects.length; i < len; i++) { instance = objects[i]; if (instance.excludeFromExport) { continue; @@ -7502,7 +10362,6 @@ fabric.ElementsParser.prototype.checkIfDone = function() { }, /** - * push single object svg representation in the markup * @private */ _setSVGObject: function(markup, instance, reviver) { @@ -7513,7 +10372,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ _setSVGBgOverlayImage: function(markup, property, reviver) { - if (this[property] && this[property].toSVG) { + if (this[property] && !this[property].excludeFromExport && this[property].toSVG) { markup.push(this[property].toSVG(reviver)); } }, @@ -7522,33 +10381,34 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ _setSVGBgOverlayColor: function(markup, property) { - var filler = this[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; + 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' ); } @@ -7566,10 +10426,10 @@ fabric.ElementsParser.prototype.checkIfDone = function() { if (!object) { return this; } - var activeGroup = this._activeGroup, + var activeSelection = this._activeObject, i, obj, objs; - if (object === activeGroup) { - objs = activeGroup._objects; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; for (i = objs.length; i--;) { obj = objs[i]; removeFromArray(this._objects, obj); @@ -7580,7 +10440,8 @@ fabric.ElementsParser.prototype.checkIfDone = function() { removeFromArray(this._objects, object); this._objects.unshift(object); } - return this.renderAll && this.renderAll(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; }, /** @@ -7594,10 +10455,10 @@ fabric.ElementsParser.prototype.checkIfDone = function() { if (!object) { return this; } - var activeGroup = this._activeGroup, + var activeSelection = this._activeObject, i, obj, objs; - if (object === activeGroup) { - objs = activeGroup._objects; + if (object === activeSelection && object.type === 'activeSelection') { + objs = activeSelection._objects; for (i = 0; i < objs.length; i++) { obj = objs[i]; removeFromArray(this._objects, obj); @@ -7608,11 +10469,16 @@ fabric.ElementsParser.prototype.checkIfDone = function() { removeFromArray(this._objects, object); this._objects.push(object); } - return this.renderAll && this.renderAll(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; }, /** * Moves an object or a selection down in stack of drawn objects + * An optional parameter, intersecting allows 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 @@ -7622,19 +10488,20 @@ fabric.ElementsParser.prototype.checkIfDone = function() { if (!object) { return this; } - var activeGroup = this._activeGroup, - i, obj, idx, newIdx, objs; + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; - if (object === activeGroup) { - objs = activeGroup._objects; + 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) { + if (idx > 0 + objsMoved) { newIdx = idx - 1; removeFromArray(this._objects, obj); this._objects.splice(newIdx, 0, obj); } + objsMoved++; } } else { @@ -7646,7 +10513,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { this._objects.splice(newIdx, 0, object); } } - this.renderAll && this.renderAll(); + this.renderOnAddRemove && this.requestRenderAll(); return this; }, @@ -7654,13 +10521,13 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ _findNewLowerIndex: function(object, idx, intersecting) { - var newIdx; + var newIdx, i; if (intersecting) { newIdx = idx; // traverse down the stack looking for the nearest intersecting object - for (var i = idx - 1; i >= 0; --i) { + for (i = idx - 1; i >= 0; --i) { var isIntersecting = object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i]) || @@ -7681,6 +10548,10 @@ fabric.ElementsParser.prototype.checkIfDone = function() { /** * Moves an object or a selection up in stack of drawn objects + * An optional parameter, intersecting allows 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 @@ -7690,19 +10561,20 @@ fabric.ElementsParser.prototype.checkIfDone = function() { if (!object) { return this; } - var activeGroup = this._activeGroup, - i, obj, idx, newIdx, objs; + var activeSelection = this._activeObject, + i, obj, idx, newIdx, objs, objsMoved = 0; - if (object === activeGroup) { - objs = activeGroup._objects; + 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) { + if (idx < this._objects.length - 1 - objsMoved) { newIdx = idx + 1; removeFromArray(this._objects, obj); this._objects.splice(newIdx, 0, obj); } + objsMoved++; } } else { @@ -7714,7 +10586,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { this._objects.splice(newIdx, 0, object); } } - this.renderAll && this.renderAll(); + this.renderOnAddRemove && this.requestRenderAll(); return this; }, @@ -7722,13 +10594,13 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * @private */ _findNewUpperIndex: function(object, idx, intersecting) { - var newIdx; + var newIdx, i, len; if (intersecting) { newIdx = idx; // traverse up the stack looking for the nearest intersecting object - for (var i = idx + 1; i < this._objects.length; ++i) { + for (i = idx + 1, len = this._objects.length; i < len; ++i) { var isIntersecting = object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i]) || @@ -7757,16 +10629,43 @@ fabric.ElementsParser.prototype.checkIfDone = function() { moveTo: function (object, index) { removeFromArray(this._objects, object); this._objects.splice(index, 0, object); - return this.renderAll && this.renderAll(); + return this.renderOnAddRemove && this.requestRenderAll(); }, /** - * Clears a canvas element and removes all event listeners + * Clears a canvas element and dispose objects * @return {fabric.Canvas} thisArg * @chainable */ dispose: function () { - this.clear(); + // 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; + // restore canvas style + this.lowerCanvasEl.classList.remove('lower-canvas'); + fabric.util.setStyle(this.lowerCanvasEl, this._originalCanvasStyle); + delete this._originalCanvasStyle; + // restore canvas size to original size in case retina scaling was applied + this.lowerCanvasEl.setAttribute('width', this.width); + this.lowerCanvasEl.setAttribute('height', this.height); + fabric.util.cleanUpJsdomNode(this.lowerCanvasEl); + this.lowerCanvasEl = undefined; return this; }, @@ -7776,7 +10675,7 @@ fabric.ElementsParser.prototype.checkIfDone = function() { */ toString: function () { return '#'; + '{ objects: ' + this._objects.length + ' }>'; } }); @@ -7798,12 +10697,12 @@ fabric.ElementsParser.prototype.checkIfDone = function() { * (either those of HTMLCanvasElement itself, or rendering context) * * @param {String} methodName Method to check support for; - * Could be one of "getImageData", "toDataURL", "toDataURLWithQuality" or "setLineDash" + * 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 = fabric.util.createCanvasElement(); + var el = createCanvasElement(); if (!el || !el.getContext) { return null; @@ -7816,23 +10715,9 @@ fabric.ElementsParser.prototype.checkIfDone = function() { switch (methodName) { - case 'getImageData': - return typeof ctx.getImageData !== 'undefined'; - case 'setLineDash': return typeof ctx.setLineDash !== 'undefined'; - case 'toDataURL': - return typeof el.toDataURL !== 'undefined'; - - case 'toDataURLWithQuality': - try { - el.toDataURL('image/jpeg', 0); - return true; - } - catch (e) { } - return false; - default: return null; } @@ -7840,22 +10725,35 @@ fabric.ElementsParser.prototype.checkIfDone = function() { }); /** - * Returns JSON representation of canvas + * Returns Object representation of canvas + * this alias is provided because if you call JSON.stringify on an instance, + * the toJSON object will be invoked if it exists. + * Having a toJSON method means you can do JSON.stringify(myCanvas) * @function * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {String} JSON string + * @return {Object} JSON compatible object * @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']); + * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']); * @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); + }; + } })(); @@ -7874,7 +10772,7 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype color: 'rgb(0, 0, 0)', /** - * Width of a brush + * Width of a brush, has to be a Number, no string literals * @type Number * @default */ @@ -7897,12 +10795,19 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype strokeLineCap: 'round', /** - * Corner style of a brush (one of "bevil", "round", "miter") + * 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 @@ -7911,30 +10816,37 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype 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; - }, + * When `true`, the free drawing is limited to the whiteboard size. Default to false. + * @type Boolean + * @default false + */ + + limitedToCanvasSize: false, + /** * Sets brush styles * @private + * @param {CanvasRenderingContext2D} ctx */ - _setBrushStyles: function() { - var ctx = this.canvas.contextTop; - + _setBrushStyles: function (ctx) { ctx.strokeStyle = this.color; ctx.lineWidth = this.width; ctx.lineCap = this.strokeLineCap; + ctx.miterLimit = this.strokeMiterLimit; ctx.lineJoin = this.strokeLineJoin; - if (this.strokeDashArray && fabric.StaticCanvas.supports('setLineDash')) { - ctx.setLineDash(this.strokeDashArray); - } + 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]); }, /** @@ -7946,13 +10858,23 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype return; } - var ctx = this.canvas.contextTop, - zoom = this.canvas.getZoom(); + var canvas = this.canvas, + shadow = this.shadow, + ctx = canvas.contextTop, + zoom = canvas.getZoom(); + if (canvas && canvas._isRetinaScaling()) { + zoom *= fabric.devicePixelRatio; + } - ctx.shadowColor = this.shadow.color; - ctx.shadowBlur = this.shadow.blur * zoom; - ctx.shadowOffsetX = this.shadow.offsetX * zoom; - ctx.shadowOffsetY = this.shadow.offsetY * zoom; + ctx.shadowColor = shadow.color; + ctx.shadowBlur = shadow.blur * zoom; + ctx.shadowOffsetX = shadow.offsetX * zoom; + ctx.shadowOffsetY = shadow.offsetY * zoom; + }, + + needsFullRender: function() { + var color = new fabric.Color(this.color); + return color.getAlpha() < 1 || !!this.shadow; }, /** @@ -7964,12 +10886,20 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype ctx.shadowColor = ''; ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * Check is pointer is outside canvas boundaries + * @param {Object} pointer + * @private + */ + _isOutSideCanvas: function(pointer) { + return pointer.x < 0 || pointer.x > this.canvas.getWidth() || pointer.y < 0 || pointer.y > this.canvas.getHeight(); } }); (function() { - /** * PencilBrush class * @class fabric.PencilBrush @@ -7977,6 +10907,29 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ 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, + + /** + * Draws a straight line between last recorded point to current pointer + * Used for `shift` functionality + * + * @type boolean + * @default false + */ + drawStraightLine: false, + + /** + * The event modifier key that makes the brush draw a straight line. + * If `null` or 'none' or any other string that is not a modifier key the feature is disabled. + * @type {'altKey' | 'shiftKey' | 'ctrlKey' | 'none' | undefined | null} + */ + straightLineKey: 'shiftKey', + /** * Constructor * @param {fabric.Canvas} canvas @@ -7987,11 +10940,29 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype this._points = []; }, + needsFullRender: function () { + return this.callSuper('needsFullRender') || this._hasStraightLine; + }, + /** - * Inovoked on mouse down + * Invoked inside on mouse down and mouse move * @param {Object} pointer */ - onMouseDown: function(pointer) { + _drawSegment: function (ctx, p1, p2) { + var midPoint = p1.midPointFrom(p2); + ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); + return midPoint; + }, + + /** + * Invoked on mouse down + * @param {Object} pointer + */ + onMouseDown: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + this.drawStraightLine = options.e[this.straightLineKey]; this._prepareForDrawing(pointer); // capture coordinates immediately // this allows to draw dots (when movement never occurs) @@ -8000,22 +10971,50 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype }, /** - * Inovoked on mouse move + * Invoked on mouse move * @param {Object} pointer */ - onMouseMove: function(pointer) { - this._captureDrawingPath(pointer); - // redraw curve - // clear top canvas - this.canvas.clearContext(this.canvas.contextTop); - this._render(); + onMouseMove: function(pointer, options) { + if (!this.canvas._isMainEvent(options.e)) { + return; + } + this.drawStraightLine = options.e[this.straightLineKey]; + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + 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() { + onMouseUp: function(options) { + if (!this.canvas._isMainEvent(options.e)) { + return true; + } + this.drawStraightLine = false; + this.oldEnd = undefined; this._finalizeAndAddPath(); + return false; }, /** @@ -8028,7 +11027,6 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype this._reset(); this._addPoint(p); - this.canvas.contextTop.moveTo(p.x, p.y); }, @@ -8037,7 +11035,15 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * @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; + } + if (this.drawStraightLine && this._points.length > 1) { + this._hasStraightLine = true; + this._points.pop(); + } this._points.push(point); + return true; }, /** @@ -8045,10 +11051,10 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype * @private */ _reset: function() { - this._points.length = 0; - - this._setBrushStyles(); + this._points = []; + this._setBrushStyles(this.canvas.contextTop); this._setShadow(); + this._hasStraightLine = false; }, /** @@ -8057,39 +11063,38 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ _captureDrawingPath: function(pointer) { var pointerPoint = new fabric.Point(pointer.x, pointer.y); - this._addPoint(pointerPoint); + return this._addPoint(pointerPoint); }, /** * Draw a smooth path on the topCanvas using quadraticCurveTo * @private + * @param {CanvasRenderingContext2D} [ctx] */ - _render: function() { - var ctx = this.canvas.contextTop, - v = this.canvas.viewportTransform, + _render: function(ctx) { + var i, len, p1 = this._points[0], p2 = this._points[1]; - - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + ctx = ctx || this.canvas.contextTop; + 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) { - p1.x -= 0.5; - p2.x += 0.5; + 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 (var i = 1, len = this._points.length; i < len; i++) { + 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. - var midPoint = p1.midPointFrom(p2); - ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); - + this._drawSegment(ctx, p1, p2); p1 = this._points[i]; p2 = this._points[i + 1]; } @@ -8104,32 +11109,26 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype /** * Converts points to SVG path * @param {Array} points Array of points - * @return {String} SVG path + * @return {(string|number)[][]} SVG path commands */ - convertPointsToSVGPath: function(points) { - var path = [], - p1 = new fabric.Point(points[0].x, points[0].y), - p2 = new fabric.Point(points[1].x, points[1].y); + convertPointsToSVGPath: function (points) { + var correction = this.width / 1000; + return fabric.util.getSmoothPathFromPoints(points, correction); + }, - path.push('M ', points[0].x, ' ', points[0].y, ' '); - for (var i = 1, len = points.length; i < len; i++) { - 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 = new fabric.Point(points[i].x, points[i].y); - if ((i + 1) < points.length) { - p2 = new fabric.Point(points[i + 1].x, points[i + 1].y); - } - } - path.push('L ', p1.x, ' ', p1.y, ' '); - return path; + /** + * @private + * @param {(string|number)[][]} pathData SVG path commands + * @returns {boolean} + */ + _isEmptySVGPath: function (pathData) { + var pathString = fabric.util.joinPath(pathData); + return pathString === 'M 0 0 Q 0 0 0 0 L 0 0'; }, /** * Creates fabric.Path object to add on canvas - * @param {String} pathData Path data + * @param {(string|number)[][]} pathData Path data * @return {fabric.Path} Path to add on canvas */ createPath: function(pathData) { @@ -8138,20 +11137,43 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype stroke: this.color, strokeWidth: this.width, strokeLineCap: this.strokeLineCap, + strokeMiterLimit: this.strokeMiterLimit, strokeLineJoin: this.strokeLineJoin, strokeDashArray: this.strokeDashArray, - originX: 'center', - originY: 'center' }); - if (this.shadow) { this.shadow.affectStroke = true; - path.setShadow(this.shadow); + path.shadow = new fabric.Shadow(this.shadow); } return path; }, + /** + * Decimate points 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 - 1; 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); + } + } + /** + * Add the last point from the original line to the end of the array. + * This ensures decimate doesn't delete the last point on the line, and ensures the line is > 1 point. + */ + newPoints.push(points[l]); + return newPoints; + }, + /** * On mouseup after drawing the path on contextTop canvas * we use the points captured to create an new fabric path object @@ -8160,25 +11182,27 @@ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype _finalizeAndAddPath: function() { var ctx = this.canvas.contextTop; ctx.closePath(); - - var pathData = this.convertPointsToSVGPath(this._points).join(''); - if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { + if (this.decimate) { + this._points = this.decimatePoints(this._points, this.decimate); + } + var pathData = this.convertPointsToSVGPath(this._points); + if (this._isEmptySVGPath(pathData)) { // 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.renderAll(); + this.canvas.requestRenderAll(); return; } var path = this.createPath(pathData); - - this.canvas.add(path); - path.setCoords(); - this.canvas.clearContext(this.canvas.contextTop); + this.canvas.fire('before:path:created', { path: path }); + this.canvas.add(path); + this.canvas.requestRenderAll(); + path.setCoords(); this._resetShadow(); - this.canvas.renderAll(); + // fire event 'path' created this.canvas.fire('path:created', { path: path }); @@ -8216,18 +11240,18 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric */ drawDot: function(pointer) { var point = this.addPoint(pointer), - ctx = this.canvas.contextTop, - v = this.canvas.viewportTransform; - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + 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(); - - ctx.restore(); }, /** @@ -8240,24 +11264,48 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric 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) { - this.drawDot(pointer); + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } + 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; + var originalRenderOnAddRemove = this.canvas.renderOnAddRemove, i, len; this.canvas.renderOnAddRemove = false; var circles = []; - for (var i = 0, len = this.points.length; i < len; i++) { + for (i = 0, len = this.points.length; i < len; i++) { var point = this.points[i], circle = new fabric.Circle({ radius: point.radius, @@ -8268,20 +11316,21 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric fill: point.fill }); - this.shadow && circle.setShadow(this.shadow); + this.shadow && (circle.shadow = new fabric.Shadow(this.shadow)); circles.push(circle); } - var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); + var group = new fabric.Group(circles); group.canvas = this.canvas; + this.canvas.fire('before:path:created', { path: group }); 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.renderAll(); + this.canvas.requestRenderAll(); }, /** @@ -8292,11 +11341,11 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric var pointerPoint = new fabric.Point(pointer.x, pointer.y), circleRadius = fabric.util.getRandomInt( - Math.max(0, this.width - 20), this.width + 20) / 2, + Math.max(0, this.width - 20), this.width + 20) / 2, circleColor = new fabric.Color(this.color) - .setAlpha(fabric.util.getRandomInt(0, 100) / 100) - .toRgba(); + .setAlpha(fabric.util.getRandomInt(0, 100) / 100) + .toRgba(); pointerPoint.radius = circleRadius; pointerPoint.fill = circleColor; @@ -8376,7 +11425,7 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric this._setShadow(); this.addSprayChunk(pointer); - this.render(); + this.render(this.sprayChunkPoints); }, /** @@ -8384,8 +11433,11 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric * @param {Object} pointer */ onMouseMove: function(pointer) { + if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) { + return; + } this.addSprayChunk(pointer); - this.render(); + this.render(this.sprayChunkPoints); }, /** @@ -8411,8 +11463,6 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric originY: 'center', fill: this.color }); - - this.shadow && rect.setShadow(this.shadow); rects.push(rect); } } @@ -8421,16 +11471,16 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric rects = this._getOptimizedRects(rects); } - var group = new fabric.Group(rects, { originX: 'center', originY: 'center' }); - group.canvas = this.canvas; - + var group = new fabric.Group(rects); + this.shadow && group.set('shadow', new fabric.Shadow(this.shadow)); + this.canvas.fire('before:path:created', { path: group }); 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.renderAll(); + this.canvas.requestRenderAll(); }, /** @@ -8440,9 +11490,9 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric _getOptimizedRects: function(rects) { // avoid creating duplicate rects at the same coordinates - var uniqueRects = { }, key; + var uniqueRects = { }, key, i, len; - for (var i = 0, len = rects.length; i < len; i++) { + for (i = 0, len = rects.length; i < len; i++) { key = rects[i].left + '' + rects[i].top; if (!uniqueRects[key]) { uniqueRects[key] = rects[i]; @@ -8457,18 +11507,16 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric }, /** - * Renders brush + * Render new chunk of spray brush */ - render: function() { - var ctx = this.canvas.contextTop; + render: function(sprayChunk) { + var ctx = this.canvas.contextTop, i, len; ctx.fillStyle = this.color; - var v = this.canvas.viewportTransform; - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this._saveAndTransform(ctx); - for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { - var point = this.sprayChunkPoints[i]; + for (i = 0, len = sprayChunk.length; i < len; i++) { + var point = sprayChunk[i]; if (typeof point.opacity !== 'undefined') { ctx.globalAlpha = point.opacity; } @@ -8477,15 +11525,30 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric 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; + var x, y, width, radius = this.width / 2, i; - for (var i = 0; i < this.density; 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); @@ -8526,7 +11589,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var dotWidth = 20, dotDistance = 5, - patternCanvas = fabric.document.createElement('canvas'), + patternCanvas = fabric.util.createCanvasElement(), patternCtx = patternCanvas.getContext('2d'); patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; @@ -8546,17 +11609,19 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * Creates "pattern" instance property + * @param {CanvasRenderingContext2D} ctx */ - getPattern: function() { - return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat'); + getPattern: function(ctx) { + return ctx.createPattern(this.source || this.getPatternSrc(), 'repeat'); }, /** * Sets brush styles + * @param {CanvasRenderingContext2D} ctx */ - _setBrushStyles: function() { - this.callSuper('_setBrushStyles'); - this.canvas.contextTop.strokeStyle = this.getPattern(); + _setBrushStyles: function(ctx) { + this.callSuper('_setBrushStyles', ctx); + ctx.strokeStyle = this.getPattern(ctx); }, /** @@ -8580,12 +11645,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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; + isTouchEvent = fabric.util.isTouchEvent; /** * Canvas class @@ -8594,23 +11654,36 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} * @see {@link fabric.Canvas#initialize} for constructor definition * - * @fires object:added - * @fires object:modified - * @fires object:rotating - * @fires object:scaling - * @fires object:moving - * @fires object:selected + * @fires object:modified at the end of a transform or any change when statefull is true + * @fires object:rotating while an object is being rotated from the control + * @fires object:scaling while an object is being scaled by controls + * @fires object:moving while an object is being dragged + * @fires object:skewing while an object is being skewed from the controls * + * @fires before:transform before a transform is is started * @fires before:selection:cleared * @fires selection:cleared + * @fires selection:updated * @fires selection:created * - * @fires path:created + * @fires path:created after a drawing operation ends and the path is added * @fires mouse:down * @fires mouse:move * @fires mouse:up + * @fires mouse:down:before on mouse down, before the inner fabric logic runs + * @fires mouse:move:before on mouse move, before the inner fabric logic runs + * @fires mouse:up:before on mouse up, before the inner fabric logic runs * @fires mouse:over * @fires mouse:out + * @fires mouse:dblclick whenever a native dbl click event fires on the canvas. + * + * @fires dragover + * @fires dragenter + * @fires dragleave + * @fires drop:before before drop event. same native event. This is added to handle edge cases + * @fires drop + * @fires after:render at the end of the render process, receives the context in the callback + * @fires before:render at start the render process, receives the context in the callback * */ fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { @@ -8623,7 +11696,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ 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(); @@ -8631,16 +11705,21 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * When true, objects can be transformed by one side (unproportionally) + * when dragged on the corners that normally would not do that. * @type Boolean * @default + * @since fabric 4.0 // changed name and default value */ - uniScaleTransform: false, + uniformScaling: true, /** - * Indicates which key enable unproportional scaling + * Indicates which key switches uniform scaling. * values: 'altKey', 'shiftKey', 'ctrlKey'. * If `null` or 'none' or any other string that is not a modifier key - * feature is disabled feature disabled. + * feature is disabled. + * totally wrong named. this sounds like `uniform scaling` + * if Canvas.uniformScaling is true, pressing this will set it to false + * and viceversa. * @since 1.6.2 * @type String * @default @@ -8666,7 +11745,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab centeredRotation: false, /** - * Indicates which key enable centered Transfrom + * 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. @@ -8702,12 +11781,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab selection: true, /** - * Indicates which key enable multiple click selection + * 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 'none' or any other string that is not a modifier key - * feature is disabled feature disabled. + * If `null` or empty or containing any other string that is not a modifier key + * feature is disabled. * @since 1.6.2 - * @type String + * @type String|Array * @default */ selectionKey: 'shiftKey', @@ -8716,8 +11796,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * 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 feature disabled. + * feature is disabled. * @since 1.6.5 * @type null|String * @default @@ -8752,6 +11834,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ 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 @@ -8781,11 +11870,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab freeDrawingCursor: 'crosshair', /** - * Cursor value used for rotation point + * Cursor value used for disabled elements ( corners with disabled action ) * @type String + * @since 2.0.0 * @default */ - rotationCursor: 'crosshair', + notAllowedCursor: 'not-allowed', /** * Default element class that's given to wrapper (div) element of canvas @@ -8809,7 +11899,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab targetFindTolerance: 0, /** - * When true, target detection is skipped when hovering over canvas. This can be used to improve performance. + * When true, target detection is skipped. Target detection will return always undefined. + * click selection won't work anymore, events will fire with no targets. + * if something is selected before setting it to true, it will be deselected at the first click. + * area selection will still work. check the `selection` property too. + * if you deactivate both, you should look into staticCanvas. * @type Boolean * @default */ @@ -8874,6 +11968,33 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ fireMiddleClick: false, + /** + * Keep track of the subTargets for Mouse Events + * @type fabric.Object[] + */ + targets: [], + + /** + * When the option is enabled, PointerEvent is used instead of MouseEvent. + * @type Boolean + * @default + */ + enablePointerEvents: false, + + /** + * Keep track of the hovered target + * @type fabric.Object + * @private + */ + _hoveredTarget: null, + + /** + * hold the list of nested targets hovered + * @type fabric.Object[] + * @private + */ + _hoveredTargets: [], + /** * @private */ @@ -8897,25 +12018,25 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @return {Array} objects to render immediately and pushes the other in the activeGroup. */ _chooseObjectsToRender: function() { - var activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(), - object, objsToRender = [], activeGroupObjects = []; + var activeObjects = this.getActiveObjects(), + object, objsToRender, activeGroupObjects; - if ((activeGroup || activeObject) && !this.preserveObjectStacking) { + if (activeObjects.length > 0 && !this.preserveObjectStacking) { + objsToRender = []; + activeGroupObjects = []; for (var i = 0, length = this._objects.length; i < length; i++) { object = this._objects[i]; - if ((!activeGroup || !activeGroup.contains(object)) && object !== activeObject) { + if (activeObjects.indexOf(object) === -1 ) { objsToRender.push(object); } else { activeGroupObjects.push(object); } } - if (activeGroup) { - activeGroup._set('_objects', activeGroupObjects); - objsToRender.push(activeGroup); + if (activeObjects.length > 1) { + this._activeObject._objects = activeGroupObjects; } - activeObject && objsToRender.push(activeObject); + objsToRender.push.apply(objsToRender, activeGroupObjects); } else { objsToRender = this._objects; @@ -8933,11 +12054,29 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.clearContext(this.contextTop); this.contextTopDirty = false; } + if (this.hasLostContext) { + this.renderTopLayer(this.contextTop); + this.hasLostContext = false; + } 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. @@ -8947,88 +12086,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab renderTop: function () { var ctx = this.contextTop; this.clearContext(ctx); - - // we render the top context - last object - if (this.selection && this._groupSelector) { - this._drawSelection(ctx); - } - + this.renderTopLayer(ctx); this.fire('after:render'); - this.contextTopDirty = true; 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.action === 'rotate') { - this._setOriginToCenter(t.target); - } - else { - 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.getActiveGroup()) { - 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 */ @@ -9047,50 +12109,75 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @return {Boolean} */ isTargetTransparent: function (target, x, y) { - var hasBorders = target.hasBorders, - transparentCorners = target.transparentCorners, - ctx = this.contextCache, - originalColor = target.selectionBackgroundColor; + // 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.hasBorders = target.transparentCorners = false; target.selectionBackgroundColor = ''; + this.clearContext(ctx); + ctx.save(); - ctx.transform.apply(ctx, this.viewportTransform); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); target.render(ctx); ctx.restore(); - target.active && target._renderControls(ctx); - - target.hasBorders = hasBorders; - target.transparentCorners = transparentCorners; target.selectionBackgroundColor = originalColor; var isTransparent = fabric.util.isTransparent( ctx, x, y, this.targetFindTolerance); - this.clearContext(ctx); - return isTransparent; }, + /** + * takes an event and determines if selection key has been pressed + * @private + * @param {Event} e Event object + */ + _isSelectionKeyPressed: function(e) { + var selectionKeyPressed = false; + + if (Array.isArray(this.selectionKey)) { + 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 activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(); + var activeObjects = this.getActiveObjects(), + activeObject = this._activeObject; return ( !target || (target && - activeGroup && - !activeGroup.contains(target) && - activeGroup !== target && - !e[this.selectionKey]) + activeObject && + activeObjects.length > 1 && + activeObjects.indexOf(target) === -1 && + activeObject !== target && + !this._isSelectionKeyPressed(e)) || (target && !target.evented) || @@ -9102,28 +12189,34 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** + * centeredScaling from object can't override centeredScaling from canvas. + * this should be fixed, since object setting should take precedence over canvas. + * also this should be something that will be migrated in the control properties. + * as ability to define the origin of the transformation that the control provide. * @private * @param {fabric.Object} target + * @param {String} action + * @param {Boolean} altKey */ - _shouldCenterTransform: function (target) { + _shouldCenterTransform: function (target, action, altKey) { if (!target) { return; } - var t = this._currentTransform, - centerTransform; + var centerTransform; - if (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY') { + if (action === 'scale' || action === 'scaleX' || action === 'scaleY' || action === 'resizing') { centerTransform = this.centeredScaling || target.centeredScaling; } - else if (t.action === 'rotate') { + else if (action === 'rotate') { centerTransform = this.centeredRotation || target.centeredRotation; } - return centerTransform ? !t.altKey : t.altKey; + return centerTransform ? !altKey : altKey; }, /** + * should disappear before release 4.0 * @private */ _getOriginFromCorner: function(target, corner) { @@ -9145,30 +12238,22 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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(target, corner, e) { - if (!corner) { + _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'; - } + var control = target.controls[corner]; + return control.getActionName(e, control, target); }, /** @@ -9176,412 +12261,55 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object * @param {fabric.Object} target */ - _setupCurrentTransform: function (e, target) { + _setupCurrentTransform: function (e, target, alreadySelected) { if (!target) { return; } - var pointer = this.getPointer(e), - corner = target._findTargetCorner(this.getPointer(e, true)), - action = this._getActionFromCorner(target, corner, e), - origin = this._getOriginFromCorner(target, corner); + var pointer = this.getPointer(e), corner = target.__corner, + control = target.controls[corner], + actionHandler = (alreadySelected && corner) ? + control.getActionHandler(e, target, control) : fabric.controlsUtils.dragHandler, + action = this._getActionFromCorner(alreadySelected, corner, e, target), + origin = this._getOriginFromCorner(target, corner), + altKey = e[this.centeredKey], + transform = { + target: target, + action: action, + actionHandler: actionHandler, + 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 useful anymore. + // left: target.left, + // top: target.top, + theta: degreesToRadians(target.angle), + // end of unsure + width: target.width * target.scaleX, + shiftKey: e.shiftKey, + altKey: altKey, + original: fabric.util.saveObjectTransform(target), + }; - this._currentTransform = { - target: target, - action: action, - corner: corner, - scaleX: target.scaleX, - scaleY: target.scaleY, - skewX: target.skewX, - skewY: target.skewY, - 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, - left: target.left, - top: target.top, - theta: degreesToRadians(target.angle), - width: target.width * target.scaleX, - mouseXSign: 1, - mouseYSign: 1, - shiftKey: e.shiftKey, - altKey: e[this.centeredKey] - }; - - this._currentTransform.original = { - left: target.left, - top: target.top, - scaleX: target.scaleX, - scaleY: target.scaleY, - skewX: target.skewX, - skewY: target.skewY, - originX: origin.x, - originY: origin.y - }; - - this._resetCurrentTransform(); - }, - - /** - * 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'; + if (this._shouldCenterTransform(target, action, altKey)) { + transform.originX = 'center'; + transform.originY = 'center'; } - 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.get('lockScalingX'), - lockScalingY = target.get('lockScalingY'), - lockScalingFlip = target.get('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, - changeX, changeY, scaleX, scaleY; - - scaleX = localMouse.x * target.scaleX / _dim.x; - scaleY = localMouse.y * target.scaleY / _dim.y; - changeX = target.scaleX !== scaleX; - changeY = target.scaleY !== scaleY; - - if (lockScalingFlip && scaleX <= 0 && scaleX < target.scaleX) { - forbidScalingX = true; - } - - if (lockScalingFlip && scaleY <= 0 && scaleY < target.scaleY) { - forbidScalingY = true; - } - - if (by === 'equally' && !lockScalingX && !lockScalingY) { - forbidScalingX || forbidScalingY || (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 = scaled || changeX)); - } - else if (by === 'y' && !target.get('lockUniScaling')) { - forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY)); - } - transform.newScaleX = scaleX; - transform.newScaleY = scaleY; - 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; - - // We use transform.scaleX/Y instead of target.scaleX/Y - // because the object may have a min scale and we'll loose the proportions - transform.newScaleX = transform.original.scaleX * dist / lastDist; - transform.newScaleY = transform.original.scaleY * dist / lastDist; - scaled = transform.newScaleX !== target.scaleX || transform.newScaleY !== target.scaleY; - target.set('scaleX', transform.newScaleX); - target.set('scaleY', transform.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; - - if (t.target.get('lockRotation')) { - return false; - } - - var lastAngle = atan2(t.ey - t.top, t.ex - t.left), - curAngle = atan2(y - t.top, x - t.left), - angle = radiansToDegrees(curAngle - lastAngle + t.theta), - hasRoated = true; - - if (t.target.snapAngle > 0) { - var snapAngle = t.target.snapAngle, - snapThreshold = t.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 (t.target.angle === angle) { - hasRoated = false; - } - else { - t.target.angle = angle; - } - - return hasRoated; + transform.original.originX = origin.x; + transform.original.originY = origin.y; + this._currentTransform = transform; + this._beforeTransform(e); }, /** @@ -9593,38 +12321,25 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.upperCanvasEl.style.cursor = value; }, - /** - * @param {fabric.Object} target to reset transform - * @private - */ - _resetObjectTransform: function (target) { - target.scaleX = 1; - target.scaleY = 1; - target.skewX = 0; - target.skewY = 0; - target.setAngle(0); - }, - /** * @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); + var selector = this._groupSelector, + viewportStart = new fabric.Point(selector.ex, selector.ey), + start = fabric.util.transformPoint(viewportStart, this.viewportTransform), + viewportExtent = new fabric.Point(selector.ex + selector.left, selector.ey + selector.top), + extent = fabric.util.transformPoint(viewportExtent, this.viewportTransform), + minX = Math.min(start.x, extent.x), + minY = Math.min(start.y, extent.y), + maxX = Math.max(start.x, extent.x), + maxY = Math.max(start.y, extent.y), + strokeOffset = this.selectionLineWidth / 2; if (this.selectionColor) { ctx.fillStyle = this.selectionColor; - - ctx.fillRect( - groupSelector.ex - ((left > 0) ? 0 : -left), - groupSelector.ey - ((top > 0) ? 0 : -top), - aleft, - atop - ); + ctx.fillRect(minX, minY, maxX - minX, maxY - minY); } if (!this.selectionLineWidth || !this.selectionBorderColor) { @@ -9633,38 +12348,23 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab ctx.lineWidth = this.selectionLineWidth; ctx.strokeStyle = this.selectionBorderColor; + minX += strokeOffset; + minY += strokeOffset; + maxX -= strokeOffset; + maxY -= strokeOffset; // 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 - ); - } + fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); + ctx.strokeRect(minX, minY, maxX - minX, maxY - minY); }, /** * 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) { @@ -9673,72 +12373,61 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var ignoreZoom = true, pointer = this.getPointer(e, ignoreZoom), - activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(), - activeTarget; + activeObject = this._activeObject, + aObjects = this.getActiveObjects(), + activeTarget, activeTargetSubs, + isTouch = isTouchEvent(e), + shouldLookForActive = (aObjects.length > 1 && !skipGroup) || aObjects.length === 1; + // first check current group (if one exists) // active group does not check sub targets like normal groups. // if active group just exits. - if (activeGroup && !skipGroup && activeGroup === this._searchPossibleTargets([activeGroup], pointer)) { - this._fireOverOutEvents(activeGroup, e); - return activeGroup; - } + this.targets = []; + // if we hit the corner of an activeObject, let's return that. - if (activeObject && activeObject._findTargetCorner(pointer)) { - this._fireOverOutEvents(activeObject, e); + if (shouldLookForActive && activeObject._findTargetCorner(pointer, isTouch)) { return activeObject; } - if (activeObject && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + return activeObject; + } + if (aObjects.length === 1 && + activeObject === this._searchPossibleTargets([activeObject], pointer)) { if (!this.preserveObjectStacking) { - this._fireOverOutEvents(activeObject, e); return activeObject; } else { activeTarget = activeObject; + activeTargetSubs = this.targets; + this.targets = []; } } - - this.targets = []; var target = this._searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; + this.targets = activeTargetSubs; } - this._fireOverOutEvents(target, e); 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 */ - _fireOverOutEvents: function(target, e) { - if (target) { - if (this._hoveredTarget !== target) { - if (this._hoveredTarget) { - this.fire('mouse:out', { target: this._hoveredTarget, e: e }); - this._hoveredTarget.fire('mouseout', { e: e }); - } - this.fire('mouse:over', { target: target, e: e }); - target.fire('mouseover', { e: e }); - this._hoveredTarget = target; - } - } - else if (this._hoveredTarget) { - this.fire('mouse:out', { target: this._hoveredTarget, e: e }); - this._hoveredTarget.fire('mouseout', { e: e }); - this._hoveredTarget = null; - } - }, - - /** - * @private - */ - _checkTarget: function(pointer, obj) { + _checkTarget: function(pointer, obj, globalPointer) { if (obj && obj.visible && obj.evented && - this.containsPoint(null, obj, pointer)){ + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + obj.containsPoint(pointer) + ) { if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { - var isTransparent = this.isTargetTransparent(obj, pointer.x, pointer.y); + var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); if (!isTransparent) { return true; } @@ -9750,20 +12439,25 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** + * 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, normalizedPointer, subTarget; + var target, i = objects.length, subTarget; // Do not check for currently grouped objects, since we check the parent group itself. - // untill we call this function specifically to search inside the activeGroup + // until we call this function specifically to search inside the activeGroup while (i--) { - if (this._checkTarget(pointer, objects[i])) { + var objToCheck = objects[i]; + var pointerToUse = objToCheck.group ? + this._normalizePointer(objToCheck.group, pointer) : pointer; + if (this._checkTarget(pointerToUse, objToCheck, pointer)) { target = objects[i]; - if (target.type === 'group' && target.subTargetCheck) { - normalizedPointer = this._normalizePointer(target, pointer); - subTarget = this._searchPossibleTargets(target._objects, normalizedPointer); + if (target.subTargetCheck && target instanceof fabric.Group) { + subTarget = this._searchPossibleTargets(target._objects, pointer); subTarget && this.targets.push(subTarget); } break; @@ -9792,6 +12486,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * 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. @@ -9800,11 +12496,17 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Boolean} ignoreZoom * @return {Object} object with "x" and "y" number values */ - getPointer: function (e, ignoreZoom, upperCanvasEl) { - if (!upperCanvasEl) { - upperCanvasEl = this.upperCanvasEl; + 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, @@ -9820,13 +12522,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } this.calcOffset(); - pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; if (!ignoreZoom) { pointer = this.restorePointerVpt(pointer); } + var retinaScaling = this.getRetinaScaling(); + if (retinaScaling !== 1) { + pointer.x /= retinaScaling; + pointer.y /= retinaScaling; + } + if (boundsWidth === 0 || boundsHeight === 0) { // If bounds are not available (i.e. not visible), do not apply scale. cssScale = { width: 1, height: 1 }; @@ -9849,16 +12556,32 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized */ _createUpperCanvas: function () { - var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''); + var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''), + lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; - this.upperCanvasEl = this._createCanvasElement(); - fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); + // there is no need to create a new upperCanvas element if we have already one. + if (upperCanvasEl) { + upperCanvasEl.className = ''; + } + else { + upperCanvasEl = this._createCanvasElement(); + this.upperCanvasEl = upperCanvasEl; + } + fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); - this.wrapperEl.appendChild(this.upperCanvasEl); + this.wrapperEl.appendChild(upperCanvasEl); - this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl); - this._applyCanvasStyle(this.upperCanvasEl); - this.contextTop = this.upperCanvasEl.getContext('2d'); + this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); + this._applyCanvasStyle(upperCanvasEl); + this.contextTop = upperCanvasEl.getContext('2d'); + }, + + /** + * Returns context of top canvas where interactions are drawn + * @returns {CanvasRenderingContext2D} + */ + getTopContext: function () { + return this.contextTop; }, /** @@ -9879,8 +12602,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 'class': this.containerClass }); fabric.util.setStyle(this.wrapperEl, { - width: this.getWidth() + 'px', - height: this.getHeight() + 'px', + width: this.width + 'px', + height: this.height + 'px', position: 'relative' }); fabric.util.makeElementUnselectable(this.wrapperEl); @@ -9891,8 +12614,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {HTMLElement} element canvas element to apply styles on */ _applyCanvasStyle: function (element) { - var width = this.getWidth() || element.width, - height = this.getHeight() || element.height; + var width = this.width || element.width, + height = this.height || element.height; fabric.util.setStyle(element, { position: 'absolute', @@ -9900,7 +12623,8 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab height: height + 'px', left: 0, top: 0, - 'touch-action': 'none' + 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', + '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' }); element.width = width; element.height = height; @@ -9908,7 +12632,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * Copys the the entire inline style from one element (fromEl) to another (toEl) + * 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 @@ -9934,19 +12658,96 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab }, /** - * @private - * @param {Object} object + * Returns currently active object + * @return {fabric.Object} active object */ - _setActiveObject: function(object) { - var obj = this._activeObject; - if (obj) { - obj.set('active', false); - if (object !== obj && obj.onDeselect && typeof obj.onDeselect === 'function') { - obj.onDeselect(); + 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]; } } - this._activeObject = object; - object.set('active', true); + 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 (obj === this._hoveredTarget){ + this._hoveredTarget = null; + this._hoveredTargets = []; + } + 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 = []; + oldObjects.forEach(function(oldObject) { + if (objects.indexOf(oldObject) === -1) { + somethingChanged = true; + oldObject.fire('deselected', { + e: e, + target: oldObject + }); + removed.push(oldObject); + } + }); + objects.forEach(function(object) { + if (oldObjects.indexOf(object) === -1) { + somethingChanged = true; + object.fire('selected', { + e: e, + target: object + }); + added.push(object); + } + }); + if (oldObjects.length > 0 && objects.length > 0) { + somethingChanged && this.fire('selection:updated', { + e: e, + selected: added, + deselected: removed, + }); + } + else if (objects.length > 0) { + this.fire('selection:created', { + e: e, + selected: added, + }); + } + else if (oldObjects.length > 0) { + this.fire('selection:cleared', { + e: e, + deselected: removed, + }); + } }, /** @@ -9957,60 +12758,61 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ setActiveObject: function (object, e) { - var currentActiveObject = this.getActiveObject(); - if (currentActiveObject && currentActiveObject !== object) { - currentActiveObject.fire('deselected', { e: e }); - } - this._setActiveObject(object); - this.renderAll(); - this.fire('object:selected', { target: object, e: e }); - object.fire('selected', { e: e }); + var currentActives = this.getActiveObjects(); + this._setActiveObject(object, e); + this._fireSelectionEvents(currentActives, e); return this; }, /** - * Returns currently active object - * @return {fabric.Object} active object + * This is a private method for now. + * This is supposed to be equivalent to setActiveObject but without firing + * any event. There is commitment to have this stay this way. + * This is the functional part of setActiveObject. + * @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 */ - getActiveObject: function () { - return this._activeObject; - }, - - /** - * @private - * @param {fabric.Object} obj Object that was removed - */ - _onObjectRemoved: function(obj) { - // removing active object should fire "selection:cleared" events - if (this.getActiveObject() === obj) { - 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); + _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; }, /** + * This is a private method for now. + * This is supposed to be equivalent to discardActiveObject but without firing + * any events. There is commitment to have this stay this way. + * This is the functional part of discardActiveObject. + * @param {Event} [e] Event (passed along when firing "object:deselected") + * @param {Object} object to set as active + * @return {Boolean} true if the selection happened * @private */ - _discardActiveObject: function() { + _discardActiveObject: function(e, object) { var obj = this._activeObject; if (obj) { - obj.set('active', false); - if (obj.onDeselect && typeof obj.onDeselect === 'function') { - obj.onDeselect(); + // onDeselect return TRUE to cancel selection; + if (obj.onDeselect({ e: e, object: object })) { + return false; } + this._activeObject = null; } - 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 parmater and + * 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 @@ -10018,114 +12820,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ discardActiveObject: function (e) { - var activeObject = this._activeObject; - if (activeObject) { + var currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); + if (currentActives.length) { this.fire('before:selection:cleared', { target: activeObject, e: e }); - this._discardActiveObject(); - this.fire('selection:cleared', { e: e }); - activeObject.fire('deselected', { e: e }); } - return this; - }, - - /** - * @private - * @param {fabric.Group} group - */ - _setActiveGroup: function(group) { - this._activeGroup = group; - if (group) { - group.set('active', true); - } - }, - - /** - * Sets active group to a specified one. If the function is called by fabric - * as a consequence of a mouse event, the event is passed as a parmater and - * sent to the fire function for the custom events. When used as a method the - * e param does not have any application. - * @param {fabric.Group} group Group to set as a current one - * @param {Event} e Event object - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveGroup: function (group, e) { - this._setActiveGroup(group); - if (group) { - this.fire('object:selected', { target: group, e: e }); - group.fire('selected', { e: e }); - } - return this; - }, - - /** - * Returns currently active group - * @return {fabric.Group} Current group - */ - getActiveGroup: function () { - return this._activeGroup; - }, - - /** - * @private - */ - _discardActiveGroup: function() { - var g = this.getActiveGroup(); - if (g) { - g.destroy(); - } - this.setActiveGroup(null); - }, - - /** - * Discards currently active group and fire events If the function is called by fabric - * as a consequence of a mouse event, the event is passed as a parmater and - * sent to the fire function for the custom events. When used as a method the - * e param does not have any application. - * @return {fabric.Canvas} thisArg - * @chainable - */ - discardActiveGroup: function (e) { - var g = this.getActiveGroup(); - if (g) { - this.fire('before:selection:cleared', { e: e, target: g }); - this._discardActiveGroup(); - this.fire('selection:cleared', { e: e }); - } - return this; - }, - - /** - * Deactivates all objects on canvas, removing any active group or object - * @return {fabric.Canvas} thisArg - * @chainable - */ - deactivateAll: function () { - var allObjects = this.getObjects(), - i = 0, - len = allObjects.length, - obj; - for ( ; i < len; i++) { - obj = allObjects[i]; - obj && obj.set('active', false); - } - this._discardActiveGroup(); - this._discardActiveObject(); - return this; - }, - - /** - * Deactivates all objects and dispatches appropriate events If the function is called by fabric - * as a consequence of a mouse event, the event is passed as a parmater and - * sent to the fire function for the custom events. When used as a method the - * e param does not have any application. - * @return {fabric.Canvas} thisArg - * @chainable - */ - deactivateAllWithDispatch: function (e) { - this.discardActiveGroup(e); - this.discardActiveObject(e); - this.deactivateAll(); + this._discardActiveObject(e); + this._fireSelectionEvents(currentActives, e); return this; }, @@ -10135,16 +12835,21 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ dispose: function () { - this.callSuper('dispose'); var wrapper = this.wrapperEl; this.removeListeners(); wrapper.removeChild(this.upperCanvasEl); wrapper.removeChild(this.lowerCanvasEl); - delete this.upperCanvasEl; + 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; }, @@ -10154,7 +12859,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @chainable */ clear: function () { - this.discardActiveGroup(); + // this.discardActiveGroup(); this.discardActiveObject(); this.clearContext(this.contextTop); return this.callSuper('clear'); @@ -10165,25 +12870,10 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {CanvasRenderingContext2D} ctx Context to render controls on */ drawControls: function(ctx) { - var activeGroup = this.getActiveGroup(); + var activeObject = this._activeObject; - if (activeGroup) { - activeGroup._renderControls(ctx); - } - else { - this._drawObjectsControls(ctx); - } - }, - - /** - * @private - */ - _drawObjectsControls: function(ctx) { - for (var i = 0, len = this._objects.length; i < len; ++i) { - if (!this._objects[i] || !this._objects[i].active) { - continue; - } - this._objects[i]._renderControls(ctx); + if (activeObject) { + activeObject._renderControls(ctx); } }, @@ -10209,14 +12899,14 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @returns the original values of instance which were changed */ _realizeGroupTransformOnObject: function(instance) { - var layoutProps = ['angle', 'flipX', 'flipY', 'height', 'left', 'scaleX', 'scaleY', 'top', 'width']; - if (instance.group && instance.group === this.getActiveGroup()) { + 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.getActiveGroup().realizeTransform(instance); + fabric.util.addTransformToObject(instance, this._activeObject.calcOwnMatrix()); return originalValues; } else { @@ -10240,13 +12930,19 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @private */ _setSVGObject: function(markup, instance, reviver) { - var originalProperties; //If the object is in a selection group, simulate what would happen to that //object when the group is deselected - originalProperties = this._realizeGroupTransformOnObject(instance); + 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, @@ -10256,93 +12952,103 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab fabric.Canvas[prop] = fabric.StaticCanvas[prop]; } } - - if (fabric.isTouchSupported) { - /** @ignore */ - fabric.Canvas.prototype._setCursorFromEvent = function() { }; - } - - /** - * @ignore - * @class fabric.Element - * @alias fabric.Canvas - * @deprecated Use {@link fabric.Canvas} instead. - * @constructor - */ - fabric.Element = fabric.Canvas; })(); (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; + var 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 + * Contains the id of the touch event that owns the fabric transform + * @type Number * @private */ - cursorMap: [ - 'n-resize', - 'ne-resize', - 'e-resize', - 'se-resize', - 's-resize', - 'sw-resize', - 'w-resize', - 'nw-resize' - ], + 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'); + }, - addListener(fabric.window, 'resize', this._onResize); + /** + * return an event prefix pointer or mouse. + * @private + */ + _getEventPrefix: function () { + return this.enablePointerEvents ? 'pointer' : 'mouse'; + }, - // mouse events - addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - addListener(this.upperCanvasEl, 'mouseout', this._onMouseOut); - addListener(this.upperCanvasEl, 'mouseenter', this._onMouseEnter); - addListener(this.upperCanvasEl, 'wheel', this._onMouseWheel); - addListener(this.upperCanvasEl, 'contextmenu', this._onContextMenu); - - // touch events - addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown, { passive: false }); - addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove, { passive: false }); - - if (typeof eventjs !== 'undefined' && 'add' in eventjs) { - eventjs.add(this.upperCanvasEl, 'gesture', this._onGesture); - eventjs.add(this.upperCanvasEl, 'drag', this._onDrag); - eventjs.add(this.upperCanvasEl, 'orientation', this._onOrientationChange); - eventjs.add(this.upperCanvasEl, 'shake', this._onShake); - eventjs.add(this.upperCanvasEl, 'longpress', this._onLongPress); + 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); @@ -10353,31 +13059,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this._onMouseOut = this._onMouseOut.bind(this); this._onMouseEnter = this._onMouseEnter.bind(this); this._onContextMenu = this._onContextMenu.bind(this); - }, - - /** - * Removes all event listeners - */ - removeListeners: function() { - removeListener(fabric.window, 'resize', this._onResize); - - removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - removeListener(this.upperCanvasEl, 'mouseout', this._onMouseOut); - removeListener(this.upperCanvasEl, 'mouseenter', this._onMouseEnter); - removeListener(this.upperCanvasEl, 'wheel', this._onMouseWheel); - removeListener(this.upperCanvasEl, 'contextmenu', this._onContextMenu); - - removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - - if (typeof eventjs !== 'undefined' && 'remove' in eventjs) { - eventjs.remove(this.upperCanvasEl, 'gesture', this._onGesture); - eventjs.remove(this.upperCanvasEl, 'drag', this._onDrag); - eventjs.remove(this.upperCanvasEl, 'orientation', this._onOrientationChange); - eventjs.remove(this.upperCanvasEl, 'shake', this._onShake); - eventjs.remove(this.upperCanvasEl, 'longpress', this._onLongPress); - } + 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._onDrop.bind(this); + this.eventsBound = true; }, /** @@ -10415,13 +13102,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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(); - } - }); - } + + var _this = this; + this._hoveredTargets.forEach(function(_target){ + _this.fire('mouse:out', { target: target, e: e }); + _target && target.fire('mouseout', { e: e }); + }); + this._hoveredTargets = []; }, /** @@ -10429,9 +13116,16 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mouseenter */ _onMouseEnter: function(e) { - if (!this.findTarget(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; + this._hoveredTargets = []; } }, @@ -10462,6 +13156,29 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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); + }, + + /** + * `drop:before` is a an event that allow you to schedule logic + * before the `drop` event. Prefer `drop` event always, but if you need + * to run some drop-disabling logic on an event, since there is no way + * to handle event handlers ordering, use `drop:before` + * @param {Event} e + */ + _onDrop: function (e) { + this._simpleEventHandler('drop:before', e); + return this._simpleEventHandler('drop', e); + }, + /** * @private * @param {Event} e Event object fired on mousedown @@ -10474,27 +13191,115 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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); + }, - addListener(fabric.document, 'touchend', this._onMouseUp, { passive: false }); - addListener(fabric.document, 'touchmove', this._onMouseMove, { passive: false }); - - removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - - if (e.type === 'touchstart') { - // Unbind mousedown to prevent double triggers from touch devices - removeListener(this.upperCanvasEl, 'mousedown', 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, 'mouseup', this._onMouseUp); - addListener(fabric.document, 'mousemove', this._onMouseMove); + 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); }, /** @@ -10503,23 +13308,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _onMouseUp: function (e) { this.__onMouseUp(e); - - removeListener(fabric.document, 'mouseup', this._onMouseUp); - removeListener(fabric.document, 'touchend', this._onMouseUp); - - removeListener(fabric.document, 'mousemove', this._onMouseMove); - removeListener(fabric.document, 'touchmove', this._onMouseMove); - - addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove, { passive: false }); - - if (e.type === 'touchend') { - // Wait 400ms before rebinding mousedown to prevent double triggers - // from touch devices - var _this = this; - setTimeout(function() { - addListener(_this.upperCanvasEl, 'mousedown', _this._onMouseDown); - }, 400); + 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); } }, @@ -10543,31 +13338,24 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * Decides whether the canvas should be redrawn in mouseup and mousedown events. * @private * @param {Object} target - * @param {Object} pointer */ - _shouldRender: function(target, pointer) { - var activeObject = this.getActiveGroup() || this.getActiveObject(); + _shouldRender: function(target) { + var activeObject = this._activeObject; - if (activeObject && activeObject.isEditing && target === 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 !!( - (target && ( - target.isMoving || - target !== activeObject)) - || - (!target && !!activeObject) - || - (!target && !activeObject && !this._groupSelector) - || - (pointer && - this._previousPointer && - this.selection && ( - pointer.x !== this._previousPointer.x || - pointer.y !== this._previousPointer.y)) - ); + return false; }, /** @@ -10578,64 +13366,145 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mouseup */ __onMouseUp: function (e) { - var target, searchTarget = true, transform = this._currentTransform, - groupSelector = this._groupSelector, + 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(); - searchTarget = !transform.actionPerformed; + this._finalizeCurrentTransform(e); + shouldRender = transform.actionPerformed; } - - target = searchTarget ? this.findTarget(e, true) : transform.target; - - var shouldRender = this._shouldRender(target, this.getPointer(e)); - - if (target || !isClick) { + if (!isClick) { + var targetWasActive = target === this._activeObject; this._maybeGroupObjects(e); + if (!shouldRender) { + shouldRender = ( + this._shouldRender(target) || + (!targetWasActive && target === this._activeObject) + ); + } } - else { - // those are done by default on mouse up - // by _maybeGroupObjects, we are skipping it in case of no target find - this._groupSelector = null; - this._currentTransform = null; - } - + var corner, pointer; if (target) { + corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + if (target.selectable && target !== this._activeObject && target.activeOn === 'up') { + this.setActiveObject(target, e); + shouldRender = true; + } + else { + var control = target.controls[corner], + mouseUpHandler = control && control.getMouseUpHandler(e, target, control); + if (mouseUpHandler) { + pointer = this.getPointer(e); + mouseUpHandler(e, transform, pointer.x, pointer.y); + } + } target.isMoving = false; } - - this._handleCursorAndEvent(e, target, 'up'); - target && (target.__corner = 0); - shouldRender && this.renderAll(); - }, - - /** - * set cursor for mouse up and handle mouseUp event - * @param {Event} e event from mouse - * @param {fabric.Object} target receiving event - * @param {String} eventType event to fire (up, down or move) - */ - _handleCursorAndEvent: function(e, target, eventType) { + // if we are ending up a transform on a different control or a new object + // fire the original mouse up from the corner that started the transform + if (transform && (transform.target !== target || transform.corner !== corner)) { + var originalControl = transform.target && transform.target.controls[transform.corner], + originalMouseUpHandler = originalControl && originalControl.getMouseUpHandler(e, target, control); + pointer = pointer || this.getPointer(e); + originalMouseUpHandler && originalMouseUpHandler(e, transform, pointer.x, pointer.y); + } this._setCursorFromEvent(e, target); - this._handleEvent(e, eventType, target ? target : null); + 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, targetObj) { - var target = typeof targetObj === 'undefined' ? this.findTarget(e) : targetObj, + _handleEvent: function(e, eventType, button, isClick) { + var target = this._target, targets = this.targets || [], - options = { e: e, target: target, subTargets: targets }; + options = { + e: e, + target: target, + subTargets: targets, + button: button || LEFT_CLICK, + isClick: isClick || false, + pointer: this._pointer, + absolutePointer: this._absolutePointer, + transform: this._currentTransform + }; + if (eventType === 'up') { + options.currentTarget = this.findTarget(e); + options.currentSubTargets = this.targets; + } this.fire('mouse:' + eventType, options); target && target.fire('mouse' + eventType, options); for (var i = 0; i < targets.length; i++) { @@ -10645,45 +13514,27 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * @private + * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event */ - _finalizeCurrentTransform: function() { + _finalizeCurrentTransform: function(e) { var transform = this._currentTransform, - target = transform.target; + target = transform.target, + options = { + e: e, + target: target, + transform: transform, + action: transform.action, + }; if (target._scaling) { target._scaling = false; } target.setCoords(); - this._restoreOriginXY(target); if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) { - this.fire('object:modified', { target: target }); - target.fire('modified'); - } - }, - - /** - * @private - * @param {Object} target Object to restore - */ - _restoreOriginXY: function(target) { - if (this._previousOriginX && this._previousOriginY) { - - var originPoint = target.translateToOriginPoint( - target.getCenterPoint(), - this._previousOriginX, - this._previousOriginY); - - target.originX = this._previousOriginX; - target.originY = this._previousOriginY; - - target.left = originPoint.x; - target.top = originPoint.y; - - this._previousOriginX = null; - this._previousOriginY = null; + this._fire('modified', options); } }, @@ -10693,12 +13544,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _onMouseDownInDrawingMode: function(e) { this._isCurrentlyDrawing = true; - this.discardActiveObject(e).renderAll(); - if (this.clipTo) { - fabric.util.clipContext(this, this.contextTop); + if (this.getActiveObject()) { + this.discardActiveObject(e).requestRenderAll(); } var pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseDown(pointer); + this.freeDrawingBrush.onMouseDown(pointer, { e: e, pointer: pointer }); this._handleEvent(e, 'down'); }, @@ -10709,7 +13559,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _onMouseMoveInDrawingMode: function(e) { if (this._isCurrentlyDrawing) { var pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseMove(pointer); + this.freeDrawingBrush.onMouseMove(pointer, { e: e, pointer: pointer }); } this.setCursor(this.freeDrawingCursor); this._handleEvent(e, 'move'); @@ -10720,11 +13570,8 @@ 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(); - } - this.freeDrawingBrush.onMouseUp(); + var pointer = this.getPointer(e); + this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ e: e, pointer: pointer }); this._handleEvent(e, 'up'); }, @@ -10737,22 +13584,20 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mousedown */ __onMouseDown: function (e) { - - var target = this.findTarget(e); - + this._cacheTransformEventData(e); + this._handleEvent(e, 'down:before'); + var target = this._target; // if right click just fire events - var isRightClick = 'which' in e ? e.which === 3 : e.button === 2; - if (isRightClick) { + if (checkClick(e, RIGHT_CLICK)) { if (this.fireRightClick) { - this._handleEvent(e, 'down', target ? target : null); + this._handleEvent(e, 'down', RIGHT_CLICK); } return; } - var isMiddleClick = 'which' in e ? e.which === 2 : e.button === 1; - if (isMiddleClick) { + if (checkClick(e, MIDDLE_CLICK)) { if (this.fireMiddleClick) { - this._handleEvent(e, 'down', target ? target : null); + this._handleEvent(e, 'down', MIDDLE_CLICK); } return; } @@ -10762,120 +13607,101 @@ 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; } + var pointer = this._pointer; // save pointer for check in __onMouseUp event - var pointer = this.getPointer(e, true); this._previousPointer = pointer; - - var shouldRender = this._shouldRender(target, pointer), + var shouldRender = this._shouldRender(target), shouldGroup = this._shouldGroup(e, target); - if (this._shouldClearSelection(e, target)) { - this._clearSelection(e, target, pointer); + this.discardActiveObject(e); } else if (shouldGroup) { this._handleGrouping(e, target); - target = this.getActiveGroup(); + target = this._activeObject; } - if (target) { - if (target.selectable && (target.__corner || !shouldGroup)) { - this._beforeTransform(e, target); - this._setupCurrentTransform(e, target); - } - var activeObject = this.getActiveObject(); - if (target !== this.getActiveGroup() && target !== activeObject) { - this.deactivateAll(); - if (target.selectable) { - activeObject && activeObject.fire('deselected', { e: e }); - this.setActiveObject(target, e); - } - } - } - this._handleEvent(e, 'down', target ? target : null); - // we must renderAll so that we update the visuals - shouldRender && this.renderAll(); - }, - - /** - * @private - */ - _beforeTransform: function(e, target) { - this.stateful && target.saveState(); - - // determine if it's a drag or rotate case - if (target._findTargetCorner(this.getPointer(e))) { - this.onBeforeScaleRotate(target); - } - - }, - - /** - * @private - */ - _clearSelection: function(e, target, pointer) { - this.deactivateAllWithDispatch(e); - - if (target && target.selectable) { - this.setActiveObject(target, e); - } - else if (this.selection) { + if (this.selection && (!target || + (!target.selectable && !target.isEditing && target !== this._activeObject))) { this._groupSelector = { - ex: pointer.x, - ey: pointer.y, + ex: this._absolutePointer.x, + ey: this._absolutePointer.y, top: 0, left: 0 }; } + + if (target) { + var alreadySelected = target === this._activeObject; + if (target.selectable && target.activeOn === 'down') { + this.setActiveObject(target, e); + } + var corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + target.__corner = corner; + if (target === this._activeObject && (corner || !shouldGroup)) { + this._setupCurrentTransform(e, target, alreadySelected); + var control = target.controls[corner], + pointer = this.getPointer(e), + mouseDownHandler = control && control.getMouseDownHandler(e, target, control); + if (mouseDownHandler) { + mouseDownHandler(e, this._currentTransform, pointer.x, pointer.y); + } + } + } + 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 - * @param {Object} target Object for that origin is set to center */ - _setOriginToCenter: function(target) { - this._previousOriginX = this._currentTransform.target.originX; - this._previousOriginY = this._currentTransform.target.originY; - - var center = target.getCenterPoint(); - - target.originX = 'center'; - target.originY = 'center'; - - target.left = center.x; - target.top = center.y; - - this._currentTransform.left = target.left; - this._currentTransform.top = target.top; - }, - - /** - * @private - * @param {Object} target Object for that center is set to origin - */ - _setCenterToOrigin: function(target) { - var originPoint = target.translateToOriginPoint( - target.getCenterPoint(), - this._previousOriginX, - this._previousOriginY); - - target.originX = this._previousOriginX; - target.originY = this._previousOriginY; - - target.left = originPoint.x; - target.top = originPoint.y; - - this._previousOriginX = null; - this._previousOriginY = null; + _beforeTransform: function(e) { + var t = this._currentTransform; + this.stateful && t.target.saveState(); + this.fire('before:transform', { + e: e, + transform: t, + }); }, /** * Method that defines the actions when mouse is hovering the canvas. - * The currentTransform parameter will definde whether the user is rotating/scaling/translating + * 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. @@ -10883,14 +13709,16 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @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 (typeof e.touches !== 'undefined' && e.touches.length > 1) { + + if (!this._isMainEvent(e)) { return; } @@ -10898,7 +13726,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab // We initially clicked in an empty area, so we draw a box for multiple selection if (groupSelector) { - pointer = this.getPointer(e, true); + pointer = this._absolutePointer; groupSelector.left = pointer.x - groupSelector.ex; groupSelector.top = pointer.y - groupSelector.ey; @@ -10906,13 +13734,101 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab this.renderTop(); } else if (!this._currentTransform) { - target = this.findTarget(e); + target = this.findTarget(e) || null; this._setCursorFromEvent(e, target); + this._fireOverOutEvents(target, e); } else { this._transformObject(e); } - this._handleEvent(e, 'move', target ? target : null); + 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) { + var _hoveredTarget = this._hoveredTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _hoveredTarget, + evtOut: 'mouseout', + canvasEvtOut: 'mouse:out', + evtIn: 'mouseover', + canvasEvtIn: 'mouse:over', + }); + for (var i = 0; i < length; i++){ + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'mouseout', + evtIn: 'mouseover', + }); + } + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + }, + + /** + * 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) { + var _draggedoverTarget = this._draggedoverTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents(target, e, { + oldTarget: _draggedoverTarget, + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + for (var i = 0; i < length; i++) { + this.fireSyntheticInOutEvents(targets[i], e, { + oldTarget: _hoveredTargets[i], + evtOut: 'dragleave', + evtIn: 'dragenter', + }); + } + this._draggedoverTarget = target; + }, + + /** + * 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 = config.oldTarget, 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 }; + } + 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); + } }, /** @@ -10920,7 +13836,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Event} e Event object fired on mouseup */ __onMouseWheel: function(e) { + this._cacheTransformEventData(e); this._handleEvent(e, 'wheel'); + this._resetTransformEventData(); }, /** @@ -10932,14 +13850,11 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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.renderAll(); + transform.actionPerformed && this.requestRenderAll(); }, /** @@ -10948,34 +13863,18 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab _performTransformAction: function(e, transform, pointer) { var x = pointer.x, y = pointer.y, - target = transform.target, action = transform.action, - actionPerformed = false; + actionPerformed = false, + actionHandler = transform.actionHandler; + // this object could be created from the function in the control handlers - if (action === 'rotate') { - (actionPerformed = this._rotateObject(x, y)) && this._fire('rotating', target, e); + + if (actionHandler) { + actionPerformed = actionHandler(e, transform, x, y); } - else if (action === 'scale') { - (actionPerformed = this._onScale(e, transform, x, y)) && this._fire('scaling', target, e); - } - else if (action === 'scaleX') { - (actionPerformed = this._scaleObject(x, y, 'x')) && this._fire('scaling', target, e); - } - else if (action === 'scaleY') { - (actionPerformed = this._scaleObject(x, y, 'y')) && this._fire('scaling', target, e); - } - else if (action === 'skewX') { - (actionPerformed = this._skewObject(x, y, 'x')) && this._fire('skewing', target, e); - } - else if (action === 'skewY') { - (actionPerformed = this._skewObject(x, y, 'y')) && this._fire('skewing', target, e); - } - else { - actionPerformed = this._translateObject(x, y); - if (actionPerformed) { - this._fire('moving', target, e); - this.setCursor(target.moveCursor || this.moveCursor); - } + if (action === 'drag' && actionPerformed) { + transform.target.isMoving = true; + this.setCursor(transform.target.moveCursor || this.moveCursor); } transform.actionPerformed = transform.actionPerformed || actionPerformed; }, @@ -10983,52 +13882,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab /** * @private */ - _fire: function(eventName, target, e) { - this.fire('object:' + eventName, { target: target, e: e }); - target.fire(eventName, { e: e }); - }, - - /** - * @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 tranform - * @param {Number} x mouse position x from origin - * @param {Number} y mouse poistion y from origin - * @return {Boolean} true if the scaling occurred - */ - _onScale: function(e, transform, x, y) { - if ((e[this.uniScaleKey] || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { - 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'); - } - }, + _fire: fabric.controlsUtils.fireEvent, /** * Sets the cursor depending on where the canvas is being hovered. @@ -11037,63 +13891,41 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Object} target Object that the mouse is hovering, if so. */ _setCursorFromEvent: function (e, target) { - if (!target || !target.selectable) { + if (!target) { this.setCursor(this.defaultCursor); return false; } - var hoverCursor = target.hoverCursor || this.hoverCursor, - activeGroup = this.getActiveGroup(), + activeSelection = this._activeObject && this._activeObject.type === 'activeSelection' ? + this._activeObject : null, // only show proper corner when group selection is not active - corner = target._findTargetCorner - && (!activeGroup || !activeGroup.contains(target)) + corner = (!activeSelection || !activeSelection.contains(target)) + // here we call findTargetCorner always with undefined for the touch parameter. + // we assume that if you are using a cursor you do not need to interact with + // the bigger touch area. && target._findTargetCorner(this.getPointer(e, true)); if (!corner) { + if (target.subTargetCheck){ + // hoverCursor should come from top-most subTarget, + // so we walk the array backwards + this.targets.concat().reverse().map(function(_target){ + hoverCursor = _target.hoverCursor || hoverCursor; + }); + } this.setCursor(hoverCursor); } else { - this._setCornerCursor(corner, target, e); - } - //actually unclear why it should return something - //is never evaluated - return true; - }, - - /** - * @private - */ - _setCornerCursor: function(corner, target, e) { - if (corner in cursorOffset) { - this.setCursor(this._getRotatedCornerCursor(corner, target, e)); - } - else if (corner === 'mtr' && target.hasRotatingPoint) { - this.setCursor(this.rotationCursor); - } - else { - this.setCursor(this.defaultCursor); - return false; + this.setCursor(this.getCornerCursor(corner, target, e)); } }, /** * @private */ - _getRotatedCornerCursor: function(corner, target, e) { - var n = Math.round((target.getAngle() % 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]; + getCornerCursor: function(corner, target, e) { + var control = target.controls[corner]; + return control.cursorStyleHandler(e, control, target); } }); })(); @@ -11113,10 +13945,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @return {Boolean} */ _shouldGroup: function(e, target) { - var activeObject = this.getActiveObject(); - return e[this.selectionKey] && target && target.selectable && - (this.getActiveGroup() || (activeObject && activeObject !== target)) - && this.selection; + var activeObject = this._activeObject; + return activeObject && this._isSelectionKeyPressed(e) && target && target.selectable && this.selection && + (activeObject !== target || activeObject.type === 'activeSelection') && !target.onSelect({ e: e }); }, /** @@ -11125,71 +13956,61 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {fabric.Object} target */ _handleGrouping: function (e, target) { - var activeGroup = this.getActiveGroup(); - - if (target === activeGroup) { + 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, bail out - if (!target) { + // if even object is not found or we are on activeObjectCorner, bail out + if (!target || !target.selectable) { return; } } - if (activeGroup) { - this._updateActiveGroup(target, e); + if (activeObject && activeObject.type === 'activeSelection') { + this._updateActiveSelection(target, e); } else { - this._createActiveGroup(target, e); - } - - if (this._activeGroup) { - this._activeGroup.saveCoords(); + this._createActiveSelection(target, e); } }, /** * @private */ - _updateActiveGroup: function(target, e) { - var activeGroup = this.getActiveGroup(); - - if (activeGroup.contains(target)) { - - activeGroup.removeWithUpdate(target); - target.set('active', false); - - if (activeGroup.size() === 1) { - // remove group alltogether if after removal it only contains 1 object - this.discardActiveGroup(e); + _updateActiveSelection: function(target, e) { + var activeSelection = this._activeObject, + currentActiveObjects = activeSelection._objects.slice(0); + if (activeSelection.contains(target)) { + activeSelection.removeWithUpdate(target); + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + if (activeSelection.size() === 1) { // activate last remaining object - this.setActiveObject(activeGroup.item(0), e); - return; + this._setActiveObject(activeSelection.item(0), e); } } else { - activeGroup.addWithUpdate(target); + activeSelection.addWithUpdate(target); + this._hoveredTarget = activeSelection; + this._hoveredTargets = this.targets.concat(); } - this.fire('selection:created', { target: activeGroup, e: e }); - activeGroup.set('active', true); + this._fireSelectionEvents(currentActiveObjects, e); }, /** * @private */ - _createActiveGroup: function(target, e) { - - if (this._activeObject && target !== this._activeObject) { - - var group = this._createGroup(target); - group.addWithUpdate(); - - this.setActiveGroup(group, e); - this._activeObject = null; - - this.fire('selection:created', { target: group, e: e }); - } - - target.set('active', true); + _createActiveSelection: function(target, e) { + var currentActives = this.getActiveObjects(), group = this._createGroup(target); + this._hoveredTarget = group; + // ISSUE 4115: should we consider subTargets here? + // this._hoveredTargets = []; + // this._hoveredTargets = this.targets.concat(); + this._setActiveObject(group, e); + this._fireSelectionEvents(currentActives, e); }, /** @@ -11197,14 +14018,13 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @param {Object} target */ _createGroup: function(target) { - - var objects = this.getObjects(), + 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.Group(groupObjects, { + return new fabric.ActiveSelection(groupObjects, { canvas: this }); }, @@ -11215,28 +14035,25 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab */ _groupSelectedObjects: function (e) { - var group = this._collectObjects(); + 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) { - group = new fabric.Group(group.reverse(), { + aGroup = new fabric.ActiveSelection(group.reverse(), { canvas: this }); - group.addWithUpdate(); - this.setActiveGroup(group, e); - group.saveCoords(); - this.fire('selection:created', { target: group, e: e }); - this.renderAll(); + this.setActiveObject(aGroup, e); } }, /** * @private */ - _collectObjects: function() { + _collectObjects: function(e) { var group = [], currentObject, x1 = this._groupSelector.ex, @@ -11245,8 +14062,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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]; @@ -11254,14 +14072,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab continue; } - if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || - currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || - currentObject.containsPoint(selectionX1Y1) || - currentObject.containsPoint(selectionX2Y2) + if ((allowIntersect && currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2, true)) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2, true) || + (allowIntersect && currentObject.containsPoint(selectionX1Y1, null, true)) || + (allowIntersect && currentObject.containsPoint(selectionX2Y2, null, true)) ) { - currentObject.set('active', true); group.push(currentObject); - // only add one object if it's a click if (isClick) { break; @@ -11269,6 +14085,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab } } + if (group.length > 1) { + group = group.filter(function(object) { + return !object.onSelect({ e: e }); + }); + } + return group; }, @@ -11279,17 +14101,9 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab if (this.selection && this._groupSelector) { this._groupSelectedObjects(e); } - - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.setObjectsCoords().setCoords(); - activeGroup.isMoving = false; - this.setCursor(this.defaultCursor); - } - + this.setCursor(this.defaultCursor); // clear selection and current transformation this._groupSelector = null; - this._currentTransform = null; } }); @@ -11297,9 +14111,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab (function () { - - var supportQuality = fabric.StaticCanvas.supports('toDataURLWithQuality'); - fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** @@ -11307,11 +14118,12 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab * @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.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 @@ -11338,84 +14150,59 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab var format = options.format || 'png', quality = options.quality || 1, - multiplier = options.multiplier || 1, - cropping = { - left: options.left || 0, - top: options.top || 0, - width: options.width || 0, - height: options.height || 0, - }; - return this.__toDataURLWithMultiplier(format, quality, cropping, multiplier); + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? this.getRetinaScaling() : 1), + canvasEl = this.toCanvasElement(multiplier, options); + return fabric.util.toDataURL(canvasEl, format, quality); }, /** - * @private + * 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. */ - __toDataURLWithMultiplier: function(format, quality, cropping, multiplier) { - - var origWidth = this.getWidth(), - origHeight = this.getHeight(), - scaledWidth = (cropping.width || this.getWidth()) * multiplier, - scaledHeight = (cropping.height || this.getHeight()) * multiplier, + 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) * multiplier, - translateY = (vp[5] - cropping.top) * multiplier, + translateX = (vp[4] - (cropping.left || 0)) * multiplier, + translateY = (vp[5] - (cropping.top || 0)) * multiplier, + originalInteractive = this.interactive, newVp = [newZoom, 0, 0, newZoom, translateX, translateY], - originalInteractive = this.interactive; - + 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; - // setting interactive to false avoid exporting controls - this.interactive && (this.interactive = false); - if (origWidth !== scaledWidth || origHeight !== scaledHeight) { - // this.setDimensions is going to renderAll also; - this.setDimensions({ width: scaledWidth, height: scaledHeight }); - } - else { - this.renderAll(); - } - var data = this.__toDataURL(format, quality, cropping); - originalInteractive && (this.interactive = originalInteractive); + this.width = scaledWidth; + this.height = scaledHeight; + this.calcViewportBoundaries(); + this.renderCanvas(canvasEl.getContext('2d'), this._objects); this.viewportTransform = vp; - //setDimensions with no option object is taking care of: - //this.width, this.height, this.renderAll() - this.setDimensions({ width: origWidth, height: origHeight }); - return data; - }, - - /** - * @private - */ - __toDataURL: function(format, quality) { - - var canvasEl = this.contextContainer.canvas; - // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 - if (format === 'jpg') { - format = 'jpeg'; - } - - var data = supportQuality - ? canvasEl.toDataURL('image/' + format, quality) - : canvasEl.toDataURL('image/' + format); - - return data; - }, - - /** - * Exports canvas element to a dataurl image (allowing to change image size via multiplier). - * @deprecated since 1.0.13 - * @param {String} format (png|jpeg) - * @param {Number} multiplier - * @param {Number} quality (0..1) - * @return {String} - */ - toDataURLWithMultiplier: function (format, multiplier, quality) { - return this.toDataURL({ - format: format, - multiplier: multiplier, - quality: quality - }); + this.width = originalWidth; + this.height = originalHeight; + this.calcViewportBoundaries(); + this.interactive = originalInteractive; + this.enableRetinaScaling = originalRetina; + this.contextTop = originalContextTop; + return canvasEl; }, }); @@ -11423,24 +14210,6 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab 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} @@ -11472,41 +14241,73 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ? JSON.parse(json) : fabric.util.object.clone(json); - this.clear(); + var _this = this, + clipPath = serialized.clipPath, + renderOnAddRemove = this.renderOnAddRemove; - var _this = this; - this._enlivenObjects(serialized.objects, function () { + this.renderOnAddRemove = false; + + delete serialized.clipPath; + + this._enlivenObjects(serialized.objects, function (enlivenedObjects) { + _this.clear(); _this._setBgOverlay(serialized, function () { - // 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); - callback && callback(); + 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 _this = this, - loaded = { - backgroundColor: false, - overlayColor: false, - backgroundImage: false, - overlayImage: false - }; + var loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { callback && callback(); @@ -11515,7 +14316,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati var cbIfLoaded = function () { if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { - _this.renderAll(); callback && callback(); } }; @@ -11564,25 +14364,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Function} [reviver] */ _enlivenObjects: function (objects, callback, reviver) { - var _this = this; - if (!objects || objects.length === 0) { - callback && callback(); + callback && callback([]); return; } - var renderOnAddRemove = this.renderOnAddRemove; - this.renderOnAddRemove = false; - fabric.util.enlivenObjects(objects, function(enlivenedObjects) { - 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; - callback && callback(); + callback && callback(enlivenedObjects); }, null, reviver); }, @@ -11630,13 +14418,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {Object} [callback] Receives cloned instance as a first argument */ cloneWithoutData: function(callback) { - var el = fabric.document.createElement('canvas'); + var el = fabric.util.createCanvasElement(); - el.width = this.getWidth(); - el.height = this.getHeight(); + 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(); @@ -11662,8 +14449,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati toFixed = fabric.util.toFixed, capitalize = fabric.util.string.capitalize, degreesToRadians = fabric.util.degreesToRadians, - supportsLineDash = fabric.StaticCanvas.supports('setLineDash'), - objectCaching = !fabric.isLikelyNode; + objectCaching = !fabric.isLikelyNode, + ALIASING_LIMIT = 2; if (fabric.Object) { return; @@ -11681,6 +14468,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @fires selected * @fires deselected * @fires modified + * @fires modified + * @fires moved + * @fires scaled + * @fires rotated + * @fires skewed + * * @fires rotating * @fires scaling * @fires moving @@ -11691,263 +14484,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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 */ { - /** - * Retrieves object's {@link fabric.Object#clipTo|clipping function} - * @method getClipTo - * @memberOf fabric.Object.prototype - * @return {Function} - */ - - /** - * Sets object's {@link fabric.Object#clipTo|clipping function} - * @method setClipTo - * @memberOf fabric.Object.prototype - * @param {Function} clipTo Clipping function - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#transformMatrix|transformMatrix} - * @method getTransformMatrix - * @memberOf fabric.Object.prototype - * @return {Array} transformMatrix - */ - - /** - * Sets object's {@link fabric.Object#transformMatrix|transformMatrix} - * @method setTransformMatrix - * @memberOf fabric.Object.prototype - * @param {Array} transformMatrix - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#visible|visible} state - * @method getVisible - * @memberOf fabric.Object.prototype - * @return {Boolean} True if visible - */ - - /** - * Sets object's {@link fabric.Object#visible|visible} state - * @method setVisible - * @memberOf fabric.Object.prototype - * @param {Boolean} value visible value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#shadow|shadow} - * @method getShadow - * @memberOf fabric.Object.prototype - * @return {Object} Shadow instance - */ - - /** - * Retrieves object's {@link fabric.Object#stroke|stroke} - * @method getStroke - * @memberOf fabric.Object.prototype - * @return {String} stroke value - */ - - /** - * Sets object's {@link fabric.Object#stroke|stroke} - * @method setStroke - * @memberOf fabric.Object.prototype - * @param {String} value stroke value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#strokeWidth|strokeWidth} - * @method getStrokeWidth - * @memberOf fabric.Object.prototype - * @return {Number} strokeWidth value - */ - - /** - * Sets object's {@link fabric.Object#strokeWidth|strokeWidth} - * @method setStrokeWidth - * @memberOf fabric.Object.prototype - * @param {Number} value strokeWidth value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#originX|originX} - * @method getOriginX - * @memberOf fabric.Object.prototype - * @return {String} originX value - */ - - /** - * Sets object's {@link fabric.Object#originX|originX} - * @method setOriginX - * @memberOf fabric.Object.prototype - * @param {String} value originX value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#originY|originY} - * @method getOriginY - * @memberOf fabric.Object.prototype - * @return {String} originY value - */ - - /** - * Sets object's {@link fabric.Object#originY|originY} - * @method setOriginY - * @memberOf fabric.Object.prototype - * @param {String} value originY value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#fill|fill} - * @method getFill - * @memberOf fabric.Object.prototype - * @return {String} Fill value - */ - - /** - * Sets object's {@link fabric.Object#fill|fill} - * @method setFill - * @memberOf fabric.Object.prototype - * @param {String} value Fill value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#opacity|opacity} - * @method getOpacity - * @memberOf fabric.Object.prototype - * @return {Number} Opacity value (0-1) - */ - - /** - * Sets object's {@link fabric.Object#opacity|opacity} - * @method setOpacity - * @memberOf fabric.Object.prototype - * @param {Number} value Opacity value (0-1) - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#angle|angle} (in degrees) - * @method getAngle - * @memberOf fabric.Object.prototype - * @return {Number} - */ - - /** - * Retrieves object's {@link fabric.Object#top|top position} - * @method getTop - * @memberOf fabric.Object.prototype - * @return {Number} Top value (in pixels) - */ - - /** - * Sets object's {@link fabric.Object#top|top position} - * @method setTop - * @memberOf fabric.Object.prototype - * @param {Number} value Top value (in pixels) - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#left|left position} - * @method getLeft - * @memberOf fabric.Object.prototype - * @return {Number} Left value (in pixels) - */ - - /** - * Sets object's {@link fabric.Object#left|left position} - * @method setLeft - * @memberOf fabric.Object.prototype - * @param {Number} value Left value (in pixels) - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#scaleX|scaleX} value - * @method getScaleX - * @memberOf fabric.Object.prototype - * @return {Number} scaleX value - */ - - /** - * Sets object's {@link fabric.Object#scaleX|scaleX} value - * @method setScaleX - * @memberOf fabric.Object.prototype - * @param {Number} value scaleX value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#scaleY|scaleY} value - * @method getScaleY - * @memberOf fabric.Object.prototype - * @return {Number} scaleY value - */ - - /** - * Sets object's {@link fabric.Object#scaleY|scaleY} value - * @method setScaleY - * @memberOf fabric.Object.prototype - * @param {Number} value scaleY value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#flipX|flipX} value - * @method getFlipX - * @memberOf fabric.Object.prototype - * @return {Boolean} flipX value - */ - - /** - * Sets object's {@link fabric.Object#flipX|flipX} value - * @method setFlipX - * @memberOf fabric.Object.prototype - * @param {Boolean} value flipX value - * @return {fabric.Object} thisArg - * @chainable - */ - - /** - * Retrieves object's {@link fabric.Object#flipY|flipY} value - * @method getFlipY - * @memberOf fabric.Object.prototype - * @return {Boolean} flipY value - */ - - /** - * Sets object's {@link fabric.Object#flipY|flipY} value - * @method setFlipY - * @memberOf fabric.Object.prototype - * @param {Boolean} value flipY value - * @return {fabric.Object} thisArg - * @chainable - */ - /** * Type of an object (rect, circle, path, etc.). * Note that this property is meant to be read-only and not meant to be modified. @@ -11959,7 +14504,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Horizontal origin of transformation of an object (one of "left", "right", "center") - * See http://jsfiddle.net/1ow02gea/40/ on how originX/originY affect objects in groups + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups * @type String * @default */ @@ -11967,7 +14512,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Vertical origin of transformation of an object (one of "top", "bottom", "center") - * See http://jsfiddle.net/1ow02gea/40/ on how originX/originY affect objects in groups + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups * @type String * @default */ @@ -12064,6 +14609,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ cornerSize: 13, + /** + * Size of object's controlling corners when touch interaction is detected + * @type Number + * @default + */ + touchCornerSize: 24, + /** * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) * @type Boolean @@ -12097,7 +14649,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type String * @default */ - borderColor: 'rgba(102,153,255,0.75)', + borderColor: 'rgb(178,204,255)', /** * Array specifying dash pattern of an object's borders (hasBorder must be true) @@ -12111,7 +14663,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type String * @default */ - cornerColor: 'rgba(102,153,255,0.5)', + cornerColor: 'rgb(178,204,255)', /** * Color of controlling corners of an object (when it's active and transparentCorners false) @@ -12157,6 +14709,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Color of object's fill + * takes css colors https://www.w3.org/TR/css-color-3/ * @type String * @default */ @@ -12180,6 +14733,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Background color of an object. + * takes css colors https://www.w3.org/TR/css-color-3/ * @type String * @default */ @@ -12195,6 +14749,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * 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 */ @@ -12213,6 +14768,13 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ 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 @@ -12221,7 +14783,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati strokeLineCap: 'butt', /** - * Corner style of an object's stroke (one of "bevil", "round", "miter") + * Corner style of an object's stroke (one of "bevel", "round", "miter") * @type String * @default */ @@ -12232,7 +14794,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type Number * @default */ - strokeMiterLimit: 10, + strokeMiterLimit: 4, /** * Shadow object representing shadow of this shape @@ -12250,23 +14812,20 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Scale factor of object's controlling borders + * bigger number will make a thicker border + * border is 1, so this is basically a border thickness + * since there is no way to change the border itself. * @type Number * @default */ borderScaleFactor: 1, - /** - * Transform matrix (similar to SVG's transform matrix) - * @type Array - */ - transformMatrix: null, - /** * Minimum allowed scale value of an object * @type Number * @default */ - minScaleLimit: 0.01, + minScaleLimit: 0, /** * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection). @@ -12304,20 +14863,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ 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 @@ -12332,13 +14877,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ includeDefaultValues: true, - /** - * Function that determines clipping of an object (context is passed as a first argument) - * Note that context origin is at the object's center point (not left/top corner) - * @type Function - */ - clipTo: null, - /** * When `true`, object horizontal movement is locked * @type Boolean @@ -12374,13 +14912,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ lockScalingY: false, - /** - * When `true`, object non-uniform scaling is locked - * @type Boolean - * @default - */ - lockUniScaling: false, - /** * When `true`, object horizontal skewing is locked * @type Boolean @@ -12403,8 +14934,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati lockScalingFlip: false, /** - * When `true`, object is not exported in SVG or OBJECT/JSON - * since 1.6.3 + * When `true`, object is not exported in OBJECT/JSON + * @since 1.6.3 * @type Boolean * @default */ @@ -12412,8 +14943,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * 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 + * @since 1.7.0 * @type Boolean * @default true */ @@ -12421,7 +14953,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * When `true`, object properties are checked for cache invalidation. In some particular - * situation you may want this to be disabled ( spray brush, very big pathgroups, groups) + * 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 @@ -12442,6 +14974,19 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ 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. + * this Property does not work on Text classes or drawing call that uses strokeText,fillText methods + * 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 @@ -12451,14 +14996,32 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati dirty: true, /** - * 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. - * since 1.7.5 - * @type Boolean - * @default false + * 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 */ - needsItsOwnCache: false, + __corner: 0, + + /** + * Determines if the fill or the stroke is drawn first (one of "fill" or "stroke") + * @type String + * @default + */ + paintFirst: 'fill', + + /** + * When 'down', object is set to active on mousedown/touchstart + * When 'up', object is set to active on mouseup/touchend + * Experimental. Let's see if this breaks anything before supporting officially + * @private + * since 4.4.0 + * @type String + * @default 'down' + */ + activeOn: 'down', /** * List of properties to consider when checking if state @@ -12468,32 +15031,69 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ stateProperties: ( 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + - 'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' + - 'angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor ' + - 'skewX skewY' + 'stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit ' + + 'angle opacity fill globalCompositeOperation shadow 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 stroke strokeWidth strokeDashArray' + - ' strokeLineCap strokeLineJoin strokeMiterLimit fillRule backgroundColor' + 'fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform' + + ' strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath' ).split(' '), + /** + * List of properties to consider for animating colors. + * @type Array + */ + colorProperties: ( + 'fill stroke backgroundColor' + ).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) { - options = options || { }; if (options) { this.setOptions(options); } - if (this.objectCaching) { - this._createCacheCanvas(); - } }, /** @@ -12502,34 +15102,85 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ _createCacheCanvas: function() { this._cacheProperties = {}; - this._cacheCanvas = fabric.document.createElement('canvas'); + 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 zoom = this.canvas && this.canvas.getZoom() || 1, - objectScale = this.getObjectScaling(), - dim = this._getNonTransformedDimensions(), - retina = this.canvas && this.canvas._isRetinaScaling() ? fabric.devicePixelRatio : 1, - zoomX = objectScale.scaleX * zoom * retina, - zoomY = objectScale.scaleY * zoom * retina, - width = dim.x * zoomX, - height = dim.y * zoomY; + 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 { - width: width + 2, - height: height + 2, - zoomX: zoomX, - zoomY: zoomY + // 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 }; }, @@ -12540,23 +15191,58 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Boolean} true if the canvas has been resized */ _updateCacheCanvas: function() { - if (this.noScaleCache && this.canvas && this.canvas._currentTransform) { - var action = this.canvas._currentTransform.action; - if (action.slice(0, 5) === 'scale') { + 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 dims = this._getCacheCanvasDimensions(), - width = dims.width, height = dims.height, - zoomX = dims.zoomX, zoomY = dims.zoomY; - - if (width !== this.cacheWidth || height !== this.cacheHeight) { - this._cacheCanvas.width = Math.ceil(width); - this._cacheCanvas.height = Math.ceil(height); - this._cacheContext.translate(width / 2, height / 2); - this._cacheContext.scale(zoomX, zoomY); + 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 (this instanceof fabric.Text && this.path) { + shouldRedraw = true; + shouldResizeCanvas = true; + additionalWidth += this.getHeightOfLine(0) * this.zoomX; + additionalHeight += this.getHeightOfLine(0) * this.zoomY; + } + 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; @@ -12572,7 +15258,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati 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'); }, @@ -12580,21 +15265,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Transforms context when rendering an object * @param {CanvasRenderingContext2D} ctx Context - * @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node */ - transform: function(ctx, fromLeft) { - if (this.group && !this.group._transformDone && this.group === this.canvas._activeGroup) { - this.group.transform(ctx); - } - var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint(); - ctx.translate(center.x, center.y); - this.angle && ctx.rotate(degreesToRadians(this.angle)); - ctx.scale( - this.scaleX * (this.flipX ? -1 : 1), - this.scaleY * (this.flipY ? -1 : 1) - ); - this.skewX && ctx.transform(1, 0, Math.tan(degreesToRadians(this.skewX)), 1, 0, 0); - this.skewY && ctx.transform(1, Math.tan(degreesToRadians(this.skewY)), 0, 1, 0, 0); + transform: function(ctx) { + var needFullTransform = (this.group && !this.group._transformDone) || + (this.group && this.canvas && ctx === this.canvas.contextTop); + var m = this.calcTransformMatrix(!needFullTransform); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); }, /** @@ -12607,6 +15283,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati object = { type: this.type, + version: fabric.version, originX: this.originX, originY: this.originY, left: toFixed(this.left, NUM_FRACTION_DIGITS), @@ -12618,27 +15295,33 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : this.strokeDashArray, strokeLineCap: this.strokeLineCap, + strokeDashOffset: this.strokeDashOffset, strokeLineJoin: this.strokeLineJoin, + strokeUniform: this.strokeUniform, strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), - angle: toFixed(this.getAngle(), 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) + skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS), }; - fabric.util.populateWithProperties(this, object, propertiesToInclude); + if (this.clipPath && !this.clipPath.excludeFromExport) { + 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); } @@ -12664,14 +15347,15 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati 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) { + if (Array.isArray(object[prop]) && Array.isArray(prototype[prop]) + && object[prop].length === 0 && prototype[prop].length === 0) { delete object[prop]; } }); @@ -12692,15 +15376,48 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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; + // if the object is a top level one, on the canvas, we go for simple aritmetic + // otherwise the complex method with angles will return approximations and decimals + // and will likely kill the cache when not needed + // https://github.com/fabricjs/fabric.js/issues/7157 + if (!this.group) { + return { + scaleX: this.scaleX, + scaleY: this.scaleY, + }; + } + // if we are inside a group total zoom calculation is complex, we defer to generic matrices + var options = fabric.util.qrDecompose(this.calcTransformMatrix()); + return { scaleX: Math.abs(options.scaleX), scaleY: Math.abs(options.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 @@ -12708,7 +15425,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {fabric.Object} thisArg */ _set: function(key, value) { - var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'); + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'), + isChanged = this[key] !== value, groupNeedsUpdate = false; if (shouldConstrainValue) { value = this._constrainScale(value); @@ -12730,21 +15448,16 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati this[key] = value; - if (this.cacheProperties.indexOf(key) > -1) { - if (this.group) { + 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); } - this.dirty = true; } - - if (this.group && this.stateProperties.indexOf(key) > -1) { - this.group.set('dirty', true); - } - - if (key === 'width' || key === 'height') { - this.minScaleLimit = Math.min(0.1, 1 / Math.max(this.width, this.height)); - } - return this; }, @@ -12758,22 +15471,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati // implemented by sub-classes, as needed. }, - /** - * Sets sourcePath of an object - * @param {String} value Value to set sourcePath to - * @return {fabric.Object} thisArg - * @chainable - */ - setSourcePath: function(value) { - this.sourcePath = value; - return this; - }, - /** * Retrieves viewportTransform from Object's canvas if possible * @method getViewportTransform * @memberOf fabric.Object.prototype - * @return {Boolean} flipY value // TODO + * @return {Array} */ getViewportTransform: function() { if (this.canvas && this.canvas.viewportTransform) { @@ -12782,64 +15484,135 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati 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 && !this.height && this.strokeWidth === 0) || + !this.visible; + }, + /** * Renders an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} [noTransform] When true, context is not transformed */ - render: function(ctx, noTransform) { + render: function(ctx) { // do not render if width/height are zeros or object is not visible - if ((this.width === 0 && this.height === 0) || !this.visible) { + if (this.isNotVisible()) { return; } if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { return; } ctx.save(); - //setup fill rule for current object this._setupCompositeOperation(ctx); this.drawSelectionBackground(ctx); - if (!noTransform) { - this.transform(ctx); - } + this.transform(ctx); this._setOpacity(ctx); - this._setShadow(ctx); - if (this.transformMatrix) { - ctx.transform.apply(ctx, this.transformMatrix); - } - this.clipTo && fabric.util.clipContext(this, ctx); + this._setShadow(ctx, this); if (this.shouldCache()) { - if (!this._cacheCanvas) { - this._createCacheCanvas(); - } - if (this.isCacheDirty(noTransform)) { - this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); - this.drawObject(this._cacheContext, noTransform); - this.dirty = false; - } + this.renderCache(); this.drawCacheOnCanvas(ctx); } else { - this.drawObject(ctx, noTransform); - if (noTransform && this.objectCaching && this.statefullCache) { + 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._cacheContext) { + this._createCacheCanvas(); + } + if (this.isCacheDirty()) { + this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); + this.drawObject(this._cacheContext, options.forClipping); + this.dirty = false; + } + }, + /** - * Decide if the object should cache or not. + * Remove cacheCanvas and its dimensions from the objects + */ + _removeCacheCanvas: function() { + this._cacheCanvas = null; + this._cacheContext = 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 approximation 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 approximation 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() { - return this.objectCaching && - (!this.group || this.needsItsOwnCache || !this.group.isCaching()); + this.ownCaching = this.needsItsOwnCache() || ( + this.objectCaching && + (!this.group || !this.group.isOnACache()) + ); + return this.ownCaching; }, /** @@ -12848,19 +15621,70 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Boolean} */ willDrawShadow: function() { - return !!this.shadow; + 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 + * @param {fabric.Object} clipPath + */ + drawClipPathOnCache: function(ctx, clipPath) { + ctx.save(); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4 + if (clipPath.inverted) { + ctx.globalCompositeOperation = 'destination-out'; + } + else { + ctx.globalCompositeOperation = 'destination-in'; + } + //ctx.scale(1 / 2, 1 / 2); + if (clipPath.absolutePositioned) { + var m = fabric.util.invertTransform(this.calcTransformMatrix()); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + clipPath.transform(ctx); + ctx.scale(1 / clipPath.zoomX, 1 / clipPath.zoomY); + ctx.drawImage(clipPath._cacheCanvas, -clipPath.cacheTranslationX, -clipPath.cacheTranslationY); + ctx.restore(); }, /** * Execute the drawing operation for an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} [noTransform] When true, context is not transformed */ - drawObject: function(ctx, noTransform) { - this._renderBackground(ctx); - this._setStrokeStyles(ctx); - this._setFillStyles(ctx); - this._render(ctx, noTransform); + 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._render(ctx); + this._drawClipPath(ctx, this.clipPath); + this.fill = originalFill; + this.stroke = originalStroke; + }, + + /** + * Prepare clipPath state and cache and draw it on instance's cache + * @param {CanvasRenderingContext2D} ctx + * @param {fabric.Object} clipPath + */ + _drawClipPath: function (ctx, clipPath) { + if (!clipPath) { return; } + // needed to setup a couple of variables + // path canvas gets overridden with this one. + // TODO find a better solution? + clipPath.canvas = this.canvas; + clipPath.shouldCache(); + clipPath._transformDone = true; + clipPath.renderCache({ forClipping: true }); + this.drawClipPathOnCache(ctx, clipPath); }, /** @@ -12869,7 +15693,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ drawCacheOnCanvas: function(ctx) { ctx.scale(1 / this.zoomX, 1 / this.zoomY); - ctx.drawImage(this._cacheCanvas, -this.cacheWidth / 2, -this.cacheHeight / 2); + ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); }, /** @@ -12878,13 +15702,19 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * on parent canvas. */ isCacheDirty: function(skipCanvas) { - if (!skipCanvas && this._updateCacheCanvas()) { + if (this.isNotVisible()) { + return false; + } + if (this._cacheCanvas && this._cacheContext && !skipCanvas && this._updateCacheCanvas()) { // in this case the context is already cleared. return true; } else { - if (this.dirty || (this.statefullCache && this.hasStateChanged('cacheProperties'))) { - if (!skipCanvas) { + if (this.dirty || + (this.clipPath && this.clipPath.absolutePositioned) || + (this.statefullCache && this.hasStateChanged('cacheProperties')) + ) { + if (this._cacheCanvas && this._cacheContext && !skipCanvas) { var width = this.cacheWidth / this.zoomX; var height = this.cacheHeight / this.zoomY; this._cacheContext.clearRect(-width / 2, -height / 2, width, height); @@ -12896,7 +15726,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Draws a background for the object big as its untrasformed dimensions + * Draws a background for the object big as its untransformed dimensions * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ @@ -12923,82 +15753,111 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @param {CanvasRenderingContext2D} ctx Context to render on */ _setOpacity: function(ctx) { - ctx.globalAlpha *= this.opacity; - }, - - _setStrokeStyles: function(ctx) { - if (this.stroke) { - ctx.lineWidth = this.strokeWidth; - ctx.lineCap = this.strokeLineCap; - ctx.lineJoin = this.strokeLineJoin; - ctx.miterLimit = this.strokeMiterLimit; - ctx.strokeStyle = this.stroke.toLive - ? this.stroke.toLive(ctx, this) - : this.stroke; + if (this.group && !this.group._transformDone) { + ctx.globalAlpha = this.getObjectOpacity(); + } + else { + ctx.globalAlpha *= this.opacity; } }, - _setFillStyles: function(ctx) { - if (this.fill) { - ctx.fillStyle = this.fill.toLive - ? this.fill.toLive(ctx, this) - : this.fill; + _setStrokeStyles: function(ctx, decl) { + var stroke = decl.stroke; + if (stroke) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = decl.strokeLineCap; + ctx.lineDashOffset = decl.strokeDashOffset; + ctx.lineJoin = decl.strokeLineJoin; + ctx.miterLimit = decl.strokeMiterLimit; + if (stroke.toLive) { + if (stroke.gradientUnits === 'percentage' || stroke.gradientTransform || stroke.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + this._applyPatternForTransformedGradient(ctx, stroke); + } + else { + // is a simple gradient or pattern + ctx.strokeStyle = stroke.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, stroke); + } + } + else { + // is a color + ctx.strokeStyle = decl.stroke; + } } }, + _setFillStyles: function(ctx, decl) { + var fill = decl.fill; + if (fill) { + if (fill.toLive) { + ctx.fillStyle = fill.toLive(ctx, this); + this._applyPatternGradientTransform(ctx, decl.fill); + } + else { + ctx.fillStyle = 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 browaser does not support lineDash */ - _setLineDash: function(ctx, dashArray, alternative) { - if (!dashArray) { + _setLineDash: function(ctx, dashArray) { + if (!dashArray || dashArray.length === 0) { 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); - } + ctx.setLineDash(dashArray); }, /** * Renders controls and borders for the object + * the context here is not transformed * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} [styleOverride] properties to override the object style */ - _renderControls: function(ctx) { - if (!this.active || (this.group && this.group !== this.canvas.getActiveGroup())) { - return; - } - + _renderControls: function(ctx, styleOverride) { var vpt = this.getViewportTransform(), matrix = this.calcTransformMatrix(), - options; + 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 (this.group && this.group === this.canvas.getActiveGroup()) { - ctx.rotate(degreesToRadians(options.angle)); - this.drawBordersInGroup(ctx, options); + if (this.flipX) { + options.angle -= 180; + } + ctx.rotate(degreesToRadians(this.group ? options.angle : this.angle)); + if (styleOverride.forActiveSelection || this.group) { + drawBorders && this.drawBordersInGroup(ctx, options, styleOverride); } else { - ctx.rotate(degreesToRadians(this.angle)); - this.drawBorders(ctx); + drawBorders && this.drawBorders(ctx, styleOverride); } - this.drawControls(ctx); + drawControls && this.drawControls(ctx, styleOverride); ctx.restore(); }, @@ -13011,17 +15870,24 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return; } - var multX = (this.canvas && this.canvas.viewportTransform[0]) || 1, - multY = (this.canvas && this.canvas.viewportTransform[3]) || 1, - scaling = this.getObjectScaling(); - if (this.canvas && this.canvas._isRetinaScaling()) { + 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 = this.shadow.color; - ctx.shadowBlur = this.shadow.blur * (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; - ctx.shadowOffsetX = this.shadow.offsetX * multX * scaling.scaleX; - ctx.shadowOffsetY = this.shadow.offsetY * multY * scaling.scaleY; + 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; }, /** @@ -13041,18 +15907,53 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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.toLive) { - return; - } - var transform = filler.gradientTransform || filler.patternTransform; - if (transform) { - ctx.transform.apply(ctx, transform); + 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; - ctx.translate(offsetX, offsetY); + + 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 */) { + }, /** @@ -13065,7 +15966,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati } ctx.save(); - this._applyPatternGradientTransform(ctx, this.fill); + this._setFillStyles(ctx, this); if (this.fillRule === 'evenodd') { ctx.fill('evenodd'); } @@ -13089,43 +15990,238 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati } ctx.save(); - this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke); - this._applyPatternGradientTransform(ctx, this.stroke); + if (this.strokeUniform && this.group) { + var scaling = this.getObjectScaling(); + ctx.scale(1 / scaling.scaleX, 1 / scaling.scaleY); + } + else if (this.strokeUniform) { + ctx.scale(1 / this.scaleX, 1 / this.scaleY); + } + this._setLineDash(ctx, this.strokeDashArray); + this._setStrokeStyles(ctx, this); ctx.stroke(); ctx.restore(); }, /** - * Clones an instance, some objects are async, so using callback method will work for every object. - * Using the direct return does not work for images and groups. + * 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 - * @return {fabric.Object} clone of an instance */ clone: function(callback, propertiesToInclude) { + var objectForm = this.toObject(propertiesToInclude); if (this.constructor.fromObject) { - return this.constructor.fromObject(this.toObject(propertiesToInclude), callback); + this.constructor.fromObject(objectForm, callback); + } + else { + fabric.Object._fromObject('Object', objectForm, callback); } - return new fabric.Object(this.toObject(propertiesToInclude)); }, /** * Creates an instance of fabric.Image out of an object + * makes use of toCanvasElement. + * Once this method was based on toDataUrl and loadImage, so it also had a quality + * and format option. toCanvasElement is faster and produce no loss of quality. + * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. + * toCanvasElement and then toBlob from the obtained canvas is also a good option. + * This method is sync now, but still support the callback because we did not want to break. + * When fabricJS 5.0 will be planned, this will probably be changed to not have a callback. * @param {Function} callback callback, invoked with an instance as a first argument * @param {Object} [options] for clone as image, passed to toDataURL - * @param {Boolean} [options.enableRetinaScaling] enable retina scaling for the cloned image + * @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 dataUrl = this.toDataURL(options); - fabric.util.loadImage(dataUrl, function(img) { - if (callback) { - callback(new fabric.Image(img)); - } - }); + 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 {HTMLCanvasElement} Returns DOM element with the fabric.Object + */ + toCanvasElement: function(options) { + options || (options = { }); + + var utils = fabric.util, origParams = utils.saveObjectTransform(this), + originalGroup = this.group, + originalShadow = this.shadow, abs = Math.abs, + multiplier = (options.multiplier || 1) * (options.enableRetinaScaling ? fabric.devicePixelRatio : 1); + delete this.group; + 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, + width, height; + + if (shadow) { + shadowBlur = shadow.blur; + if (shadow.nonScaling) { + scaling = { scaleX: 1, scaleY: 1 }; + } + else { + scaling = this.getObjectScaling(); + } + // consider non scaling shadow. + shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * (abs(scaling.scaleX)); + shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * (abs(scaling.scaleY)); + } + width = boundingRect.width + shadowOffset.x; + height = boundingRect.height + shadowOffset.y; + // if the current width/height is not an integer + // we need to make it so. + el.width = Math.ceil(width); + el.height = Math.ceil(height); + 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.set('canvas', originalCanvas); + if (originalGroup) { + this.group = originalGroup; + } + 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 @@ -13136,48 +16232,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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.enableRetina] Enable retina scaling for clone image. Introduce in 1.6.4 + * @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 = { }); - - var el = fabric.util.createCanvasElement(), - boundingRect = this.getBoundingRect(); - - el.width = boundingRect.width; - el.height = boundingRect.height; - fabric.util.wrapElement(el, 'div'); - var canvas = new fabric.StaticCanvas(el, { enableRetinaScaling: options.enableRetinaScaling }); - // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 - if (options.format === 'jpg') { - options.format = 'jpeg'; - } - - if (options.format === 'jpeg') { - canvas.backgroundColor = '#fff'; - } - - var origParams = { - active: this.get('active'), - left: this.getLeft(), - top: this.getTop() - }; - - this.set('active', false); - this.setPositionByOrigin(new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2), 'center', 'center'); - - var originalCanvas = this.canvas; - canvas.add(this); - var data = canvas.toDataURL(options); - - this.set(origParams).setCoords(); - this.canvas = originalCanvas; - - canvas.dispose(); - canvas = null; - - return data; + return fabric.util.toDataURL(this.toCanvasElement(options), options.format || 'png', options.quality || 1); }, /** @@ -13186,7 +16248,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Boolean} */ isType: function(type) { - return this.type === type; + return arguments.length > 1 ? Array.from(arguments).includes(this.type) : this.type === type; }, /** @@ -13208,144 +16270,12 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /** - * Sets gradient (fill or stroke) of an object - * 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] transforMatrix for gradient - * @return {fabric.Object} thisArg - * @chainable - * @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 - }; - - 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 - * @return {fabric.Object} thisArg - * @chainable - * @see {@link http://jsfiddle.net/fabricjs/QT3pa/|jsFiddle demo} - * @example Set pattern - * fabric.util.loadImage('http://fabricjs.com/assets/escheresque_ste.png', function(img) { - * object.setPatternFill({ - * source: img, - * repeat: 'repeat' - * }); - * canvas.renderAll(); - * }); - */ - setPatternFill: function(options) { - return this.set('fill', new fabric.Pattern(options)); - }, - - /** - * 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 + * Sets "angle" of an instance with centered rotation * @param {Number} angle Angle value (in degrees) * @return {fabric.Object} thisArg * @chainable */ - setAngle: function(angle) { + rotate: function(angle) { var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; if (shouldCenterOrigin) { @@ -13427,16 +16357,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this; }, - /** - * Removes object from canvas to which it was added last - * @return {fabric.Object} thisArg - * @chainable - */ - remove: function() { - this.canvas && this.canvas.remove(this); - return this; - }, - /** * Returns coordinates of a pointer relative to an object * @param {Event} e Event to operate upon @@ -13459,24 +16379,27 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati /** * Sets canvas globalCompositeOperation for specific object - * custom composition operation for the particular object can be specifed using globalCompositeOperation property + * 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; } + }, + + /** + * cancel instance's running animations + * override if necessary to dispose artifacts such as `clipPath` + */ + dispose: function () { + if (fabric.runningAnimations) { + fabric.runningAnimations.cancelByTarget(this); + } } }); - fabric.util.createAccessors(fabric.Object); - - /** - * Alias for {@link fabric.Object.prototype.setAngle} - * @alias rotate -> setAngle - * @memberOf fabric.Object - */ - fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; + fabric.util.createAccessors && fabric.util.createAccessors(fabric.Object); extend(fabric.Object.prototype, fabric.Observable); @@ -13490,26 +16413,30 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ fabric.Object.NUM_FRACTION_DIGITS = 2; - fabric.Object._fromObject = function(className, object, callback, forceAsync, extraParam) { + /** + * Defines which properties should be enlivened from the object passed to {@link fabric.Object._fromObject} + * @static + * @memberOf fabric.Object + * @constant + * @type string[] + */ + fabric.Object.ENLIVEN_PROPS = ['clipPath']; + + fabric.Object._fromObject = function(className, object, callback, extraParam) { var klass = fabric[className]; object = clone(object, true); - if (forceAsync) { - 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.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.enlivenObjectEnlivables(object, object, function () { var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); callback && callback(instance); }); - } - else { - var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); - callback && callback(instance); - return instance; - } + }); }; /** @@ -13519,7 +16446,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @type Number */ fabric.Object.__uid = 0; - })(typeof exports !== 'undefined' ? exports : this); @@ -13540,7 +16466,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** - * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * 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' @@ -13697,7 +16623,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati 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); }, @@ -13707,9 +16632,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ adjustPosition: function(to) { var angle = degreesToRadians(this.angle), - hypotFull = this.getWidth(), - xFull = Math.cos(angle) * hypotFull, - yFull = Math.sin(angle) * hypotFull, + 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. @@ -13776,13 +16701,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati _getLeftTopCoords: function() { return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); }, - - /** - * Callback; invoked right before object is about to go from active to inactive - */ - onDeselect: function() { - /* NOOP */ - } }); })(); @@ -13790,7 +16708,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati (function() { - function getCoords(coords) { + function arrayFromCoords(coords) { return [ new fabric.Point(coords.tl.x, coords.tl.y), new fabric.Point(coords.tr.x, coords.tr.y), @@ -13799,21 +16717,21 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati ]; } - var degreesToRadians = fabric.util.degreesToRadians, - multiplyMatrices = fabric.util.multiplyTransformMatrices; + var util = fabric.util, + degreesToRadians = util.degreesToRadians, + multiplyMatrices = util.multiplyTransformMatrices, + transformPoint = util.transformPoint; - fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + 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. + * properties are depending on control keys and padding 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; + * The coordinates depends from the controls positionHandler and are used + * to draw and locate controls * @memberOf fabric.Object.prototype */ oCoords: null, @@ -13824,23 +16742,62 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * 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 usefull to understand where an object is. They get updated + * 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); + * You can calculate them without updating with @method calcACoords(); * @memberOf fabric.Object.prototype */ aCoords: null, + /** + * Describe object's corner position in canvas element coordinates. + * includes padding. Used of object detection. + * set and refreshed with setCoords. + * @memberOf fabric.Object.prototype + */ + lineCoords: null, + + /** + * storage for object transform matrix + */ + ownMatrixCache: null, + + /** + * storage for object full transform matrix + */ + matrixCache: null, + + /** + * custom controls interface + * controls are added by default_controls.js + */ + controls: { }, + /** * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * @param {Boolean} absolute will return aCoords if true or lineCoords + * @return {Object} {tl, tr, br, bl} points + */ + _getCoords: function(absolute, calculate) { + if (calculate) { + return (absolute ? this.calcACoords() : this.calcLineCoords()); + } + if (!this.aCoords || !this.lineCoords) { + this.setCoords(true); + } + return (absolute ? this.aCoords : this.lineCoords); + }, + + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * The coords are returned in an array. + * @return {Array} [tl, tr, br, bl] of points */ getCoords: function(absolute, calculate) { - if (!this.oCoords) { - this.setCoords(); - } - var coords = absolute ? this.aCoords : this.oCoords; - return getCoords(calculate ? this.calcCoords(absolute) : coords); + return arrayFromCoords(this._getCoords(absolute, calculate)); }, /** @@ -13870,9 +16827,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ intersectsWithObject: function(other, absolute, calculate) { var intersection = fabric.Intersection.intersectPolygonPolygon( - this.getCoords(absolute, calculate), - other.getCoords(absolute, calculate) - ); + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) + ); return intersection.status === 'Intersection' || other.isContainedWithinObject(this, absolute, calculate) @@ -13888,9 +16845,8 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ 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 - ); + otherCoords = absolute ? other.aCoords : other.lineCoords, + i = 0, lines = other._getImageLines(otherCoords); for (; i < 4; i++) { if (!other.containsPoint(points[i], lines)) { return false; @@ -13927,52 +16883,85 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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 - ), + var coords = this._getCoords(absolute, calculate), + lines = lines || this._getImageLines(coords), 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 appear on screen - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object is fully contained within canvas + * 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)) { + var points = this.getCoords(true, calculate); + // if some point is on screen, the object is on screen. + if (points.some(function(point) { + return point.x <= pointBR.x && point.x >= pointTL.x && + point.y <= pointBR.y && point.y >= pointTL.y; + })) { return true; } - // worst case scenario the object is so big that contanins the screen + // 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)) { + 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; + } + var allPointsAreOutside = this.getCoords(true, calculate).every(function(point) { + return (point.x >= pointBR.x || point.x <= pointTL.x) && + (point.y >= pointBR.y || point.y <= pointTL.y); + }); + return allPointsAreOutside && 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 { + + var lines = { topline: { o: oCoords.tl, d: oCoords.tr @@ -13990,6 +16979,23 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati d: oCoords.tl } }; + + // // debugging + // if (this.canvas.contextTop) { + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + // } + + return lines; }, /** @@ -13999,7 +17005,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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. + // remove yi, not used but left code here just in case. _findCrossPoints: function(point, lines) { var b1, b2, a1, a2, xi, // yi, xcount = 0, @@ -14042,50 +17048,33 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return xcount; }, - /** - * Returns width of an object's bounding rectangle - * @deprecated since 1.0.4 - * @return {Number} width value - */ - getBoundingRectWidth: function() { - return this.getBoundingRect().width; - }, - - /** - * Returns height of an object's bounding rectangle - * @deprecated since 1.0.4 - * @return {Number} height value - */ - getBoundingRectHeight: function() { - return this.getBoundingRect().height; - }, - /** * Returns coordinates of object's bounding rectangle (left, top, width, height) - * the box is intented as aligned to axis of canvas. + * 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 + * @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); + return util.makeBoundingBoxFromPoints(coords); }, /** - * Returns width of an object bounding box counting transformations + * Returns width of an object's bounding box counting transformations + * before 2.0 it was named getWidth(); * @return {Number} width value */ - getWidth: function() { + getScaledWidth: function() { return this._getTransformedDimensions().x; }, /** * Returns height of an object bounding box counting transformations - * to be renamed in 2.0 + * before 2.0 it was named getHeight(); * @return {Number} height value */ - getHeight: function() { + getScaledHeight: function() { return this._getTransformedDimensions().y; }, @@ -14104,6 +17093,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati return this.minScaleLimit; } } + else if (value === 0) { + return 0.0001; + } return value; }, @@ -14114,122 +17106,128 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @chainable */ scale: function(value) { - value = this._constrainScale(value); - - if (value < 0) { - this.flipX = !this.flipX; - this.flipY = !this.flipY; - value *= -1; - } - - this.scaleX = value; - this.scaleY = 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) { + scaleToWidth: function(value, absolute) { // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRect().width / this.getWidth(); + 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) { + scaleToHeight: function(value, absolute) { // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRect().height / this.getHeight(); + var boundingRectFactor = this.getBoundingRect(absolute).height / this.getScaledHeight(); return this.scale(value / this.height / boundingRectFactor); }, - /** - * Calculate and returns the .coords of an object. - * @return {Object} Object with tl, tr, br, bl .... - * @chainable - */ - calcCoords: function(absolute) { - var theta = degreesToRadians(this.angle), - vpt = this.getViewportTransform(), - dim = absolute ? this._getTransformedDimensions() : this._calculateCurrentDimensions(), - currentWidth = dim.x, currentHeight = dim.y, - sinTh = Math.sin(theta), - cosTh = Math.cos(theta), - _angle = currentWidth > 0 ? Math.atan(currentHeight / currentWidth) : 0, - _hypotenuse = (currentWidth / Math.cos(_angle)) / 2, - offsetX = Math.cos(_angle + theta) * _hypotenuse, - offsetY = Math.sin(_angle + theta) * _hypotenuse, - center = this.getCenterPoint(), - // offset added for rotate and scale actions - coords = absolute ? center : fabric.util.transformPoint(center, vpt), - tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY), - tr = new fabric.Point(tl.x + (currentWidth * cosTh), tl.y + (currentWidth * sinTh)), - bl = new fabric.Point(tl.x - (currentHeight * sinTh), tl.y + (currentHeight * cosTh)), - br = new fabric.Point(coords.x + offsetX, coords.y + offsetY); - if (!absolute) { - 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 + sinTh * this.rotatingPointOffset, mt.y - cosTh * this.rotatingPointOffset); - } + calcLineCoords: function() { + var vpt = this.getViewportTransform(), + padding = this.padding, angle = degreesToRadians(this.angle), + cos = util.cos(angle), sin = util.sin(angle), + cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP, + cosPMinusSinP = cosP - sinP, aCoords = this.calcACoords(); - // debugging - - /* setTimeout(function() { - 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, + var lineCoords = { + tl: transformPoint(aCoords.tl, vpt), + tr: transformPoint(aCoords.tr, vpt), + bl: transformPoint(aCoords.bl, vpt), + br: transformPoint(aCoords.br, vpt), }; - if (!absolute) { - // middle - coords.ml = ml; - coords.mt = mt; - coords.mr = mr; - coords.mb = mb; - // rotating point - coords.mtr = mtr; + + if (padding) { + lineCoords.tl.x -= cosPMinusSinP; + lineCoords.tl.y -= cosPSinP; + lineCoords.tr.x += cosPSinP; + lineCoords.tr.y -= cosPMinusSinP; + lineCoords.bl.x -= cosPSinP; + lineCoords.bl.y += cosPMinusSinP; + lineCoords.br.x += cosPMinusSinP; + lineCoords.br.y += cosPSinP; } + + return lineCoords; + }, + + calcOCoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + vpt = this.getViewportTransform(), + startMatrix = multiplyMatrices(vpt, translateMatrix), + finalMatrix = multiplyMatrices(startMatrix, rotateMatrix), + finalMatrix = multiplyMatrices(finalMatrix, [1 / vpt[0], 0, 0, 1 / vpt[3], 0, 0]), + dim = this._calculateCurrentDimensions(), + coords = {}; + this.forEachControl(function(control, key, fabricObject) { + coords[key] = control.positionHandler(dim, finalMatrix, fabricObject); + }); + + // debug code + // var canvas = this.canvas; + // setTimeout(function() { + // canvas.contextTop.clearRect(0, 0, 700, 700); + // canvas.contextTop.fillStyle = 'green'; + // Object.keys(coords).forEach(function(key) { + // var control = coords[key]; + // canvas.contextTop.fillRect(control.x, control.y, 3, 3); + // }); + // }, 50); return coords; }, + calcACoords: function() { + var rotateMatrix = this._calcRotateMatrix(), + translateMatrix = this._calcTranslateMatrix(), + finalMatrix = multiplyMatrices(translateMatrix, rotateMatrix), + dim = this._getTransformedDimensions(), + w = dim.x / 2, h = dim.y / 2; + return { + // corners + 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) + }; + }, + /** - * Sets corner position coordinates based on current angle, width and height - * See https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords - * @param {Boolean} [ignoreZoom] set oCoords with or without the viewport transform. - * @param {Boolean} [skipAbsolute] skip calculation of aCoords, usefull in setViewportTransform + * Sets corner and controls position coordinates based on current angle, width and height, left and top. + * oCoords are used to find the corners + * aCoords are used to quickly find an object on the canvas + * lineCoords are used to quickly find object during pointer events. + * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} + * + * @param {Boolean} [skipCorners] skip calculation of oCoords. * @return {fabric.Object} thisArg * @chainable */ - setCoords: function(ignoreZoom, skipAbsolute) { - this.oCoords = this.calcCoords(ignoreZoom); - if (!skipAbsolute) { - this.aCoords = this.calcCoords(true); + setCoords: function(skipCorners) { + this.aCoords = this.calcACoords(); + // in case we are in a group, for how the inner group target check works, + // lineCoords are exactly aCoords. Since the vpt gets absorbed by the normalized pointer. + this.lineCoords = this.group ? this.aCoords : this.calcLineCoords(); + if (skipCorners) { + return this; } - // set coordinates of the draggable boxes in the corners used to scale/rotate the image - ignoreZoom || (this._setCornerCoords && this._setCornerCoords()); - + this.oCoords = this.calcOCoords(); + this._setCornerCoords && this._setCornerCoords(); return this; }, @@ -14238,43 +17236,77 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @return {Array} rotation matrix for the object */ _calcRotateMatrix: function() { - if (this.angle) { - var theta = degreesToRadians(this.angle), cos = Math.cos(theta), sin = Math.sin(theta); - // trying to keep rounding error small, ugly but it works. - if (cos === 6.123233995736766e-17 || cos === -1.8369701987210297e-16) { - cos = 0; - } - return [cos, sin, -sin, cos, 0, 0]; - } - return fabric.iMatrix.concat(); + return util.calcRotateMatrix(this); }, /** - * calculate trasform Matrix that represent current transformation from - * object properties. - * @param {Boolean} [skipGroup] return transformMatrix for object and not go upward with parents - * @return {Array} matrix Transform Matrix for the object + * 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 + * There are some situation in which this is useful to avoid the fake rotation. + * @return {Array} transform matrix for the object */ calcTransformMatrix: function(skipGroup) { - var center = this.getCenterPoint(), - translateMatrix = [1, 0, 0, 1, center.x, center.y], - rotateMatrix = this._calcRotateMatrix(), - dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true), - matrix = this.group && !skipGroup ? this.group.calcTransformMatrix() : fabric.iMatrix.concat(); - matrix = multiplyMatrices(matrix, translateMatrix); - matrix = multiplyMatrices(matrix, rotateMatrix); - matrix = multiplyMatrices(matrix, dimensionMatrix); + var matrix = this.calcOwnMatrix(); + if (skipGroup || !this.group) { + return matrix; + } + var key = this.transformMatrixKey(skipGroup), cache = this.matrixCache || (this.matrixCache = {}); + if (cache.key === key) { + return cache.value; + } + if (this.group) { + matrix = multiplyMatrices(this.group.calcTransformMatrix(false), matrix); + } + cache.key = key; + cache.value = matrix; return matrix; }, - _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { - var skewMatrixX = [1, 0, Math.tan(degreesToRadians(skewX)), 1], - skewMatrixY = [1, Math.tan(degreesToRadians(skewY)), 0, 1], - scaleX = this.scaleX * (flipping && this.flipX ? -1 : 1), - scaleY = this.scaleY * (flipping && this.flipY ? -1 : 1), - scaleMatrix = [scaleX, 0, 0, scaleY], - m = multiplyMatrices(scaleMatrix, skewMatrixX, true); - return multiplyMatrices(m, skewMatrixY, true); + /** + * 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(), + options = { + angle: this.angle, + translateX: tMatrix[4], + translateY: tMatrix[5], + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + flipX: this.flipX, + flipY: this.flipY, + }; + cache.key = key; + cache.value = util.composeMatrix(options); + return cache.value; }, /* @@ -14291,7 +17323,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati }, /* - * Calculate object bounding boxdimensions from its properties scale, skew. + * 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 @@ -14303,43 +17337,54 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati if (typeof skewY === 'undefined') { skewY = this.skewY; } - var dimensions = this._getNonTransformedDimensions(), - dimX = dimensions.x / 2, dimY = dimensions.y / 2, - points = [ - { - x: -dimX, - y: -dimY - }, - { - x: dimX, - y: -dimY - }, - { - x: -dimX, - y: dimY - }, - { - x: dimX, - y: dimY - }], - i, transformMatrix = this._calcDimensionsTransformMatrix(skewX, skewY, false), - bbox; - for (i = 0; i < points.length; i++) { - points[i] = fabric.util.transformPoint(points[i], transformMatrix); + var dimensions, dimX, dimY, + noSkew = skewX === 0 && skewY === 0; + + if (this.strokeUniform) { + dimX = this.width; + dimY = this.height; } - bbox = fabric.util.makeBoundingBoxFromPoints(points); - return { x: bbox.width, y: bbox.height }; + else { + dimensions = this._getNonTransformedDimensions(); + dimX = dimensions.x; + dimY = dimensions.y; + } + if (noSkew) { + return this._finalizeDimensions(dimX * this.scaleX, dimY * this.scaleY); + } + var bbox = util.sizeAfterTransform(dimX, dimY, { + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: skewX, + skewY: skewY, + }); + return this._finalizeDimensions(bbox.x, bbox.y); }, /* - * Calculate object dimensions for controls. include padding and canvas zoom + * 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 box, including padding and canvas zoom. + * and active selection * private */ _calculateCurrentDimensions: function() { var vpt = this.getViewportTransform(), dim = this._getTransformedDimensions(), - p = fabric.util.transformPoint(dim, vpt, true); - + p = transformPoint(dim, vpt, true); return p.scalarAdd(2 * this.padding); }, }); @@ -14357,7 +17402,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (this.group) { fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); } - else { + else if (this.canvas) { this.canvas.sendToBack(this); } return this; @@ -14372,7 +17417,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (this.group) { fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); } - else { + else if (this.canvas) { this.canvas.bringToFront(this); } return this; @@ -14388,7 +17433,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (this.group) { fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); } - else { + else if (this.canvas) { this.canvas.sendBackwards(this, intersecting); } return this; @@ -14404,7 +17449,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (this.group) { fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); } - else { + else if (this.canvas) { this.canvas.bringForward(this, intersecting); } return this; @@ -14417,10 +17462,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @chainable */ moveTo: function(index) { - if (this.group) { + if (this.group && this.group.type !== 'activeSelection') { fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); } - else { + else if (this.canvas) { this.canvas.moveTo(this, index); } return this; @@ -14430,7 +17475,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /* _TO_SVG_START_ */ (function() { - function getSvgColorString(prop, value) { if (!value) { return prop + ': none; '; @@ -14450,6 +17494,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } } + var toFixed = fabric.util.toFixed; + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * Returns styles-string for svg-export @@ -14458,9 +17504,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ getSvgStyles: function(skipShadow) { - var fillRule = this.fillRule, + 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', @@ -14475,6 +17522,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 'stroke-width: ', strokeWidth, '; ', 'stroke-dasharray: ', strokeDashArray, '; ', 'stroke-linecap: ', strokeLineCap, '; ', + 'stroke-dashoffset: ', strokeDashOffset, '; ', 'stroke-linejoin: ', strokeLineJoin, '; ', 'stroke-miterlimit: ', strokeMiterLimit, '; ', fill, @@ -14485,6 +17533,55 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ].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) { + return ['overline', 'underline', 'line-through'].filter(function(decoration) { + return style[decoration.replace('-', '')]; + }).join(' '); + }, + /** * Returns filter for svg shadow * @return {String} @@ -14497,85 +17594,139 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Returns id attribute for svg output * @return {String} */ - getSvgId: function() { - return this.id ? 'id="' + this.id + '" ' : ''; - }, - - /** - * Returns transform-string for svg-export - * @return {String} - */ - getSvgTransform: function() { - if (this.group && this.group.type === 'path-group') { - return ''; - } - var toFixed = fabric.util.toFixed, - angle = this.getAngle(), - skewX = (this.getSkewX() % 360), - skewY = (this.getSkewY() % 360), - center = this.getCenterPoint(), - - NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, - - translatePart = this.type === 'path-group' ? '' : 'translate(' + - toFixed(center.x, NUM_FRACTION_DIGITS) + - ' ' + - toFixed(center.y, NUM_FRACTION_DIGITS) + - ')', - - anglePart = angle !== 0 - ? (' rotate(' + toFixed(angle, NUM_FRACTION_DIGITS) + ')') - : '', - - scalePart = (this.scaleX === 1 && this.scaleY === 1) - ? '' : - (' scale(' + - toFixed(this.scaleX, NUM_FRACTION_DIGITS) + - ' ' + - toFixed(this.scaleY, NUM_FRACTION_DIGITS) + - ')'), - - skewXPart = skewX !== 0 ? ' skewX(' + toFixed(skewX, NUM_FRACTION_DIGITS) + ')' : '', - - skewYPart = skewY !== 0 ? ' skewY(' + toFixed(skewY, NUM_FRACTION_DIGITS) + ')' : '', - - addTranslateX = this.type === 'path-group' ? this.width : 0, - - flipXPart = this.flipX ? ' matrix(-1 0 0 1 ' + addTranslateX + ' 0) ' : '', - - addTranslateY = this.type === 'path-group' ? this.height : 0, - - flipYPart = this.flipY ? ' matrix(1 0 0 -1 0 ' + addTranslateY + ')' : ''; - + getSvgCommons: function() { return [ - translatePart, anglePart, scalePart, flipXPart, flipYPart, skewXPart, skewYPart + this.id ? 'id="' + this.id + '" ' : '', + this.clipPath ? 'clip-path="url(#' + this.clipPath.clipPathId + ')" ' : '', ].join(''); }, /** - * Returns transform-string for svg-export from the transform matrix of single elements + * Returns transform-string for svg-export + * @param {Boolean} use the full transform or the single object one. * @return {String} */ - getSvgTransformMatrix: function() { - return this.transformMatrix ? ' matrix(' + this.transformMatrix.join(' ') + ') ' : ''; + getSvgTransform: function(full, additionalTransform) { + var transform = full ? this.calcTransformMatrix() : this.calcOwnMatrix(), + svgTransform = 'transform="' + fabric.util.matrixToSVG(transform); + return svgTransform + + (additionalTransform || '') + '" '; + }, + + _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 */ - _createBaseSVGMarkup: function() { - var markup = []; + _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(''); + }, - if (this.fill && this.fill.toLive) { - markup.push(this.fill.toSVG(this, false)); + /** + * @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 (this.stroke && this.stroke.toLive) { - markup.push(this.stroke.toSVG(this, false)); + if (absoluteClipPath) { + markup.push( + '\n' + ); } - if (this.shadow) { - markup.push(this.shadow.toSVG(this)); + 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)); } - return markup; + 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 + '" ' : ''; } }); })(); @@ -14595,39 +17746,48 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot props.forEach(function(prop) { tmpObj[prop] = origin[prop]; }); + extend(origin[destination], tmpObj, deep); } function _isEqual(origValue, currentValue, firstPass) { - if (!fabric.isLikelyNode && origValue instanceof Element) { - // avoid checking deep html elements - return origValue === currentValue; + if (origValue === currentValue) { + // if the objects are identical, return + return true; } - else if (origValue instanceof Array) { - if (origValue.length !== currentValue.length) { + 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 (origValue[i] !== currentValue[i]) { + if (!_isEqual(origValue[i], currentValue[i])) { return false; } } return true; } else if (origValue && typeof origValue === 'object') { - if (!firstPass && Object.keys(origValue).length !== Object.keys(currentValue).length) { + var keys = Object.keys(origValue), key; + if (!currentValue || + typeof currentValue !== 'object' || + (!firstPass && keys.length !== Object.keys(currentValue).length) + ) { return false; } - for (var key in origValue) { + 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 + // we do not want to compare those. + if (key === 'canvas' || key === 'group') { + continue; + } if (!_isEqual(origValue[key], currentValue[key])) { return false; } } return true; } - else { - return origValue === currentValue; - } } @@ -14640,11 +17800,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ hasStateChanged: function(propertySet) { propertySet = propertySet || originalSet; - propertySet = '_' + propertySet; - if (!Object.keys(this[propertySet]).length) { + var dashedPropertySet = '_' + propertySet; + if (Object.keys(this[dashedPropertySet]).length < this[propertySet].length) { return true; } - return !_isEqual(this[propertySet], this, true); + return !_isEqual(this[dashedPropertySet], this, true); }, /** @@ -14684,64 +17844,50 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot (function() { - var degreesToRadians = fabric.util.degreesToRadians, - /* eslint-disable camelcase */ - isVML = function() { return typeof G_vmlCanvasManager !== 'undefined'; }; - /* eslint-enable camelcase */ + 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) { - if (!this.hasControls || !this.active) { + _findTargetCorner: function(pointer, forTouch) { + // 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; + lines, keys = Object.keys(this.oCoords), + j = keys.length - 1, i; this.__corner = 0; - for (var i in this.oCoords) { + // cycle in reverse order so we pick first the one on top + for (; j >= 0; j--) { + i = keys[j]; 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); + lines = this._getImageLines(forTouch ? this.oCoords[i].touchCorner : this.oCoords[i].corner); + // // debugging + // + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.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) { @@ -14752,42 +17898,33 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return false; }, + /** + * Calls a function for each control. The function gets called, + * with the control, the object that is calling the iterator and the control's key + * @param {Function} fn function to iterate over the controls over + */ + forEachControl: function(fn) { + for (var i in this.controls) { + fn(this.controls[i], i, this); + }; + }, + /** * Sets the coordinates of the draggable boxes in the corners of * the image used to scale/rotate it. + * note: if we would switch to ROUND corner area, all of this would disappear. + * everything would resolve to a single point and a pythagorean theorem for the distance * @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 * Math.cos(newTheta), - sinHalfOffset = cornerHypotenuse * Math.sin(newTheta), - x, y; + var coords = this.oCoords; - 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 - } - }; + for (var control in coords) { + var controlObject = this.controls[control]; + coords[control].corner = controlObject.calcCornerCoords( + this.angle, this.cornerSize, coords[control].x, coords[control].y, false); + coords[control].touchCorner = controlObject.calcCornerCoords( + this.angle, this.touchCornerSize, coords[control].x, coords[control].y, true); } }, @@ -14801,8 +17938,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @chainable */ drawSelectionBackground: function(ctx) { - if (!this.selectionBackgroundColor || this.group || !this.active || - (this.canvas && !this.canvas.interactive)) { + if (!this.selectionBackgroundColor || + (this.canvas && !this.canvas.interactive) || + (this.canvas && this.canvas._activeObject !== this) + ) { return this; } ctx.save(); @@ -14822,22 +17961,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * 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) { - if (!this.hasBorders) { - return this; - } - + drawBorders: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; var wh = this._calculateCurrentDimensions(), - strokeWidth = 1 / this.borderScaleFactor, + strokeWidth = this.borderScaleFactor, width = wh.x + strokeWidth, - height = wh.y + strokeWidth; + height = wh.y + strokeWidth, + hasControls = typeof styleOverride.hasControls !== 'undefined' ? + styleOverride.hasControls : this.hasControls, + shouldStroke = false; ctx.save(); - ctx.strokeStyle = this.borderColor; - this._setLineDash(ctx, this.borderDashArray, null); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); ctx.strokeRect( -width / 2, @@ -14846,17 +17986,25 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot height ); - if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { - - var rotateHeight = -height / 2; - + if (hasControls) { ctx.beginPath(); - ctx.moveTo(0, rotateHeight); - ctx.lineTo(0, rotateHeight - this.rotatingPointOffset); - ctx.closePath(); - ctx.stroke(); + this.forEachControl(function(control, key, fabricObject) { + // in this moment, the ctx is centered on the object. + // width and height of the above function are the size of the bbox. + if (control.withConnection && control.getVisibility(fabricObject, key)) { + // reset movement for each control + shouldStroke = true; + ctx.moveTo(control.x * width, control.y * height); + ctx.lineTo( + control.x * width + control.offsetX, + control.y * height + control.offsetY + ); + } + }); + if (shouldStroke) { + ctx.stroke(); + } } - ctx.restore(); return this; }, @@ -14867,25 +18015,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * 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) { - if (!this.hasBorders) { - return this; - } - - var p = this._getNonTransformedDimensions(), - matrix = fabric.util.customTransformMatrix(options.scaleX, options.scaleY, options.skewX), - wh = fabric.util.transformPoint(p, matrix), - strokeWidth = 1 / this.borderScaleFactor, - width = wh.x + strokeWidth, - height = wh.y + strokeWidth; - + drawBordersInGroup: function(ctx, options, styleOverride) { + styleOverride = styleOverride || {}; + var bbox = fabric.util.sizeAfterTransform(this.width, this.height, options), + strokeWidth = this.strokeWidth, + strokeUniform = this.strokeUniform, + borderScaleFactor = this.borderScaleFactor, + width = + bbox.x + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleX) + borderScaleFactor, + height = + bbox.y + strokeWidth * (strokeUniform ? this.canvas.getZoom() : options.scaleY) + borderScaleFactor; ctx.save(); - this._setLineDash(ctx, this.borderDashArray, null); - ctx.strokeStyle = this.borderColor; - + this._setLineDash(ctx, styleOverride.borderDashArray || this.borderDashArray); + ctx.strokeStyle = styleOverride.borderColor || this.borderColor; ctx.strokeRect( -width / 2, -height / 2, @@ -14902,128 +18048,64 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * 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) { - if (!this.hasControls) { - return this; - } - - var wh = this._calculateCurrentDimensions(), - width = wh.x, - height = wh.y, - scaleOffset = this.cornerSize, - left = -(width + scaleOffset) / 2, - top = -(height + scaleOffset) / 2, - methodName = this.transparentCorners ? 'stroke' : 'fill'; - + drawControls: function(ctx, styleOverride) { + styleOverride = styleOverride || {}; ctx.save(); - ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + var retinaScaling = this.canvas.getRetinaScaling(), matrix, p; + ctx.setTransform(retinaScaling, 0, 0, retinaScaling, 0, 0); + ctx.strokeStyle = ctx.fillStyle = styleOverride.cornerColor || this.cornerColor; if (!this.transparentCorners) { - ctx.strokeStyle = this.cornerStrokeColor; + ctx.strokeStyle = styleOverride.cornerStrokeColor || this.cornerStrokeColor; } - this._setLineDash(ctx, this.cornerDashArray, null); - - // top-left - this._drawControl('tl', ctx, methodName, - left, - top); - - // top-right - this._drawControl('tr', ctx, methodName, - left + width, - top); - - // bottom-left - this._drawControl('bl', ctx, methodName, - left, - top + height); - - // bottom-right - this._drawControl('br', ctx, methodName, - left + width, - top + height); - - if (!this.get('lockUniScaling')) { - - // middle-top - this._drawControl('mt', ctx, methodName, - left + width / 2, - top); - - // middle-bottom - this._drawControl('mb', ctx, methodName, - left + width / 2, - top + height); - - // middle-right - this._drawControl('mr', ctx, methodName, - left + width, - top + height / 2); - - // middle-left - this._drawControl('ml', ctx, methodName, - left, - top + height / 2); + this._setLineDash(ctx, styleOverride.cornerDashArray || this.cornerDashArray); + this.setCoords(); + if (this.group) { + // fabricJS does not really support drawing controls inside groups, + // this piece of code here helps having at least the control in places. + // If an application needs to show some objects as selected because of some UI state + // can still call Object._renderControls() on any object they desire, independently of groups. + // using no padding, circular controls and hiding the rotating cursor is higly suggested, + matrix = this.group.calcTransformMatrix(); } - - // middle-top-rotate - if (this.hasRotatingPoint) { - this._drawControl('mtr', ctx, methodName, - left + width / 2, - top - this.rotatingPointOffset); - } - + this.forEachControl(function(control, key, fabricObject) { + p = fabricObject.oCoords[key]; + if (control.getVisibility(fabricObject, key)) { + if (matrix) { + p = fabric.util.transformPoint(p, matrix); + } + control.render(ctx, p.x, p.y, styleOverride, fabricObject); + } + }); ctx.restore(); return this; }, - /** - * @private - */ - _drawControl: function(control, ctx, methodName, left, top) { - if (!this.isControlVisible(control)) { - return; - } - var size = this.cornerSize, stroke = !this.transparentCorners && this.cornerStrokeColor; - switch (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: - isVML() || 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'. + * @param {String} controlKey The key 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]; + isControlVisible: function(controlKey) { + return this.controls[controlKey] && this.controls[controlKey].getVisibility(this, controlKey); }, /** * 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 {String} controlKey The key 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; + setControlVisible: function(controlKey, visible) { + if (!this._controlsVisibility) { + this._controlsVisibility = {}; + } + this._controlsVisibility[controlKey] = visible; return this; }, @@ -15051,26 +18133,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return this; }, + /** - * Returns the instance of the control visibility set for this object. - * @private - * @returns {Object} + * 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 */ - _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; + 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. } }); })(); @@ -15091,8 +18173,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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 + * @return {fabric.AnimationContext} context */ fxCenterObjectH: function (object, callbacks) { callbacks = callbacks || { }; @@ -15102,13 +18183,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati onChange = callbacks.onChange || empty, _this = this; - fabric.util.animate({ - startValue: object.get('left'), - endValue: this.getCenter().left, + return fabric.util.animate({ + target: this, + startValue: object.left, + endValue: this.getCenterPoint().x, duration: this.FX_DURATION, onChange: function(value) { object.set('left', value); - _this.renderAll(); + _this.requestRenderAll(); onChange(); }, onComplete: function() { @@ -15116,8 +18198,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati onComplete(); } }); - - return this; }, /** @@ -15126,8 +18206,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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 + * @return {fabric.AnimationContext} context */ fxCenterObjectV: function (object, callbacks) { callbacks = callbacks || { }; @@ -15137,13 +18216,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati onChange = callbacks.onChange || empty, _this = this; - fabric.util.animate({ - startValue: object.get('top'), - endValue: this.getCenter().top, + return fabric.util.animate({ + target: this, + startValue: object.top, + endValue: this.getCenterPoint().y, duration: this.FX_DURATION, onChange: function(value) { object.set('top', value); - _this.renderAll(); + _this.requestRenderAll(); onChange(); }, onComplete: function() { @@ -15151,8 +18231,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati onComplete(); } }); - - return this; }, /** @@ -15161,8 +18239,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * @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 + * @return {fabric.AnimationContext} context */ fxRemove: function (object, callbacks) { callbacks = callbacks || { }; @@ -15172,16 +18249,14 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati onChange = callbacks.onChange || empty, _this = this; - fabric.util.animate({ - startValue: object.get('opacity'), + return fabric.util.animate({ + target: this, + startValue: object.opacity, endValue: 0, duration: this.FX_DURATION, - onStart: function() { - object.set('active', false); - }, onChange: function(value) { object.set('opacity', value); - _this.renderAll(); + _this.requestRenderAll(); onChange(); }, onComplete: function () { @@ -15189,8 +18264,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati onComplete(); } }); - - return this; } }); @@ -15201,7 +18274,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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 + * @return {fabric.AnimationContext | fabric.AnimationContext[]} animation context (or an array if passed multiple properties) * * As object — multiple properties * @@ -15214,22 +18287,22 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * object.animate('left', { duration: ... }); * */ - animate: function() { + animate: function () { if (arguments[0] && typeof arguments[0] === 'object') { - var propsToAnimate = [], prop, skipCallbacks; + var propsToAnimate = [], prop, skipCallbacks, out = []; 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); + out.push(this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks)); } + return out; } else { - this._animate.apply(this, arguments); + return this._animate.apply(this, arguments); } - return this; }, /** @@ -15255,6 +18328,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot propPair = property.split('.'); } + var propIsColor = + _this.colorProperties.indexOf(property) > -1 || + (propPair && _this.colorProperties.indexOf(propPair[1]) > -1); + var currentValue = propPair ? this.get(propPair[0])[propPair[1]] : this.get(property); @@ -15263,23 +18340,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options.from = currentValue; } - if (~to.indexOf('=')) { - to = currentValue + parseFloat(to.replace('=', '')); - } - else { - to = parseFloat(to); + if (!propIsColor) { + if (~to.indexOf('=')) { + to = currentValue + parseFloat(to.replace('=', '')); + } + else { + to = parseFloat(to); + } } - fabric.util.animate({ + var _options = { + target: this, startValue: options.from, endValue: to, byValue: options.by, easing: options.easing, duration: options.duration, - abort: options.abort && function() { - return options.abort.call(_this); + abort: options.abort && function(value, valueProgress, timeProgress) { + return options.abort.call(_this, value, valueProgress, timeProgress); }, - onChange: function(value) { + onChange: function (value, valueProgress, timeProgress) { if (propPair) { _this[propPair[0]][propPair[1]] = value; } @@ -15289,17 +18369,24 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot if (skipCallbacks) { return; } - options.onChange && options.onChange(); + options.onChange && options.onChange(value, valueProgress, timeProgress); }, - onComplete: function() { + onComplete: function (value, valueProgress, timeProgress) { if (skipCallbacks) { return; } _this.setCoords(); - options.onComplete && options.onComplete(); + options.onComplete && options.onComplete(value, valueProgress, timeProgress); } - }); + }; + + if (propIsColor) { + return fabric.util.animateColor(_options.startValue, _options.endValue, _options.duration, _options); + } + else { + return fabric.util.animate(_options); + } } }); @@ -15311,22 +18398,13 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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'); + coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }; if (fabric.Line) { fabric.warn('fabric.Line is already defined'); return; } - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push( - 'x1', - 'x2', - 'y1', - 'y2' - ); - /** * Line class * @class fabric.Line @@ -15370,7 +18448,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ y2: 0, - cacheProperties: cacheProperties, + cacheProperties: fabric.Object.prototype.cacheProperties.concat('x1', 'x2', 'y1', 'y2'), /** * Constructor @@ -15464,30 +18542,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} noTransform */ - _render: function(ctx, noTransform) { + _render: function(ctx) { ctx.beginPath(); - if (noTransform) { - // Line coords are distances from left-top of canvas to origin of line. - // To render line in a path-group, we need to translate them to - // distances from center of path-group to center of line. - var cp = this.getCenterPoint(), - offset = this.strokeWidth / 2; - ctx.translate( - cp.x - (this.strokeLineCap === 'butt' && this.height === 0 ? 0 : offset), - cp.y - (this.strokeLineCap === 'butt' && this.width === 0 ? 0 : offset) - ); - } - 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); - } + var p = this.calcLinePoints(); + ctx.moveTo(p.x1, p.y1); + ctx.lineTo(p.x2, p.y2); ctx.lineWidth = this.strokeWidth; @@ -15501,20 +18563,21 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, /** + * This function is an helper for svg import. it returns the center of the object in the svg + * untransformed coordinates * @private - * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {Object} center point from element coordinates */ - _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(); + _findCenterFromElement: function() { + return { + x: (this.x1 + this.x2) / 2, + y: (this.y1 + this.y2) / 2, + }; }, /** * Returns object representation of an instance - * @methd toObject + * @method toObject * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ @@ -15561,30 +18624,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /* _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 + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance */ - toSVG: function(reviver) { - var markup = this._createBaseSVGMarkup(), - p = { x1: this.x1, x2: this.x2, y1: this.y1, y2: this.y2 }; - - if (!(this.group && this.group.type === 'path-group')) { - p = this.calcLinePoints(); - } - markup.push( - '\n' - ); - - return reviver ? reviver(markup.join('')) : markup.join(''); + _toSVG: function() { + var p = this.calcLinePoints(); + return [ + '\n' + ]; }, /* _TO_SVG_END_ */ }); @@ -15604,9 +18657,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @memberOf fabric.Line * @param {SVGElement} element Element to parse * @param {Object} [options] Options object - * @return {fabric.Line} instance of fabric.Line + * @param {Function} [callback] callback function invoked after parsing */ - fabric.Line.fromElement = function(element, options) { + fabric.Line.fromElement = function(element, callback, options) { options = options || { }; var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES), points = [ @@ -15615,9 +18668,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot parsedAttributes.x2 || 0, parsedAttributes.y2 || 0 ]; - options.originX = 'left'; - options.originY = 'top'; - return new fabric.Line(points, extend(parsedAttributes, options)); + callback(new fabric.Line(points, extend(parsedAttributes, options))); }; /* _FROM_SVG_END_ */ @@ -15627,21 +18678,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @memberOf fabric.Line * @param {Object} object Object to create an instance from * @param {function} [callback] invoked with new instance as first argument - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.Line} instance of fabric.Line */ - fabric.Line.fromObject = function(object, callback, forceAsync) { + 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]; - var line = fabric.Object._fromObject('Line', options, _callback, forceAsync, 'points'); - if (line) { - delete line.points; - } - return line; + fabric.Object._fromObject('Line', options, _callback, 'points'); }; /** @@ -15677,19 +18722,13 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 'use strict'; var fabric = global.fabric || (global.fabric = { }), - pi = Math.PI, - extend = fabric.util.object.extend; + degreesToRadians = fabric.util.degreesToRadians; if (fabric.Circle) { fabric.warn('fabric.Circle is already defined.'); return; } - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push( - 'radius' - ); - /** * Circle class * @class fabric.Circle @@ -15713,30 +18752,22 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot radius: 0, /** - * Start angle of the circle, moving clockwise - * @type Number + * degrees of start of the circle. + * probably will change to degrees in next major version + * @type Number 0 - 359 * @default 0 */ startAngle: 0, /** * End angle of the circle - * @type Number - * @default 2Pi + * probably will change to degrees in next major version + * @type Number 1 - 360 + * @default 360 */ - endAngle: pi * 2, + endAngle: 360, - cacheProperties: cacheProperties, - - /** - * Constructor - * @param {Object} [options] Options object - * @return {fabric.Circle} thisArg - */ - initialize: function(options) { - this.callSuper('initialize', options); - this.set('radius', options && options.radius || 0); - }, + cacheProperties: fabric.Object.prototype.cacheProperties.concat('radius', 'startAngle', 'endAngle'), /** * @private @@ -15764,66 +18795,59 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, /* _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 + * @return {Array} an array of strings with the specific svg representation + * of the instance */ - toSVG: function(reviver) { - var markup = this._createBaseSVGMarkup(), x = 0, y = 0, - angle = (this.endAngle - this.startAngle) % ( 2 * pi); + _toSVG: function() { + var svgString, x = 0, y = 0, + angle = (this.endAngle - this.startAngle) % 360; if (angle === 0) { - if (this.group && this.group.type === 'path-group') { - x = this.left + this.radius; - y = this.top + this.radius; - } - markup.push( - '\n' - ); + svgString = [ + '\n' + ]; } else { - var startX = Math.cos(this.startAngle) * this.radius, - startY = Math.sin(this.startAngle) * this.radius, - endX = Math.cos(this.endAngle) * this.radius, - endY = Math.sin(this.endAngle) * this.radius, - largeFlag = angle > pi ? '1' : '0'; - - markup.push( + var start = degreesToRadians(this.startAngle), + end = degreesToRadians(this.endAngle), + radius = this.radius, + startX = fabric.util.cos(start) * radius, + startY = fabric.util.sin(start) * radius, + endX = fabric.util.cos(end) * radius, + endY = fabric.util.sin(end) * radius, + largeFlag = angle > 180 ? '1' : '0'; + svgString = [ '\n' - ); + '" ', 'COMMON_PARTS', ' />\n' + ]; } - - return reviver ? reviver(markup.join('')) : markup.join(''); + return svgString; }, /* _TO_SVG_END_ */ /** * @private * @param {CanvasRenderingContext2D} ctx context to render on - * @param {Boolean} [noTransform] When true, context is not transformed */ - _render: function(ctx, noTransform) { + _render: function(ctx) { ctx.beginPath(); - ctx.arc(noTransform ? this.left + this.radius : 0, - noTransform ? this.top + this.radius : 0, - this.radius, - this.startAngle, - this.endAngle, false); - this._renderFill(ctx); - this._renderStroke(ctx); + ctx.arc( + 0, + 0, + this.radius, + degreesToRadians(this.startAngle), + degreesToRadians(this.endAngle), + false + ); + this._renderPaintInOrder(ctx); }, /** @@ -15866,27 +18890,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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 - * @return {fabric.Circle} Instance of fabric.Circle */ - fabric.Circle.fromElement = function(element, options) { - options || (options = { }); - + 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.top = parsedAttributes.top || 0; - - var obj = new fabric.Circle(extend(parsedAttributes, options)); - - obj.left -= obj.radius; - obj.top -= obj.radius; - return obj; + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.radius; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.radius; + callback(new fabric.Circle(parsedAttributes)); }; /** @@ -15903,11 +18920,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @memberOf fabric.Circle * @param {Object} object Object to create an instance from * @param {function} [callback] invoked with new instance as first argument - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {Object} Instance of fabric.Circle + * @return {void} */ - fabric.Circle.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Circle', object, callback, forceAsync); + fabric.Circle.fromObject = function(object, callback) { + fabric.Object._fromObject('Circle', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -15941,15 +18957,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot type: 'triangle', /** - * Constructor - * @param {Object} [options] Options object - * @return {Object} thisArg + * Width is set to 100 to compensate the old initialize code that was setting it to 100 + * @type Number + * @default */ - initialize: function(options) { - this.callSuper('initialize', options); - this.set('width', options && options.width || 100) - .set('height', options && options.height || 100); - }, + 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 @@ -15965,51 +18984,28 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.lineTo(widthBy2, heightBy2); ctx.closePath(); - this._renderFill(ctx); - this._renderStroke(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(); + this._renderPaintInOrder(ctx); }, /* _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 + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance */ - toSVG: function(reviver) { - var markup = this._createBaseSVGMarkup(), - widthBy2 = this.width / 2, + _toSVG: function() { + var widthBy2 = this.width / 2, heightBy2 = this.height / 2, points = [ -widthBy2 + ' ' + heightBy2, '0 ' + -heightBy2, widthBy2 + ' ' + heightBy2 - ] - .join(','); - - markup.push( - '' - ); - - return reviver ? reviver(markup.join('')) : markup.join(''); + ].join(','); + return [ + '' + ]; }, /* _TO_SVG_END_ */ }); @@ -16020,11 +19016,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @memberOf fabric.Triangle * @param {Object} object Object to create an instance from * @param {function} [callback] invoked with new instance as first argument - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.Triangle} */ - fabric.Triangle.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Triangle', object, callback, forceAsync); + fabric.Triangle.fromObject = function(object, callback) { + return fabric.Object._fromObject('Triangle', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -16035,20 +19029,13 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 'use strict'; var fabric = global.fabric || (global.fabric = { }), - piBy2 = Math.PI * 2, - extend = fabric.util.object.extend; + piBy2 = Math.PI * 2; if (fabric.Ellipse) { fabric.warn('fabric.Ellipse is already defined.'); return; } - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push( - 'rx', - 'ry' - ); - /** * Ellipse class * @class fabric.Ellipse @@ -16079,7 +19066,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ ry: 0, - cacheProperties: cacheProperties, + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), /** * Constructor @@ -16144,49 +19131,37 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /* _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 + * @return {Array} an array of strings with the specific svg representation + * of the instance */ - toSVG: function(reviver) { - var markup = this._createBaseSVGMarkup(), x = 0, y = 0; - if (this.group && this.group.type === 'path-group') { - x = this.left + this.rx; - y = this.top + this.ry; - } - markup.push( - '\n' - ); - - return reviver ? reviver(markup.join('')) : markup.join(''); + _toSVG: function() { + return [ + '\n' + ]; }, /* _TO_SVG_END_ */ /** * @private * @param {CanvasRenderingContext2D} ctx context to render on - * @param {Boolean} [noTransform] When true, context is not transformed */ - _render: function(ctx, noTransform) { + _render: function(ctx) { ctx.beginPath(); ctx.save(); ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0); ctx.arc( - noTransform ? this.left + this.rx : 0, - noTransform ? (this.top + this.ry) * this.rx / this.ry : 0, + 0, + 0, this.rx, 0, piBy2, false); ctx.restore(); - this._renderFill(ctx); - this._renderStroke(ctx); + this._renderPaintInOrder(ctx); }, }); @@ -16204,22 +19179,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Ellipse * @param {SVGElement} element Element to parse - * @param {Object} [options] Options object + * @param {Function} [callback] Options callback invoked after parsing is finished * @return {fabric.Ellipse} */ - fabric.Ellipse.fromElement = function(element, options) { - options || (options = { }); + fabric.Ellipse.fromElement = function(element, callback) { var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); - parsedAttributes.left = parsedAttributes.left || 0; - parsedAttributes.top = parsedAttributes.top || 0; - - var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); - - ellipse.top -= ellipse.ry; - ellipse.left -= ellipse.rx; - return ellipse; + parsedAttributes.left = (parsedAttributes.left || 0) - parsedAttributes.rx; + parsedAttributes.top = (parsedAttributes.top || 0) - parsedAttributes.ry; + callback(new fabric.Ellipse(parsedAttributes)); }; /* _FROM_SVG_END_ */ @@ -16229,11 +19198,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @memberOf fabric.Ellipse * @param {Object} object Object to create an instance from * @param {function} [callback] invoked with new instance as first argument - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.Ellipse} + * @return {void} */ - fabric.Ellipse.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Ellipse', object, callback, forceAsync); + fabric.Ellipse.fromObject = function(object, callback) { + fabric.Object._fromObject('Ellipse', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -16251,12 +19219,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return; } - var stateProperties = fabric.Object.prototype.stateProperties.concat(); - stateProperties.push('rx', 'ry'); - - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push('rx', 'ry'); - /** * Rectangle class * @class fabric.Rect @@ -16271,7 +19233,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * as well as for history (undo/redo) purposes * @type Array */ - stateProperties: stateProperties, + stateProperties: fabric.Object.prototype.stateProperties.concat('rx', 'ry'), /** * Type of an object @@ -16294,7 +19256,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ ry: 0, - cacheProperties: cacheProperties, + cacheProperties: fabric.Object.prototype.cacheProperties.concat('rx', 'ry'), /** * Constructor @@ -16322,22 +19284,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} noTransform */ - _render: function(ctx, noTransform) { + _render: function(ctx) { - // optimize 1x1 case (used in spray brush) - if (this.width === 1 && this.height === 1) { - ctx.fillRect(-0.5, -0.5, 1, 1); - return; - } + // 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 = noTransform ? this.left : -this.width / 2, - y = noTransform ? this.top : -this.height / 2, + 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; @@ -16359,26 +19317,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.closePath(); - this._renderFill(ctx); - this._renderStroke(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(); + this._renderPaintInOrder(ctx); }, /** @@ -16393,26 +19332,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /* _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 + * @return {Array} an array of strings with the specific svg representation + * of the instance */ - toSVG: function(reviver) { - var markup = this._createBaseSVGMarkup(), x = this.left, y = this.top; - if (!(this.group && this.group.type === 'path-group')) { - x = -this.width / 2; - y = -this.height / 2; - } - markup.push( - '\n'); - - return reviver ? reviver(markup.join('')) : markup.join(''); + _toSVG: function() { + var x = -this.width / 2, y = -this.height / 2; + return [ + '\n' + ]; }, /* _TO_SVG_END_ */ }); @@ -16431,22 +19362,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Rect * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing * @param {Object} [options] Options object - * @return {fabric.Rect} Instance of fabric.Rect */ - fabric.Rect.fromElement = function(element, options) { + fabric.Rect.fromElement = function(element, callback, options) { if (!element) { - return null; + 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; - return rect; + callback(rect); }; /* _FROM_SVG_END_ */ @@ -16456,11 +19388,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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 - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {Object} instance of fabric.Rect */ - fabric.Rect.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Rect', object, callback, forceAsync); + fabric.Rect.fromObject = function(object, callback) { + return fabric.Object._fromObject('Rect', object, callback); }; })(typeof exports !== 'undefined' ? exports : this); @@ -16475,16 +19405,13 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot min = fabric.util.array.min, max = fabric.util.array.max, toFixed = fabric.util.toFixed, - NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + projectStrokeOnPoints = fabric.util.projectStrokeOnPoints; if (fabric.Polyline) { fabric.warn('fabric.Polyline is already defined'); return; } - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push('points'); - /** * Polyline class * @class fabric.Polyline @@ -16508,20 +19435,17 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot points: null, /** - * Minimum X from points values, necessary to offset points - * @type Number - * @default + * WARNING: Feature in progress + * Calculate the exact bounding box taking in account strokeWidth on acute angles + * this will be turned to true by default on fabric 6.0 + * maybe will be left in as an optimization since calculations may be slow + * @deprecated + * @type Boolean + * @default false */ - minX: 0, + exactBoundingBox: false, - /** - * Minimum Y from points values, necessary to offset points - * @type Number - * @default - */ - minY: 0, - - cacheProperties: cacheProperties, + cacheProperties: fabric.Object.prototype.cacheProperties.concat('points'), /** * Constructor @@ -16546,34 +19470,72 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options = options || {}; this.points = points || []; this.callSuper('initialize', options); - this._calcDimensions(); - if (!('top' in options)) { - this.top = this.minY; - } - if (!('left' in options)) { - this.left = this.minX; - } - this.pathOffset = { - x: this.minX + this.width / 2, - y: this.minY + this.height / 2 - }; + this._setPositionDimensions(options); }, /** * @private */ + _projectStrokeOnPoints: function () { + return projectStrokeOnPoints(this.points, this, true); + }, + + _setPositionDimensions: function(options) { + var calcDim = this._calcDimensions(options), correctLeftTop, + correctSize = this.exactBoundingBox ? this.strokeWidth : 0; + this.width = calcDim.width - correctSize; + this.height = calcDim.height - correctSize; + if (!options.fromSVG) { + correctLeftTop = this.translateToGivenOrigin( + { + // this looks bad, but is one way to keep it optional for now. + x: calcDim.left - this.strokeWidth / 2 + correctSize / 2, + y: calcDim.top - this.strokeWidth / 2 + correctSize / 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 + correctSize / 2, + y: calcDim.top + this.height / 2 + correctSize / 2 + }; + }, + + /** + * Calculate the polygon min and max point from points array, + * returning an object with left, top, width, 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'), - minY = min(points, 'y'), - maxX = max(points, 'x'), - maxY = max(points, 'y'); + var points = this.exactBoundingBox ? this._projectStrokeOnPoints() : 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); - this.width = (maxX - minX) || 0; - this.height = (maxY - minY) || 0; - this.minX = minX || 0; - this.minY = minY || 0; + return { + left: minX, + top: minY, + width: width, + height: height, + }; }, /** @@ -16590,17 +19552,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /* _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 + * @return {Array} an array of strings with the specific svg representation + * of the instance */ - toSVG: function(reviver) { - var points = [], diffX, diffY, - markup = this._createBaseSVGMarkup(); - - if (!(this.group && this.group.type === 'path-group')) { - diffX = this.pathOffset.x; - diffY = this.pathOffset.y; - } + _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( @@ -16608,16 +19565,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ' ); } - markup.push( - '<', this.type, ' ', this.getSvgId(), - 'points="', points.join(''), - '" style="', this.getSvgStyles(), - '" transform="', this.getSvgTransform(), - ' ', this.getSvgTransformMatrix(), - '"/>\n' - ); - - return reviver ? reviver(markup.join('')) : markup.join(''); + return [ + '<' + this.type + ' ', 'COMMON_PARTS', + 'points="', points.join(''), + '" />\n' + ]; }, /* _TO_SVG_END_ */ @@ -16625,12 +19577,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} noTransform */ - commonRender: function(ctx, noTransform) { + commonRender: function(ctx) { var point, len = this.points.length, - x = noTransform ? 0 : this.pathOffset.x, - y = noTransform ? 0 : this.pathOffset.y; + 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 @@ -16649,29 +19600,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} noTransform */ - _render: function(ctx, noTransform) { - if (!this.commonRender(ctx, noTransform)) { + _render: function(ctx) { + if (!this.commonRender(ctx)) { return; } - this._renderFill(ctx); - this._renderStroke(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); - } + this._renderPaintInOrder(ctx); }, /** @@ -16696,21 +19630,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Returns fabric.Polyline instance from an SVG element * @static * @memberOf fabric.Polyline - * @param {SVGElement} element Element to parse + * @param {SVGElement} element Element to parser + * @param {Function} callback callback function invoked after parsing * @param {Object} [options] Options object - * @return {fabric.Polyline} Instance of fabric.Polyline */ - fabric.Polyline.fromElement = function(element, options) { - if (!element) { - return null; - } - options || (options = { }); + 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.Polyline.ATTRIBUTE_NAMES); - - return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, 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_ */ /** @@ -16719,11 +19658,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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 - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.Polyline} Instance of fabric.Polyline */ - fabric.Polyline.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Polyline', object, callback, forceAsync, 'points'); + fabric.Polyline.fromObject = function(object, callback) { + return fabric.Object._fromObject('Polyline', object, callback, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -16733,8 +19670,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 'use strict'; - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend; + var fabric = global.fabric || (global.fabric = {}), + projectStrokeOnPoints = fabric.util.projectStrokeOnPoints; if (fabric.Polygon) { fabric.warn('fabric.Polygon is already defined'); @@ -16758,26 +19695,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} noTransform */ - _render: function(ctx, noTransform) { - if (!this.commonRender(ctx, noTransform)) { - return; - } - ctx.closePath(); - this._renderFill(ctx); - this._renderStroke(ctx); + _projectStrokeOnPoints: function () { + return projectStrokeOnPoints(this.points, this); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _renderDashedStroke: function(ctx) { - this.callSuper('_renderDashedStroke', ctx); + _render: function(ctx) { + if (!this.commonRender(ctx)) { + return; + } ctx.closePath(); + this._renderPaintInOrder(ctx); }, + }); /* _FROM_SVG_START_ */ @@ -16794,21 +19728,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @static * @memberOf fabric.Polygon * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing * @param {Object} [options] Options object - * @return {fabric.Polygon} Instance of fabric.Polygon */ - fabric.Polygon.fromElement = function(element, options) { - if (!element) { - return null; - } - - options || (options = { }); - - var points = fabric.parsePointsAttribute(element.getAttribute('points')), - parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES); - - return new fabric.Polygon(points, extend(parsedAttributes, options)); - }; + fabric.Polygon.fromElement = fabric.Polyline.fromElementGenerator('Polygon'); /* _FROM_SVG_END_ */ /** @@ -16817,11 +19740,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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 - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.Polygon} Instance of fabric.Polygon + * @return {void} */ - fabric.Polygon.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Polygon', object, callback, forceAsync, 'points'); + fabric.Polygon.fromObject = function(object, callback) { + fabric.Object._fromObject('Polygon', object, callback, 'points'); }; })(typeof exports !== 'undefined' ? exports : this); @@ -16835,32 +19757,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot min = fabric.util.array.min, max = fabric.util.array.max, extend = fabric.util.object.extend, - _toString = Object.prototype.toString, - drawArc = fabric.util.drawArc, - commandLengths = { - m: 2, - l: 2, - h: 1, - v: 1, - c: 6, - s: 4, - q: 4, - t: 2, - a: 7 - }, - repeatedCommands = { - m: 'l', - M: 'L' - }; + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed; if (fabric.Path) { fabric.warn('fabric.Path is already defined'); return; } - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push('path'); - /** * Path class * @class fabric.Path @@ -16884,21 +19788,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ path: null, - /** - * Minimum X from points values, necessary to offset points - * @type Number - * @default - */ - minX: 0, + cacheProperties: fabric.Object.prototype.cacheProperties.concat('path', 'fillRule'), - /** - * Minimum Y from points values, necessary to offset points - * @type Number - * @default - */ - minY: 0, - - cacheProperties: cacheProperties, + stateProperties: fabric.Object.prototype.stateProperties.concat('path'), /** * Constructor @@ -16906,71 +19798,24 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Object} [options] Options object * @return {fabric.Path} thisArg */ - initialize: function(path, options) { - options = options || { }; - - if (options) { - this.setOptions(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(); - } - - this._setPositionDimensions(options); - - if (this.objectCaching) { - this._createCacheCanvas(); - } + initialize: function (path, options) { + options = clone(options || {}); + delete options.path; + this.callSuper('initialize', options); + this._setPath(path || [], options); }, /** - * @private - * @param {Object} options Options object - */ - _setPositionDimensions: function(options) { - var calcDim = this._parseDimensions(); + * @private + * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) + * @param {Object} [options] Options object + */ + _setPath: function (path, options) { + this.path = fabric.util.makePathSimpler( + Array.isArray(path) ? path : fabric.util.parsePath(path) + ); - this.minX = calcDim.left; - this.minY = calcDim.top; - this.width = calcDim.width; - this.height = calcDim.height; - - if (typeof options.left === 'undefined') { - this.left = calcDim.left + (this.originX === 'center' - ? this.width / 2 - : this.originX === 'right' - ? this.width - : 0); - } - - if (typeof options.top === 'undefined') { - this.top = calcDim.top + (this.originY === 'center' - ? this.height / 2 - : this.originY === 'bottom' - ? this.height - : 0); - } - - this.pathOffset = this.pathOffset || { - x: this.minX + this.width / 2, - y: this.minY + this.height / 2 - }; + fabric.Polyline.prototype._setPositionDimensions.call(this, options || {}); }, /** @@ -16979,23 +19824,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _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; - if (this.group && this.group.type === 'path-group') { - l = 0; - t = 0; - } - ctx.beginPath(); for (var i = 0, len = this.path.length; i < len; ++i) { @@ -17004,46 +19841,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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]; @@ -17052,23 +19855,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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]; @@ -17084,195 +19870,19 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ); 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 + current[3] + l, + current[4] + t ); - x = tempX; - y = tempY; + x = current[3]; + y = current[4]; 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; @@ -17280,7 +19890,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ctx.closePath(); break; } - previous = current; } }, @@ -17290,8 +19899,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _render: function(ctx) { this._renderPathCommands(ctx); - this._renderFill(ctx); - this._renderStroke(ctx); + this._renderPaintInOrder(ctx); }, /** @@ -17309,12 +19917,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { - var o = extend(this.callSuper('toObject', ['sourcePath', 'pathOffset'].concat(propertiesToInclude)), { + return extend(this.callSuper('toObject', propertiesToInclude), { path: this.path.map(function(item) { return item.slice(); }), - top: this.top, - left: this.left, }); - return o; }, /** @@ -17323,41 +19928,55 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Object} object representation of an instance */ toDatalessObject: function(propertiesToInclude) { - var o = this.toObject(propertiesToInclude); - if (this.sourcePath) { - o.path = this.sourcePath; + var o = this.toObject(['sourcePath'].concat(propertiesToInclude)); + if (o.sourcePath) { + delete o.path; } - delete o.sourcePath; 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 = fabric.util.joinPath(this.path); + 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 chunks = [], - markup = this._createBaseSVGMarkup(), addTransform = ''; - - for (var i = 0, len = this.path.length; i < len; i++) { - chunks.push(this.path[i].join(' ')); - } - var path = chunks.join(' '); - if (!(this.group && this.group.type === 'path-group')) { - addTransform = ' translate(' + (-this.pathOffset.x) + ', ' + (-this.pathOffset.y) + ') '; - } - markup.push( - '\n' - ); - - return reviver ? reviver(markup.join('')) : markup.join(''); + var additionalTransform = this._getOffsetTransform(); + return this._createBaseSVGMarkup(this._toSVG(), { reviver: reviver, additionalTransform: additionalTransform }); }, /* _TO_SVG_END_ */ @@ -17372,69 +19991,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * @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 - */ - _parseDimensions: function() { + _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) { @@ -17443,46 +20008,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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]; @@ -17491,31 +20022,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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[3], + current[4], current[5], current[6] ); @@ -17523,100 +20035,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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; - } - + case 'Q': // quadraticCurveTo, absolute 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[1], + current[2], current[3], current[4] ); @@ -17624,99 +20048,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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); @@ -17730,16 +20067,14 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot maxX = max(aX) || 0, maxY = max(aY) || 0, deltaX = maxX - minX, - deltaY = maxY - minY, + deltaY = maxY - minY; - o = { - left: minX, - top: minY, - width: deltaX, - height: deltaY - }; - - return o; + return { + left: minX, + top: minY, + width: deltaX, + height: deltaY + }; } }); @@ -17749,25 +20084,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @memberOf fabric.Path * @param {Object} object * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first */ - fabric.Path.fromObject = function(object, callback, forceAsync) { - // remove this pattern rom 2.0, accept just object. - var path; - if (typeof object.path === 'string') { - fabric.loadSVGFromURL(object.path, function (elements) { - var pathUrl = object.path; - path = elements[0]; - delete object.path; - - fabric.util.object.extend(path, object); - path.setSourcePath(pathUrl); - - callback && callback(path); + 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); + if (object.clipPath) { + fabric.util.enlivenObjects([object.clipPath], function(elivenedObjects) { + path.clipPath = elivenedObjects[0]; + callback && callback(path); + }); + } + else { + callback && callback(path); + } }); } else { - return fabric.Object._fromObject('Path', object, callback, forceAsync, 'path'); + fabric.Object._fromObject('Path', object, callback, 'path'); } }; @@ -17787,22 +20123,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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); - callback && callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); + parsedAttributes.fromSVG = true; + callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); }; /* _FROM_SVG_END_ */ - /** - * Indicates that instances of this type are async - * @static - * @memberOf fabric.Path - * @type Boolean - * @default - */ - fabric.Path.async = true; - })(typeof exports !== 'undefined' ? exports : this); @@ -17811,338 +20140,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 'use strict'; var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend; - - if (fabric.PathGroup) { - fabric.warn('fabric.PathGroup is already defined'); - return; - } - - /** - * Path group class - * @class fabric.PathGroup - * @extends fabric.Path - * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} - * @see {@link fabric.PathGroup#initialize} for constructor definition - */ - fabric.PathGroup = fabric.util.createClass(fabric.Object, /** @lends fabric.PathGroup.prototype */ { - - /** - * Type of an object - * @type String - * @default - */ - type: 'path-group', - - /** - * Fill value - * @type String - * @default - */ - fill: '', - - /** - * Constructor - * @param {Array} paths - * @param {Object} [options] Options object - * @return {fabric.PathGroup} thisArg - */ - initialize: function(paths, options) { - - options = options || { }; - this.paths = paths || []; - - for (var i = this.paths.length; i--;) { - this.paths[i].group = this; - } - - if (options.toBeParsed) { - this.parseDimensionsFromPaths(options); - delete options.toBeParsed; - } - this.setOptions(options); - this.setCoords(); - if (this.objectCaching) { - this._createCacheCanvas(); - } - }, - - /** - * Calculate width and height based on paths contained - */ - parseDimensionsFromPaths: function(options) { - var points, p, xC = [], yC = [], path, height, width, - m; - for (var j = this.paths.length; j--;) { - path = this.paths[j]; - height = path.height + path.strokeWidth; - width = path.width + path.strokeWidth; - points = [ - { x: path.left, y: path.top }, - { x: path.left + width, y: path.top }, - { x: path.left, y: path.top + height }, - { x: path.left + width, y: path.top + height } - ]; - m = this.paths[j].transformMatrix; - for (var i = 0; i < points.length; i++) { - p = points[i]; - if (m) { - p = fabric.util.transformPoint(p, m, false); - } - xC.push(p.x); - yC.push(p.y); - } - } - options.width = Math.max.apply(null, xC); - options.height = Math.max.apply(null, yC); - }, - - /** - * Execute the drawing operation for an object on a specified context - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} [noTransform] When true, context is not transformed - */ - drawObject: function(ctx) { - ctx.save(); - ctx.translate(-this.width / 2, -this.height / 2); - for (var i = 0, l = this.paths.length; i < l; ++i) { - this.paths[i].render(ctx, true); - } - ctx.restore(); - }, - - /** - * Decide if the object should cache or not. - * 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() { - var parentCache = this.objectCaching && (!this.group || this.needsItsOwnCache || !this.group.isCaching()); - this.caching = parentCache; - if (parentCache) { - for (var i = 0, len = this.paths.length; i < len; i++) { - if (this.paths[i].willDrawShadow()) { - this.caching = false; - return false; - } - } - } - return parentCache; - }, - - /** - * Check if this object or a child object will cast a shadow - * @return {Boolean} - */ - willDrawShadow: function() { - if (this.shadow) { - return true; - } - for (var i = 0, len = this.paths.length; i < len; i++) { - if (this.paths[i].willDrawShadow()) { - return true; - } - } - return false; - }, - - /** - * Check if this group or its parent group are caching, recursively up - * @return {Boolean} - */ - isCaching: function() { - return this.caching || this.group && this.group.isCaching(); - }, - - /** - * Check if cache is dirty - */ - isCacheDirty: function() { - if (this.callSuper('isCacheDirty')) { - return true; - } - if (!this.statefullCache) { - return false; - } - for (var i = 0, len = this.paths.length; i < len; i++) { - if (this.paths[i].isCacheDirty(true)) { - var dim = this._getNonTransformedDimensions(); - this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); - return true; - } - } - return false; - }, - - /** - * Sets certain property to a certain value - * @param {String} prop - * @param {*} value - * @return {fabric.PathGroup} thisArg - */ - _set: function(prop, value) { - - if (prop === 'fill' && value && this.isSameColor()) { - var i = this.paths.length; - while (i--) { - this.paths[i]._set(prop, value); - } - } - - return this.callSuper('_set', prop, value); - }, - - /** - * Returns object representation of this path group - * @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 pathsToObject = this.paths.map(function(path) { - var originalDefaults = path.includeDefaultValues; - path.includeDefaultValues = path.group.includeDefaultValues; - var obj = path.toObject(propertiesToInclude); - path.includeDefaultValues = originalDefaults; - return obj; - }); - var o = extend(this.callSuper('toObject', ['sourcePath'].concat(propertiesToInclude)), { - paths: pathsToObject - }); - return o; - }, - - /** - * Returns dataless object representation of this path group - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} dataless object representation of an instance - */ - toDatalessObject: function(propertiesToInclude) { - var o = this.toObject(propertiesToInclude); - if (this.sourcePath) { - o.paths = this.sourcePath; - } - return o; - }, - - /* _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 objects = this.getObjects(), - p = this.getPointByOrigin('left', 'top'), - translatePart = 'translate(' + p.x + ' ' + p.y + ')', - markup = this._createBaseSVGMarkup(); - markup.push( - '\n' - ); - - for (var i = 0, len = objects.length; i < len; i++) { - markup.push('\t', objects[i].toSVG(reviver)); - } - markup.push('\n'); - - return reviver ? reviver(markup.join('')) : markup.join(''); - }, - /* _TO_SVG_END_ */ - - /** - * Returns a string representation of this path group - * @return {String} string representation of an object - */ - toString: function() { - return '#'; - }, - - /** - * Returns true if all paths in this group are of same color - * @return {Boolean} true if all paths are of the same color (`fill`) - */ - isSameColor: function() { - var firstPathFill = this.getObjects()[0].get('fill') || ''; - if (typeof firstPathFill !== 'string') { - return false; - } - firstPathFill = firstPathFill.toLowerCase(); - return this.getObjects().every(function(path) { - var pathFill = path.get('fill') || ''; - return typeof pathFill === 'string' && (pathFill).toLowerCase() === firstPathFill; - }); - }, - - /** - * Returns number representation of object's complexity - * @return {Number} complexity - */ - complexity: function() { - return this.paths.reduce(function(total, path) { - return total + ((path && path.complexity) ? path.complexity() : 0); - }, 0); - }, - - /** - * Returns all paths in this path group - * @return {Array} array of path objects included in this path group - */ - getObjects: function() { - return this.paths; - } - }); - - /** - * Creates fabric.PathGroup instance from an object representation - * @static - * @memberOf fabric.PathGroup - * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when an fabric.PathGroup instance is created - */ - fabric.PathGroup.fromObject = function(object, callback) { - var originalPaths = object.paths; - delete object.paths; - if (typeof originalPaths === 'string') { - fabric.loadSVGFromURL(originalPaths, function (elements) { - var pathUrl = originalPaths; - var pathGroup = fabric.util.groupSVGElements(elements, object, pathUrl); - object.paths = originalPaths; - callback(pathGroup); - }); - } - else { - fabric.util.enlivenObjects(originalPaths, function(enlivenedObjects) { - var pathGroup = new fabric.PathGroup(enlivenedObjects, object); - object.paths = originalPaths; - callback(pathGroup); - }); - } - }; - - /** - * Indicates that instances of this type are async - * @static - * @memberOf fabric.PathGroup - * @type Boolean - * @default - */ - fabric.PathGroup.async = true; - -})(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; @@ -18150,18 +20147,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return; } - // lock-related properties, for use in fabric.Group#get - // to enable locking behavior on group - // when one of its objects has lock-related properties set - var _lockProperties = { - lockMovementX: true, - lockMovementY: true, - lockRotation: true, - lockScalingX: true, - lockScalingY: true, - lockUniScaling: true - }; - /** * Group class * @class fabric.Group @@ -18187,12 +20172,28 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot strokeWidth: 0, /** - * Indicates if click events should also check for subtargets + * Indicates if click, mouseover, mouseout events & hoverCursor 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 @@ -18201,50 +20202,61 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Object} thisArg */ initialize: function(objects, options, isAlreadyGrouped) { - options = options || { }; - + 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, - // because delegatedProperties propagate to objects. isAlreadyGrouped && this.callSuper('initialize', 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; - } - - if (isAlreadyGrouped) { - // do not change coordinate of objects enclosed in a group, - // because objects coordinate system have been group coodinate system already. - this._updateObjectsCoords(true); + 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._calcBounds(); - this._updateObjectsCoords(); - this.callSuper('initialize', options); + this._updateObjectsACoords(); } this.setCoords(); - this.saveCoords(); + }, + + /** + * @private + */ + _updateObjectsACoords: function() { + var skipControls = true; + for (var i = this._objects.length; i--; ){ + this._objects[i].setCoords(skipControls); + } }, /** * @private * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change */ - _updateObjectsCoords: function(skipCoordsChange) { - var center = this.getCenterPoint(); + _updateObjectsCoords: function(center) { + var center = center || this.getCenterPoint(); for (var i = this._objects.length; i--; ){ - this._updateObjectCoords(this._objects[i], center, skipCoordsChange); + this._updateObjectCoords(this._objects[i], center); } }, @@ -18252,26 +20264,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @private * @param {Object} object * @param {fabric.Point} center, current center of group. - * @param {Boolean} [skipCoordsChange] if true, coordinates of object dose not change */ - _updateObjectCoords: function(object, center, skipCoordsChange) { - // do not display corners of objects enclosed in a group - object.__origHasControls = object.hasControls; - object.hasControls = false; - - if (skipCoordsChange) { - return; - } - - var objectLeft = object.getLeft(), - objectTop = object.getTop(), - ignoreZoom = true, skipAbsolute = true; + _updateObjectCoords: function(object, center) { + var objectLeft = object.left, + objectTop = object.top, + skipControls = true; object.set({ left: objectLeft - center.x, top: objectTop - center.y }); - object.setCoords(ignoreZoom, skipAbsolute); + object.group = this; + object.setCoords(skipControls); }, /** @@ -18289,29 +20293,30 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @chainable */ addWithUpdate: function(object) { + var nested = !!this.group; this._restoreObjectsState(); fabric.util.resetObjectTransform(this); if (object) { + if (nested) { + // if this group is inside another group, we need to pre transform the object + fabric.util.removeTransformFromObject(object, this.group.calcTransformMatrix()); + } this._objects.push(object); object.group = this; object._set('canvas', this.canvas); } - // since _restoreObjectsState set objects inactive - this.forEachObject(this._setObjectActive, this); this._calcBounds(); this._updateObjectsCoords(); this.dirty = true; + if (nested) { + this.group.addWithUpdate(); + } + else { + this.setCoords(); + } return this; }, - /** - * @private - */ - _setObjectActive: function(object) { - object.set('active', true); - object.group = this; - }, - /** * Removes an object from a group; Then recalculates group's dimension, position. * @param {Object} object @@ -18321,12 +20326,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot removeWithUpdate: function(object) { this._restoreObjectsState(); fabric.util.resetObjectTransform(this); - // since _restoreObjectsState set objects inactive - this.forEachObject(this._setObjectActive, this); this.remove(object); this._calcBounds(); this._updateObjectsCoords(); + this.setCoords(); this.dirty = true; return this; }, @@ -18346,25 +20350,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _onObjectRemoved: function(object) { this.dirty = true; delete object.group; - object.set('active', false); - }, - - /** - * Properties that are delegated to group objects when reading/writing - * @param {Object} delegatedProperties - */ - delegatedProperties: { - fill: true, - stroke: true, - strokeWidth: true, - fontFamily: true, - fontWeight: true, - fontSize: true, - fontStyle: true, - lineHeight: true, - textDecoration: true, - textAlign: true, - backgroundColor: true }, /** @@ -18372,19 +20357,17 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _set: function(key, value) { var i = this._objects.length; - - if (this.delegatedProperties[key] || key === 'canvas') { - while (i--) { - this._objects[i].set(key, value); - } - } - else { + if (this.useSetOnGroup) { while (i--) { this._objects[i].setOnGroup(key, value); } } - - this.callSuper('_set', key, value); + if (key === 'canvas') { + while (i--) { + this._objects[i]._set(key, value); + } + } + fabric.Object.prototype._set.call(this, key, value); }, /** @@ -18393,16 +20376,21 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { - var objsToObject = this.getObjects().map(function(obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = obj.group.includeDefaultValues; - var _obj = obj.toObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - return extend(this.callSuper('toObject', propertiesToInclude), { - objects: objsToObject - }); + var _includeDefaultValues = this.includeDefaultValues; + var objsToObject = this._objects + .filter(function (obj) { + return !obj.excludeFromExport; + }) + .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; }, /** @@ -18411,16 +20399,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Object} object representation of an instance */ toDatalessObject: function(propertiesToInclude) { - var objsToObject = this.getObjects().map(function(obj) { - var originalDefaults = obj.includeDefaultValues; - obj.includeDefaultValues = obj.group.includeDefaultValues; - var _obj = obj.toDatalessObject(propertiesToInclude); - obj.includeDefaultValues = originalDefaults; - return _obj; - }); - return extend(this.callSuper('toDatalessObject', propertiesToInclude), { - objects: objsToObject - }); + 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; }, /** @@ -18434,25 +20429,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, /** - * Decide if the object should cache or not. - * objectCaching is a global flag, wins over everything + * 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. + * Generally you do not cache objects in groups because the group is already cached. * @return {Boolean} */ shouldCache: function() { - var parentCache = this.objectCaching && (!this.group || this.needsItsOwnCache || !this.group.isCaching()); - this.caching = parentCache; - if (parentCache) { + 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.caching = false; + this.ownCaching = false; return false; } } } - return parentCache; + return ownCache; }, /** @@ -18460,7 +20453,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Boolean} */ willDrawShadow: function() { - if (this.shadow) { + if (fabric.Object.prototype.willDrawShadow.call(this)) { return true; } for (var i = 0, len = this._objects.length; i < len; i++) { @@ -18475,26 +20468,26 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * Check if this group or its parent group are caching, recursively up * @return {Boolean} */ - isCaching: function() { - return this.caching || this.group && this.group.isCaching(); + 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 - * @param {Boolean} [noTransform] When true, context is not transformed */ drawObject: function(ctx) { for (var i = 0, len = this._objects.length; i < len; i++) { - this._renderObject(this._objects[i], ctx); + this._objects[i].render(ctx); } + this._drawClipPath(ctx, this.clipPath); }, /** * Check if cache is dirty */ - isCacheDirty: function() { - if (this.callSuper('isCacheDirty')) { + isCacheDirty: function(skipCanvas) { + if (this.callSuper('isCacheDirty', skipCanvas)) { return true; } if (!this.statefullCache) { @@ -18502,8 +20495,11 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } for (var i = 0, len = this._objects.length; i < len; i++) { if (this._objects[i].isCacheDirty(true)) { - var dim = this._getNonTransformedDimensions(); - this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); + 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; } } @@ -18511,83 +20507,22 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot }, /** - * Renders controls and borders for the object - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} [noTransform] When true, context is not transformed - */ - _renderControls: function(ctx, noTransform) { - ctx.save(); - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - this.callSuper('_renderControls', ctx, noTransform); - for (var i = 0, len = this._objects.length; i < len; i++) { - this._objects[i]._renderControls(ctx); - } - ctx.restore(); - }, - - /** - * @private - */ - _renderObject: function(object, ctx) { - // do not render if object is not visible - if (!object.visible) { - return; - } - - var originalHasRotatingPoint = object.hasRotatingPoint; - object.hasRotatingPoint = false; - object.render(ctx); - object.hasRotatingPoint = originalHasRotatingPoint; - }, - - /** - * Retores original state of each of group objects (original state is that which was before group was created). + * Restores original state of each of group objects (original state is that which was before group was created). + * if the nested boolean is true, the original state will be restored just for the + * first group and not for all the group chain * @private + * @param {Boolean} nested tell the function to restore object state up to the parent group and not more * @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(); - object.hasControls = object.__origHasControls; - delete object.__origHasControls; - object.set('active', false); - delete object.group; - + var groupMatrix = this.calcOwnMatrix(); + this._objects.forEach(function(object) { + // instead of using _this = this; + fabric.util.addTransformToObject(object, groupMatrix); + delete object.group; + object.setCoords(); + }); return this; }, @@ -18597,28 +20532,59 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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(); }, - /** - * Saves coordinates of this instance (to be used together with `hasMoved`) - * @saveCoords - * @return {fabric.Group} thisArg - * @chainable - */ - saveCoords: function() { - this._originalLeft = this.get('left'); - this._originalTop = this.get('top'); - return this; + dispose: function () { + this.callSuper('dispose'); + this.forEachObject(function (object) { + object.dispose && object.dispose(); + }); + this._objects = []; }, /** - * Checks whether this group was moved (since `saveCoords` was called last) - * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) + * 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 */ - hasMoved: function() { - return this._originalLeft !== this.get('left') || - this._originalTop !== this.get('top'); + 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(); }, /** @@ -18627,9 +20593,9 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @chainable */ setObjectsCoords: function() { - var ignoreZoom = true, skipAbsolute = true; + var skipControls = true; this.forEachObject(function(object) { - object.setCoords(ignoreZoom, skipAbsolute); + object.setCoords(skipControls); }); return this; }, @@ -18640,23 +20606,23 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _calcBounds: function(onlyWidthHeight) { var aX = [], aY = [], - o, prop, + o, prop, coords, props = ['tr', 'br', 'bl', 'tl'], i = 0, iLen = this._objects.length, - j, jLen = props.length, - ignoreZoom = true; + j, jLen = props.length; for ( ; i < iLen; ++i) { o = this._objects[i]; - o.setCoords(ignoreZoom); + coords = o.calcACoords(); for (j = 0; j < jLen; j++) { prop = props[j]; - aX.push(o.oCoords[prop].x); - aY.push(o.oCoords[prop].y); + aX.push(coords[prop].x); + aY.push(coords[prop].y); } + o.aCoords = coords; } - this.set(this._getBounds(aX, aY, onlyWidthHeight)); + this._getBounds(aX, aY, onlyWidthHeight); }, /** @@ -18665,28 +20631,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot _getBounds: function(aX, aY, onlyWidthHeight) { var minXY = new fabric.Point(min(aX), min(aY)), maxXY = new fabric.Point(max(aX), max(aY)), - obj = { - width: (maxXY.x - minXY.x) || 0, - height: (maxXY.y - minXY.y) || 0 - }; - + 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) { - obj.left = minXY.x || 0; - obj.top = minXY.y || 0; - if (this.originX === 'center') { - obj.left += obj.width / 2; - } - if (this.originX === 'right') { - obj.left += obj.width; - } - if (this.originY === 'center') { - obj.top += obj.height / 2; - } - if (this.originY === 'bottom') { - obj.top += obj.height; - } + // 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'); } - return obj; }, /* _TO_SVG_START_ */ @@ -18695,54 +20649,46 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ - toSVG: function(reviver) { - var markup = this._createBaseSVGMarkup(); - markup.push( - '\n' - ); + _toSVG: function(reviver) { + var svgString = ['\n']; for (var i = 0, len = this._objects.length; i < len; i++) { - markup.push('\t', this._objects[i].toSVG(reviver)); + svgString.push('\t\t', this._objects[i].toSVG(reviver)); } - - markup.push('\n'); - - return reviver ? reviver(markup.join('')) : markup.join(''); + svgString.push('\n'); + return svgString; }, - /* _TO_SVG_END_ */ /** - * Returns requested property - * @param {String} prop Property to get - * @return {*} + * Returns styles-string for svg-export, specific version for group + * @return {String} */ - get: function(prop) { - if (prop in _lockProperties) { - if (this[prop]) { - return this[prop]; - } - else { - for (var i = 0, len = this._objects.length; i < len; i++) { - if (this._objects[i][prop]) { - return true; - } - } - return false; - } + 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)); } - else { - if (prop in this.delegatedProperties) { - return this._objects[0] && this._objects[0].get(prop); - } - return this[prop]; - } - } + + return this._createBaseClipPathSVGMarkup(svgString, { reviver: reviver }); + }, + /* _TO_SVG_END_ */ }); /** @@ -18753,20 +20699,190 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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) { - delete object.objects; - callback && callback(new fabric.Group(enlivenedObjects, object, true)); + var objects = object.objects, + options = fabric.util.object.clone(object, true); + delete options.objects; + if (typeof objects === 'string') { + // it has to be an url or something went wrong. + fabric.loadSVGFromURL(objects, function (elements) { + var group = fabric.util.groupSVGElements(elements, object, objects); + var clipPath = options.clipPath; + delete options.clipPath; + group.set(options); + if (clipPath) { + fabric.util.enlivenObjects([clipPath], function(elivenedObjects) { + group.clipPath = elivenedObjects[0]; + callback && callback(group); + }); + } + else { + callback && callback(group); + } + }); + return; + } + fabric.util.enlivenObjects(objects, function (enlivenedObjects) { + fabric.util.enlivenObjectEnlivables(object, options, function () { + 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; + } /** - * Indicates that instances of this type are async - * @static - * @memberOf fabric.Group - * @type Boolean - * @default + * 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.Group.async = true; + 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; + } + 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); @@ -18786,13 +20902,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return; } - var stateProperties = fabric.Object.prototype.stateProperties.concat(); - stateProperties.push( - 'alignX', - 'alignY', - 'meetOrSlice' - ); - /** * Image class * @class fabric.Image @@ -18809,41 +20918,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ 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: '', - - /** - * AlignX value, part of preserveAspectRatio (one of "none", "mid", "min", "max") - * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute - * This parameter defines how the picture is aligned to its viewport when image element width differs from image width. - * @type String - * @default - */ - alignX: 'none', - - /** - * AlignY value, part of preserveAspectRatio (one of "none", "mid", "min", "max") - * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute - * This parameter defines how the picture is aligned to its viewport when image element height differs from image height. - * @type String - * @default - */ - alignY: 'none', - - /** - * meetOrSlice value, part of preserveAspectRatio (one of "meet", "slice"). - * if meet the image is always fully visibile, if slice the viewport is always filled with image. - * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute - * @type String - * @default - */ - meetOrSlice: 'meet', - /** * Width of a stroke. * For image quality a stroke multiple of 2 gives better results. @@ -18852,6 +20926,15 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ 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 @@ -18868,11 +20951,24 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot */ _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 can be used in case we want to scale with some filter above - * the natural image dimensions + * number bigger than 1 are not implemented yet. * @type Number */ minimumScaleTrigger: 0.5, @@ -18883,30 +20979,67 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * as well as for history (undo/redo) purposes * @type Array */ - stateProperties: stateProperties, + stateProperties: fabric.Object.prototype.stateProperties.concat('cropX', 'cropY'), /** - * When `true`, object is cached on an additional canvas. - * default to false for images - * since 1.7.0 + * 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: fabric.Object.prototype.cacheProperties.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, + + /** + * Indicates whether this canvas will use image smoothing when painting this image. + * Also influence if the cacheCanvas for this image uses imageSmoothing + * @since 4.0.0-beta.11 * @type Boolean * @default */ - objectCaching: false, + imageSmoothing: true, /** * Constructor - * @param {HTMLImageElement | String} element Image element + * Image can be initialized with any canvas drawable or a string. + * The string should be a url and will be loaded as an image. + * Canvas and Image element work out of the box, while videos require extra code to work. + * Please check video element events for seeking. + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | 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, callback) { + initialize: function(element, options) { options || (options = { }); this.filters = []; - this.resizeFilters = []; + this.cacheKey = 'texture' + fabric.Object.__uid++; this.callSuper('initialize', options); - this._initElement(element, options, callback); + this._initElement(element, options); }, /** @@ -18914,7 +21047,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {HTMLImageElement} Image element */ getElement: function() { - return this._element; + return this._element || {}; }, /** @@ -18922,49 +21055,58 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * 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 {Function} [callback] Callback is invoked when all filters have been applied and new image is generated * @param {Object} [options] Options object * @return {fabric.Image} thisArg * @chainable */ - setElement: function(element, callback, options) { - - var _callback, _this; - + setElement: function(element, options) { + this.removeTexture(this.cacheKey); + this.removeTexture(this.cacheKey + '_filtered'); this._element = element; this._originalElement = element; this._initConfig(options); - - if (this.resizeFilters.length === 0) { - _callback = callback; - } - else { - _this = this; - _callback = function() { - _this.applyFilters(callback, _this.resizeFilters, _this._filteredEl || _this._originalElement, true); - }; - } - if (this.filters.length !== 0) { - this.applyFilters(_callback); + this.applyFilters(); } - else if (_callback) { - _callback(this); + // resizeFilters work on the already filtered copy. + // we need to apply resizeFilters AFTER normal filters. + // applyResizeFilters is run more often than normal filters + // and is triggered by user interactions rather than dev code + if (this.resizeFilter) { + this.applyResizeFilters(); } - return this; }, /** - * Sets crossOrigin value (on an instance and corresponding image element) - * @return {fabric.Image} thisArg - * @chainable + * Delete a single texture if in webgl mode */ - setCrossOrigin: function(value) { - this.crossOrigin = value; - this._element.crossOrigin = value; + removeTexture: function(key) { + var backend = fabric.filterBackend; + if (backend && backend.evictCachesForKey) { + backend.evictCachesForKey(key); + } + }, - return this; + /** + * Delete textures, reference to elements and eventually JSDOM cleanup + */ + dispose: function () { + this.callSuper('dispose'); + 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)); + }, + + /** + * Get the crossOrigin value (of the corresponding image element) + */ + getCrossOrigin: function() { + return this._originalElement && (this._originalElement.crossOrigin || null); }, /** @@ -18974,8 +21116,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot getOriginalSize: function() { var element = this.getElement(); return { - width: element.width, - height: element.height + width: element.naturalWidth || element.width, + height: element.naturalHeight || element.height }; }, @@ -18997,112 +21139,96 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot 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); - - 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 = [], resizeFilters = [], - scaleX = 1, scaleY = 1; + var filters = []; this.filters.forEach(function(filterObj) { if (filterObj) { - if (filterObj.type === 'Resize') { - scaleX *= filterObj.scaleX; - scaleY *= filterObj.scaleY; - } filters.push(filterObj.toObject()); } }); - - this.resizeFilters.forEach(function(filterObj) { - filterObj && resizeFilters.push(filterObj.toObject()); - }); var object = extend( this.callSuper( 'toObject', - ['crossOrigin', 'alignX', 'alignY', 'meetOrSlice'].concat(propertiesToInclude) + ['cropX', 'cropY'].concat(propertiesToInclude) ), { src: this.getSrc(), + crossOrigin: this.getCrossOrigin(), filters: filters, - resizeFilters: resizeFilters, }); - - object.width /= scaleX; - object.height /= scaleY; - + if (this.resizeFilter) { + object.resizeFilter = this.resizeFilter.toObject(); + } return object; }, + /** + * Returns true if an image has crop applied, inspecting values of cropX,cropY,width,height. + * @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 - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance + * Returns svg representation of an instance + * @return {Array} an array of strings with the specific svg representation + * of the instance */ - toSVG: function(reviver) { - var markup = this._createBaseSVGMarkup(), x = -this.width / 2, y = -this.height / 2, - preserveAspectRatio = 'none', filtered = true; - if (this.group && this.group.type === 'path-group') { - x = this.left; - y = this.top; + _toSVG: function() { + var svgString = [], imageMarkup = [], strokeSvg, element = this._element, + x = -this.width / 2, y = -this.height / 2, clipPath = '', imageRendering = ''; + if (!element) { + return []; } - if (this.alignX !== 'none' && this.alignY !== 'none') { - preserveAspectRatio = 'x' + this.alignX + 'Y' + this.alignY + ' ' + this.meetOrSlice; + if (this.hasCrop()) { + var clipPathId = fabric.Object.__uid++; + svgString.push( + '\n', + '\t\n', + '\n' + ); + clipPath = ' clip-path="url(#imageCrop_' + clipPathId + ')" '; } - markup.push( - '\n', - '\n' - ); + if (!this.imageSmoothing) { + imageRendering = '" image-rendering="optimizeSpeed'; + } + imageMarkup.push('\t\n'); if (this.stroke || this.strokeDashArray) { var origFill = this.fill; this.fill = null; - markup.push( - '\n' - ); + ]; this.fill = origFill; } - - markup.push('\n'); - - return reviver ? reviver(markup.join('')) : markup.join(''); + if (this.paintFirst !== 'fill') { + svgString = svgString.concat(strokeSvg, imageMarkup); + } + else { + svgString = svgString.concat(imageMarkup, strokeSvg); + } + return svgString; }, /* _TO_SVG_END_ */ @@ -19114,7 +21240,16 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot getSrc: function(filtered) { var element = filtered ? this._element : this._originalElement; if (element) { - return fabric.isLikelyNode ? element._src : element.src; + if (element.toDataURL) { + return element.toDataURL(); + } + + if (this.srcFromAttribute) { + return element.getAttribute('src'); + } + else { + return element.src; + } } else { return this.src || ''; @@ -19126,13 +21261,18 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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 + * @param {String} [options.crossOrigin] crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes * @return {fabric.Image} thisArg * @chainable */ setSrc: function(src, callback, options) { - fabric.util.loadImage(src, function(img) { - return this.setElement(img, callback, options); + fabric.util.loadImage(src, function(img, isError) { + this.setElement(img, options); + this._setWidthHeight(); + callback && callback(this, isError); }, this, options && options.crossOrigin); + return this; }, /** @@ -19143,173 +21283,175 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot return '#'; }, - /** - * Applies filters assigned to this image (from "filters" array) - * @method applyFilters - * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated - * @param {Array} filters to be applied - * @param {fabric.Image} imgElement image to filter ( default to this._element ) - * @param {Boolean} forResizing - * @return {CanvasElement} canvasEl to be drawn immediately - * @chainable - */ - applyFilters: function(callback, filters, imgElement, forResizing) { - - filters = filters || this.filters; - imgElement = imgElement || this._originalElement; - - if (!imgElement) { + 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; + }, - var replacement = fabric.util.createImage(), - retinaScaling = this.canvas ? this.canvas.getRetinaScaling() : fabric.devicePixelRatio, - minimumScale = this.minimumScaleTrigger / retinaScaling, - _this = this, scaleX, scaleY; + /** + * 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 = imgElement; - callback && callback(this); - return imgElement; + this._element = this._originalElement; + this._filteredEl = null; + this._filterScalingX = 1; + this._filterScalingY = 1; + return this; } - var canvasEl = fabric.util.createCanvasElement(); - canvasEl.width = imgElement.width; - canvasEl.height = imgElement.height; - canvasEl.getContext('2d').drawImage(imgElement, 0, 0, imgElement.width, imgElement.height); + var imgElement = this._originalElement, + sourceWidth = imgElement.naturalWidth || imgElement.width, + sourceHeight = imgElement.naturalHeight || imgElement.height; - filters.forEach(function(filter) { - if (!filter) { - return; - } - if (forResizing) { - scaleX = _this.scaleX < minimumScale ? _this.scaleX : 1; - scaleY = _this.scaleY < minimumScale ? _this.scaleY : 1; - if (scaleX * retinaScaling < 1) { - scaleX *= retinaScaling; - } - if (scaleY * retinaScaling < 1) { - scaleY *= retinaScaling; - } - } - else { - scaleX = filter.scaleX; - scaleY = filter.scaleY; - } - filter.applyTo(canvasEl, scaleX, scaleY); - if (!forResizing && filter.type === 'Resize') { - _this.width *= filter.scaleX; - _this.height *= filter.scaleY; - } - }); - - /** @ignore */ - replacement.width = canvasEl.width; - replacement.height = canvasEl.height; - if (fabric.isLikelyNode) { - replacement.src = canvasEl.toBuffer(undefined, fabric.Image.pngCompression); - // onload doesn't fire in some node versions, so we invoke callback manually - _this._element = replacement; - !forResizing && (_this._filteredEl = replacement); - callback && callback(_this); + 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 { - replacement.onload = function() { - _this._element = replacement; - !forResizing && (_this._filteredEl = replacement); - callback && callback(_this); - replacement.onload = canvasEl = null; - }; - replacement.src = canvasEl.toDataURL('image/png'); + // 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; } - return canvasEl; + 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 - * @param {Boolean} noTransform */ - _render: function(ctx, noTransform) { - var x, y, imageMargins = this._findMargins(), elementToDraw; - - x = (noTransform ? this.left : -this.width / 2); - y = (noTransform ? this.top : -this.height / 2); - - if (this.meetOrSlice === 'slice') { - ctx.beginPath(); - ctx.rect(x, y, this.width, this.height); - ctx.clip(); + _render: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + if (this.isMoving !== true && this.resizeFilter && this._needsResize()) { + this.applyResizeFilters(); } - - if (this.isMoving === false && this.resizeFilters.length && this._needsResize()) { - this._lastScaleX = this.scaleX; - this._lastScaleY = this.scaleY; - elementToDraw = this.applyFilters(null, this.resizeFilters, this._filteredEl || this._originalElement, true); - } - else { - elementToDraw = this._element; - } - elementToDraw && ctx.drawImage(elementToDraw, - x + imageMargins.marginX, - y + imageMargins.marginY, - imageMargins.width, - imageMargins.height - ); - this._stroke(ctx); - this._renderStroke(ctx); + this._renderPaintInOrder(ctx); }, /** - * @private, needed to check if image needs resize + * Paint the cached copy of the object on the target context. + * it will set the imageSmoothing for the draw operation + * @param {CanvasRenderingContext2D} ctx Context to render on */ - _needsResize: function() { - return (this.scaleX !== this._lastScaleX || this.scaleY !== this._lastScaleY); + drawCacheOnCanvas: function(ctx) { + fabric.util.setImageSmoothing(ctx, this.imageSmoothing); + fabric.Object.prototype.drawCacheOnCanvas.call(this, 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; + if (!elementToDraw) { + return; + } + var scaleX = this._filterScalingX, scaleY = this._filterScalingY, + w = this.width, h = this.height, min = Math.min, max = Math.max, + // crop values cannot be lesser than 0. + cropX = max(this.cropX, 0), cropY = max(this.cropY, 0), + elWidth = elementToDraw.naturalWidth || elementToDraw.width, + elHeight = elementToDraw.naturalHeight || elementToDraw.height, + sX = cropX * scaleX, + sY = cropY * scaleY, + // the width height cannot exceed element width/height, starting from the crop offset. + sW = min(w * scaleX, elWidth - sX), + sH = min(h * scaleY, elHeight - sY), + x = -w / 2, y = -h / 2, + maxDestW = min(w, elWidth / scaleX - cropX), + maxDestH = min(h, elHeight / scaleY - cropY); + + elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH); + }, + + /** + * needed to check if image needs resize * @private */ - _findMargins: function() { - var width = this.width, height = this.height, scales, - scale, marginX = 0, marginY = 0; - - if (this.alignX !== 'none' || this.alignY !== 'none') { - scales = [this.width / this._element.width, this.height / this._element.height]; - scale = this.meetOrSlice === 'meet' - ? Math.min.apply(null, scales) : Math.max.apply(null, scales); - width = this._element.width * scale; - height = this._element.height * scale; - if (this.alignX === 'Mid') { - marginX = (this.width - width) / 2; - } - if (this.alignX === 'Max') { - marginX = this.width - width; - } - if (this.alignY === 'Mid') { - marginY = (this.height - height) / 2; - } - if (this.alignY === 'Max') { - marginY = this.height - height; - } - } - return { - width: width, - height: height, - marginX: marginX, - marginY: marginY - }; + _needsResize: function() { + var scale = this.getTotalObjectScaling(); + return (scale.scaleX !== this._lastScaleX || scale.scaleY !== this._lastScaleY); }, /** * @private */ _resetWidthHeight: function() { - var element = this.getElement(); - - this.set('width', element.width); - this.set('height', element.height); + this.set(this.getOriginalSize()); }, /** @@ -19319,8 +21461,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @param {HTMLImageElement|String} element The element representing the image * @param {Object} [options] Options object */ - _initElement: function(element, options, callback) { - this.setElement(fabric.util.getById(element), callback, options); + _initElement: function(element, options) { + this.setElement(fabric.util.getById(element), options); fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); }, @@ -19332,9 +21474,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot options || (options = { }); this.setOptions(options); this._setWidthHeight(options); - if (this._element && this.crossOrigin) { - this._element.crossOrigin = this.crossOrigin; - } }, /** @@ -19355,21 +21494,81 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot /** * @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) { - this.width = 'width' in options - ? options.width - : (this.getElement() - ? this.getElement().width || 0 - : 0); - - this.height = 'height' in options - ? options.height - : (this.getElement() - ? this.getElement().height || 0 - : 0); + 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 + }; + } }); /** @@ -19392,17 +21591,21 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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) { - fabric.util.loadImage(object.src, function(img, error) { - if (error) { - callback && callback(null, error); + fabric.Image.fromObject = function(_object, callback) { + var object = fabric.util.object.clone(_object); + fabric.util.loadImage(object.src, function(img, isError) { + if (isError) { + callback && callback(null, true); return; } fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { object.filters = filters || []; - fabric.Image.prototype._initFilters.call(object, object.resizeFilters, function(resizeFilters) { - object.resizeFilters = resizeFilters || []; - return new fabric.Image(img, object, callback); + fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) { + object.resizeFilter = resizeFilters[0]; + fabric.util.enlivenObjectEnlivables(object, object, function () { + var image = new fabric.Image(img, object); + callback(image, false); + }); }); }); }, null, object.crossOrigin); @@ -19412,12 +21615,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * 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 {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument). Second argument is a boolean indicating if an error occurred or not. * @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)); + fabric.util.loadImage(url, function(img, isError) { + callback && callback(new fabric.Image(img, imgOptions), isError); }, null, imgOptions && imgOptions.crossOrigin); }; @@ -19428,46 +21631,25 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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(' ')); + fabric.SHARED_ATTRIBUTES.concat( + 'x y width height preserveAspectRatio xlink:href crossOrigin image-rendering'.split(' ') + ); /** * Returns {@link fabric.Image} instance from an SVG element * @static * @param {SVGElement} element Element to parse - * @param {Function} callback Callback to execute when fabric.Image object is created * @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), - preserveAR; - - if (parsedAttributes.preserveAspectRatio) { - preserveAR = fabric.util.parsePreserveAspectRatioAttribute(parsedAttributes.preserveAspectRatio); - extend(parsedAttributes, preserveAR); - } - + 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_ */ - /** - * Indicates that instances of this type are async - * @static - * @type Boolean - * @default - */ - fabric.Image.async = true; - - /** - * Indicates compression level used when generating PNG under Node (in applyFilters). Any of 0-9 - * @static - * @type Number - * @default - */ - fabric.Image.pngCompression = 1; - })(typeof exports !== 'undefined' ? exports : this); @@ -19478,7 +21660,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @return {Number} angle value */ _getAngleValueForStraighten: function() { - var angle = this.getAngle() % 360; + var angle = this.angle % 360; if (angle > 0) { return Math.round((angle - 1) / 90) * 90; } @@ -19491,8 +21673,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @chainable */ straighten: function() { - this.setAngle(this._getAngleValueForStraighten()); - return this; + return this.rotate(this._getAngleValueForStraighten()); }, /** @@ -19501,7 +21682,6 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot * @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 || { }; @@ -19511,24 +21691,20 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot onChange = callbacks.onChange || empty, _this = this; - fabric.util.animate({ + return fabric.util.animate({ + target: this, startValue: this.get('angle'), endValue: this._getAngleValueForStraighten(), duration: this.FX_DURATION, onChange: function(value) { - _this.setAngle(value); + _this.rotate(value); onChange(); }, onComplete: function() { _this.setCoords(); onComplete(); }, - onStart: function() { - _this.set('active', false); - } }); - - return this; } }); @@ -19542,7 +21718,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati */ straightenObject: function (object) { object.straighten(); - this.renderAll(); + this.requestRenderAll(); return this; }, @@ -19550,23 +21726,487 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati * 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.renderAll.bind(this) + return 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 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: { + + }, + + /** + * 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 texture 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. + * @param {Number} filterType gl.NEAREST or gl.LINEAR usually, webgl numeri constants + * @returns {WebGLTexture} + */ + createTexture: function(gl, width, height, textureImageSource, filterType) { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterType || gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterType || 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 || { }; /** @@ -19583,6 +22223,25 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag */ 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 @@ -19603,12 +22262,289 @@ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Imag } }, + /** + * 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 version ( 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() { - return { type: this.type }; + var object = { type: this.type }, mainP = this.mainParameter; + if (mainP) { + object[mainP] = this[mainP]; + } + return object; }, /** @@ -19633,7 +22569,167 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, + 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, mainly for non webgl scenario + * to save some calculation + * @type Boolean + * @default true + */ + 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; @@ -19646,10 +22742,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Brightness({ - * brightness: 200 + * brightness: 0.05 * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); */ filters.Brightness = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { @@ -19661,44 +22757,75 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { type: 'Brightness', /** - * Constructor - * @memberOf fabric.Image.filters.Brightness.prototype - * @param {Object} [options] Options object - * @param {Number} [options.brightness=0] Value to brighten the image up (-255..255) + * Fragment source for the brightness program */ - initialize: function(options) { - options = options || { }; - this.brightness = options.brightness || 0; - }, + 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' + + '}', /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to + * 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 */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - brightness = this.brightness; + brightness: 0, - for (var i = 0, len = data.length; i < len; i += 4) { - data[i] += brightness; - data[i + 1] += brightness; - data[i + 2] += brightness; + /** + * 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; } - - context.putImageData(imageData, 0, 0); }, /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance + * 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. */ - toObject: function() { - return extend(this.callSuper('toObject'), { - brightness: this.brightness - }); - } + 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); + }, }); /** @@ -19736,7 +22863,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * 0, -1, 0 ] * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); + * canvas.renderAll(); * @example Blur filter * var filter = new fabric.Image.filters.Convolute({ * matrix: [ 1/9, 1/9, 1/9, @@ -19744,7 +22872,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * 1/9, 1/9, 1/9 ] * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); + * canvas.renderAll(); * @example Emboss filter * var filter = new fabric.Image.filters.Convolute({ * matrix: [ 1, 1, 1, @@ -19752,7 +22881,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * -1, -1, -1 ] * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); + * canvas.renderAll(); * @example Emboss filter with opaqueness * var filter = new fabric.Image.filters.Convolute({ * opaque: true, @@ -19761,7 +22891,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * -1, -1, -1 ] * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); + * canvas.renderAll(); */ filters.Convolute = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { @@ -19772,6 +22903,158 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ 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 @@ -19779,73 +23062,112 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Boolean} [options.opaque=false] Opaque value (true/false) * @param {Array} [options.matrix] Filter matrix */ - initialize: function(options) { - options = options || { }; - this.opaque = options.opaque; - this.matrix = options.matrix || [ - 0, 0, 0, - 0, 1, 0, - 0, 0, 0 - ]; + + /** + * 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]; }, /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to + * 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. */ - applyTo: function(canvasEl) { - - var weights = this.matrix, - context = canvasEl.getContext('2d'), - pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - + 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), - src = pixels.data, - sw = pixels.width, - sh = pixels.height, - output = context.createImageData(sw, sh), + 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; + scx, scy, srcOff, wt, + x, y, cx, cy; - for (var y = 0; y < sh; y++) { - for (var x = 0; x < sw; x++) { + 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 (var cy = 0; cy < side; cy++) { - for (var cx = 0; cx < side; cx++) { + 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) { + if (scy < 0 || scy >= sh || scx < 0 || scx >= sw) { continue; } srcOff = (scy * sw + scx) * 4; wt = weights[cy * side + cx]; - r += src[srcOff] * wt; - g += src[srcOff + 1] * wt; - b += src[srcOff + 2] * wt; - a += src[srcOff + 3] * wt; + 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; - dst[dstOff + 3] = a + alphaFac * (255 - a); + if (!alphaFac) { + dst[dstOff + 3] = a; + } + else { + dst[dstOff + 3] = data[dstOff + 3]; + } } } + options.imageData = output; + }, - context.putImageData(output, 0, 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 { + 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); }, /** @@ -19872,91 +23194,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { })(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; - - /** - * GradientTransparency filter class - * @class fabric.Image.filters.GradientTransparency - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.GradientTransparency#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example - * var filter = new fabric.Image.filters.GradientTransparency({ - * threshold: 200 - * }); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - */ - // eslint-disable-next-line max-len - filters.GradientTransparency = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'GradientTransparency', - - /** - * Constructor - * @memberOf fabric.Image.filters.GradientTransparency.prototype - * @param {Object} [options] Options object - * @param {Number} [options.threshold=100] Threshold value - */ - initialize: function(options) { - options = options || { }; - this.threshold = options.threshold || 100; - }, - - /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - threshold = this.threshold, - total = data.length; - - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 3] = threshold + 255 * (total - i) / total; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - threshold: this.threshold - }); - } - }); - - /** - * 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.GradientTransparency} Instance of fabric.Image.filters.GradientTransparency - */ - fabric.Image.filters.GradientTransparency.fromObject = fabric.Image.filters.BaseFilter.fromObject; - -})(typeof exports !== 'undefined' ? exports : this); - - (function(global) { 'use strict'; @@ -19974,7 +23211,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @example * var filter = new fabric.Image.filters.Grayscale(); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); */ filters.Grayscale = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { @@ -19985,29 +23222,120 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ 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' + + '}', + }, + + /** - * Applies filter to canvas element - * @memberOf fabric.Image.filters.Grayscale.prototype - * @param {Object} canvasEl Canvas element to apply filter to + * Grayscale mode, between 'average', 'lightness', 'luminosity' + * @param {String} type + * @default */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - len = imageData.width * imageData.height * 4, - index = 0, - average; + mode: 'average', - while (index < len) { - average = (data[index] + data[index + 1] + data[index + 2]) / 3; - data[index] = average; - data[index + 1] = average; - data[index + 2] = average; - index += 4; + 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; } + }, - context.putImageData(imageData, 0, 0); - } + /** + * 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; + }, }); /** @@ -20017,11 +23345,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @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 = function(object, callback) { - object = object || { }; - object.type = 'Grayscale'; - return fabric.Image.filters.BaseFilter.fromObject(object, callback); - }; + fabric.Image.filters.Grayscale.fromObject = fabric.Image.filters.BaseFilter.fromObject; })(typeof exports !== 'undefined' ? exports : this); @@ -20054,25 +23378,76 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ type: 'Invert', - /** - * Applies filter to canvas element - * @memberOf fabric.Image.filters.Invert.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i; + 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' + + '}', - for (i = 0; i < iLen; i += 4) { + /** + * 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]; } + }, - context.putImageData(imageData, 0, 0); - } + /** + * 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); + }, }); /** @@ -20082,120 +23457,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @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 = function(object, callback) { - object = object || { }; - object.type = 'Invert'; - return fabric.Image.filters.BaseFilter.fromObject(object, callback); - }; + 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; - - /** - * Mask filter class - * See http://resources.aleph-1.com/mask/ - * @class fabric.Image.filters.Mask - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Mask#initialize} for constructor definition - */ - filters.Mask = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Mask', - - /** - * Constructor - * @memberOf fabric.Image.filters.Mask.prototype - * @param {Object} [options] Options object - * @param {fabric.Image} [options.mask] Mask image object - * @param {Number} [options.channel=0] Rgb channel (0, 1, 2 or 3) - */ - initialize: function(options) { - options = options || { }; - - this.mask = options.mask; - this.channel = [0, 1, 2, 3].indexOf(options.channel) > -1 ? options.channel : 0; - }, - - /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - if (!this.mask) { - return; - } - - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - maskEl = this.mask.getElement(), - maskCanvasEl = fabric.util.createCanvasElement(), - channel = this.channel, - i, - iLen = imageData.width * imageData.height * 4; - - maskCanvasEl.width = canvasEl.width; - maskCanvasEl.height = canvasEl.height; - - maskCanvasEl.getContext('2d').drawImage(maskEl, 0, 0, canvasEl.width, canvasEl.height); - - var maskImageData = maskCanvasEl.getContext('2d').getImageData(0, 0, canvasEl.width, canvasEl.height), - maskData = maskImageData.data; - - for (i = 0; i < iLen; i += 4) { - data[i + 3] = maskData[i + channel]; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - mask: this.mask.toObject(), - channel: this.channel - }); - } - }); - - /** - * Returns filter instance from an object representation - * @static - * @param {Object} object Object to create an instance from - * @param {Function} [callback] Callback to invoke when a mask filter instance is created - */ - fabric.Image.filters.Mask.fromObject = function(object, callback) { - fabric.util.loadImage(object.mask.src, function(img) { - object.mask = new fabric.Image(img, object.mask); - return fabric.Image.filters.BaseFilter.fromObject(object, callback); - }); - }; - - /** - * Indicates that instances of this type are async - * @static - * @type Boolean - * @default - */ - fabric.Image.filters.Mask.async = true; })(typeof exports !== 'undefined' ? exports : this); @@ -20221,7 +23484,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * noise: 700 * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); + * canvas.renderAll(); */ filters.Noise = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { @@ -20233,27 +23497,52 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { type: 'Noise', /** - * Constructor - * @memberOf fabric.Image.filters.Noise.prototype - * @param {Object} [options] Options object - * @param {Number} [options.noise=0] Noise value + * Fragment source for the noise program */ - initialize: function(options) { - options = options || { }; - this.noise = options.noise || 0; - }, + 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' + + '}', /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to + * Describe the property that is the filter parameter + * @param {String} m + * @default */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, + 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 (var i = 0, len = data.length; i < len; i += 4) { + for (i = 0, len = data.length; i < len; i += 4) { rand = (0.5 - Math.random()) * noise; @@ -20261,8 +23550,30 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { data[i + 1] += rand; data[i + 2] += rand; } + }, - context.putImageData(imageData, 0, 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 { + 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()); }, /** @@ -20293,7 +23604,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, filters = fabric.Image.filters, createClass = fabric.util.createClass; @@ -20309,7 +23619,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * blocksize: 8 * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); */ filters.Pixelate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { @@ -20320,28 +23630,44 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ type: 'Pixelate', - /** - * Constructor - * @memberOf fabric.Image.filters.Pixelate.prototype - * @param {Object} [options] Options object - * @param {Number} [options.blocksize=4] Blocksize for pixelate - */ - initialize: function(options) { - options = options || { }; - this.blocksize = options.blocksize || 4; - }, + blocksize: 4, + + mainParameter: 'blocksize', /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to + * Fragment source for the Pixelate program */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + 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; + 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) { @@ -20353,18 +23679,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { b = data[index + 2]; a = data[index + 3]; - /* - blocksize: 4 - - [1,x,x,x,1] - [x,x,x,x,1] - [x,x,x,x,1] - [x,x,x,x,1] - [1,1,1,1,1] - */ - - for (var _i = i, _ilen = i + this.blocksize; _i < _ilen; _i++) { - for (var _j = j, _jlen = j + this.blocksize; _j < _jlen; _j++) { + _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; @@ -20374,19 +23692,38 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { } } } - - context.putImageData(imageData, 0, 0); }, /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance + * 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. */ - toObject: function() { - return extend(this.callSuper('toObject'), { - blocksize: this.blocksize - }); - } + 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); + }, }); /** @@ -20412,72 +23749,144 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Remove white filter class - * @class fabric.Image.filters.RemoveWhite + * @class fabric.Image.filters.RemoveColor * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.RemoveWhite#initialize} for constructor definition + * @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.RemoveWhite({ - * threshold: 40, - * distance: 140 + * var filter = new fabric.Image.filters.RemoveColor({ + * threshold: 0.2, * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); + * canvas.renderAll(); */ - filters.RemoveWhite = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveWhite.prototype */ { + filters.RemoveColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.RemoveColor.prototype */ { /** * Filter type * @param {String} type * @default */ - type: 'RemoveWhite', + 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.threshold=30] Threshold value - * @param {Number} [options.distance=20] Distance value + * @param {Number} [options.color=#RRGGBB] Threshold value + * @param {Number} [options.distance=10] Distance value */ - initialize: function(options) { - options = options || { }; - this.threshold = options.threshold || 30; - this.distance = options.distance || 20; - }, /** * Applies filter to canvas element * @param {Object} canvasEl Canvas element to apply filter to */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - threshold = this.threshold, - distance = this.distance, - limit = 255 - threshold, - abs = Math.abs, - r, g, b; + 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 (var i = 0, len = data.length; i < len; i += 4) { + + for (i = 0; i < data.length; i += 4) { r = data[i]; g = data[i + 1]; b = data[i + 2]; - if (r > limit && - g > limit && - b > limit && - abs(r - g) < distance && - abs(r - b) < distance && - abs(g - b) < distance - ) { + if (r > lowC[0] && + g > lowC[1] && + b > lowC[2] && + r < highC[0] && + g < highC[1] && + b < highC[2]) { data[i + 3] = 0; } } + }, - context.putImageData(imageData, 0, 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); }, /** @@ -20486,7 +23895,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ toObject: function() { return extend(this.callSuper('toObject'), { - threshold: this.threshold, + color: this.color, distance: this.distance }); } @@ -20497,9 +23906,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @param {Object} object Object to create an instance from * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.RemoveWhite} Instance of fabric.Image.filters.RemoveWhite + * @return {fabric.Image.filters.RemoveColor} Instance of fabric.Image.filters.RemoveWhite */ - fabric.Image.filters.RemoveWhite.fromObject = fabric.Image.filters.BaseFilter.fromObject; + fabric.Image.filters.RemoveColor.fromObject = fabric.Image.filters.BaseFilter.fromObject; })(typeof exports !== 'undefined' ? exports : this); @@ -20512,339 +23921,82 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { filters = fabric.Image.filters, createClass = fabric.util.createClass; - /** - * Sepia filter class - * @class fabric.Image.filters.Sepia - * @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.Sepia(); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - */ - filters.Sepia = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Sepia', - - /** - * Applies filter to canvas element - * @memberOf fabric.Image.filters.Sepia.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, avg; - - for (i = 0; i < iLen; i += 4) { - avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; - data[i] = avg + 100; - data[i + 1] = avg + 50; - data[i + 2] = avg + 255; - } - - context.putImageData(imageData, 0, 0); - } - }); - - /** - * 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.Sepia} Instance of fabric.Image.filters.Sepia - */ - fabric.Image.filters.Sepia.fromObject = function(object, callback) { - object = object || { }; - object.type = 'Sepia'; - return new fabric.Image.filters.BaseFilter.fromObject(object, callback); + 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, + ] }; -})(typeof exports !== 'undefined' ? exports : this); + for (var key in matrices) { + filters[key] = createClass(filters.ColorMatrix, /** @lends fabric.Image.filters.Sepia.prototype */ { + /** + * Filter type + * @param {String} type + * @default + */ + type: key, -(function(global) { + /** + * 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], - 'use strict'; - - var fabric = global.fabric || (global.fabric = { }), - filters = fabric.Image.filters, - createClass = fabric.util.createClass; - - /** - * Sepia2 filter class - * @class fabric.Image.filters.Sepia2 - * @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.Sepia2(); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - */ - filters.Sepia2 = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Sepia2', - - /** - * Applies filter to canvas element - * @memberOf fabric.Image.filters.Sepia.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, r, g, b; - - for (i = 0; i < iLen; i += 4) { - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - - data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; - data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; - data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; - } - - context.putImageData(imageData, 0, 0); - } - }); - - /** - * 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.Sepia2} Instance of fabric.Image.filters.Sepia2 - */ - fabric.Image.filters.Sepia2.fromObject = function(object, callback) { - object = object || { }; - object.type = 'Sepia2'; - return new fabric.Image.filters.BaseFilter.fromObject(object, callback); - }; - -})(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; - - /** - * Tint filter class - * Adapted from https://github.com/mezzoblue/PaintbrushJS - * @class fabric.Image.filters.Tint - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Tint#initialize} for constructor definition - * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} - * @example Tint filter with hex color and opacity - * var filter = new fabric.Image.filters.Tint({ - * color: '#3513B0', - * opacity: 0.5 - * }); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - * @example Tint filter with rgba color - * var filter = new fabric.Image.filters.Tint({ - * color: 'rgba(53, 21, 176, 0.5)' - * }); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - */ - filters.Tint = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Tint', - - /** - * Constructor - * @memberOf fabric.Image.filters.Tint.prototype - * @param {Object} [options] Options object - * @param {String} [options.color=#000000] Color to tint the image with - * @param {Number} [options.opacity] Opacity value that controls the tint effect's transparency (0..1) - */ - initialize: function(options) { - options = options || { }; - - this.color = options.color || '#000000'; - this.opacity = typeof options.opacity !== 'undefined' - ? options.opacity - : new fabric.Color(this.color).getAlpha(); - }, - - /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, - tintR, tintG, tintB, - r, g, b, alpha1, - source; - - source = new fabric.Color(this.color).getSource(); - - tintR = source[0] * this.opacity; - tintG = source[1] * this.opacity; - tintB = source[2] * this.opacity; - - alpha1 = 1 - this.opacity; - - for (i = 0; i < iLen; i += 4) { - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - - // alpha compositing - data[i] = tintR + r * alpha1; - data[i + 1] = tintG + g * alpha1; - data[i + 2] = tintB + b * alpha1; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - color: this.color, - opacity: this.opacity - }); - } - }); - - /** - * 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.Tint} Instance of fabric.Image.filters.Tint - */ - fabric.Image.filters.Tint.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; - - /** - * Multiply filter class - * Adapted from http://www.laurenscorijn.com/articles/colormath-basics - * @class fabric.Image.filters.Multiply - * @memberOf fabric.Image.filters - * @extends fabric.Image.filters.BaseFilter - * @example Multiply filter with hex color - * var filter = new fabric.Image.filters.Multiply({ - * color: '#F0F' - * }); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - * @example Multiply filter with rgb color - * var filter = new fabric.Image.filters.Multiply({ - * color: 'rgb(53, 21, 176)' - * }); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); - */ - filters.Multiply = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Multiply.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'Multiply', - - /** - * Constructor - * @memberOf fabric.Image.filters.Multiply.prototype - * @param {Object} [options] Options object - * @param {String} [options.color=#000000] Color to multiply the image pixels with - */ - initialize: function(options) { - options = options || { }; - - this.color = options.color || '#000000'; - }, - - /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, - source; - - source = new fabric.Color(this.color).getSource(); - - for (i = 0; i < iLen; i += 4) { - data[i] *= source[0] / 255; - data[i + 1] *= source[1] / 255; - data[i + 2] *= source[2] / 255; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - color: this.color - }); - } - }); - - /** - * 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.Multiply} Instance of fabric.Image.filters.Multiply - */ - fabric.Image.filters.Multiply.fromObject = fabric.Image.filters.BaseFilter.fromObject; + /** + * 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); @@ -20857,80 +24009,142 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Color Blend filter class - * @class fabric.Image.filter.Blend + * @class fabric.Image.filter.BlendColor * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @example - * var filter = new fabric.Image.filters.Blend({ + * var filter = new fabric.Image.filters.BlendColor({ * color: '#000', * mode: 'multiply' * }); * - * var filter = new fabric.Image.filters.Blend({ + * var filter = new fabric.Image.filters.BlendImage({ * image: fabricImageObject, * mode: 'multiply', * alpha: 0.5 * }); - * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); + * canvas.renderAll(); */ - filters.Blend = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { - type: 'Blend', + filters.BlendColor = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { + type: 'BlendColor', - initialize: function(options) { - options = options || {}; - this.color = options.color || '#000'; - this.image = options.image || false; - this.mode = options.mode || 'multiply'; - this.alpha = options.alpha || 1; + /** + * Color to make the blend operation with. default to a reddish color since black or white + * gives always strong result. + * @type String + * @default + **/ + color: '#F95C63', + + /** + * Blend mode for the filter: one of multiply, add, diff, screen, subtract, + * darken, lighten, overlay, exclusion, tint. + * @type String + * @default + **/ + mode: 'multiply', + + /** + * alpha value. represent the strength of the blend color operation. + * @type Number + * @default + **/ + 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', }, - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, + /** + * 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, - _r, _g, _b, - source, - isImage = false; + source, alpha1 = 1 - this.alpha; - if (this.image) { - // Blend images - isImage = true; + source = new fabric.Color(this.color).getSource(); + tr = source[0] * this.alpha; + tg = source[1] * this.alpha; + tb = source[2] * this.alpha; - var _el = fabric.util.createCanvasElement(); - _el.width = this.image.width; - _el.height = this.image.height; - - var tmpCanvas = new fabric.StaticCanvas(_el); - tmpCanvas.add(this.image); - var context2 = tmpCanvas.getContext('2d'); - source = context2.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height).data; - } - else { - // Blend color - 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, len = data.length; i < len; i += 4) { + for (var i = 0; i < iLen; i += 4) { r = data[i]; g = data[i + 1]; b = data[i + 2]; - if (isImage) { - tr = source[i] * this.alpha; - tg = source[i + 1] * this.alpha; - tb = source[i + 2] * this.alpha; - } - switch (this.mode) { case 'multiply': data[i] = r * tr / 255; @@ -20938,14 +24152,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { data[i + 2] = b * tb / 255; break; case 'screen': - data[i] = 1 - (1 - r) * (1 - tr); - data[i + 1] = 1 - (1 - g) * (1 - tg); - data[i + 2] = 1 - (1 - b) * (1 - tb); + 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] = Math.min(255, r + tr); - data[i + 1] = Math.min(255, g + tg); - data[i + 2] = Math.min(255, b + tb); + data[i] = r + tr; + data[i + 1] = g + tg; + data[i + 2] = b + tb; break; case 'diff': case 'difference': @@ -20954,13 +24168,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { data[i + 2] = Math.abs(b - tb); break; case 'subtract': - _r = r - tr; - _g = g - tg; - _b = b - tb; - - data[i] = (_r < 0) ? 0 : _r; - data[i + 1] = (_g < 0) ? 0 : _g; - data[i + 2] = (_b < 0) ? 0 : _b; + data[i] = r - tr; + data[i + 1] = g - tg; + data[i + 2] = b - tb; break; case 'darken': data[i] = Math.min(r, tr); @@ -20972,10 +24182,49 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 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; } } + }, - context.putImageData(imageData, 0, 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 { + 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); }, /** @@ -20984,8 +24233,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ toObject: function() { return { + type: this.type, color: this.color, - image: this.image, mode: this.mode, alpha: this.alpha }; @@ -20997,9 +24246,258 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @param {Object} object Object to create an instance from * @param {function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Blend} Instance of fabric.Image.filters.Blend + * @return {fabric.Image.filters.BlendColor} Instance of fabric.Image.filters.BlendColor */ - fabric.Image.filters.Blend.fromObject = fabric.Image.filters.BaseFilter.fromObject; + 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", "mask") + * @type String + * @default + **/ + 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); @@ -21009,7 +24507,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor, - sqrt = Math.sqrt, abs = Math.abs, max = Math.max, round = Math.round, sin = Math.sin, + sqrt = Math.sqrt, abs = Math.abs, round = Math.round, sin = Math.sin, ceil = Math.ceil, filters = fabric.Image.filters, createClass = fabric.util.createClass; @@ -21036,6 +24534,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Resize type + * for webgl resizeType is just lanczos, for canvas2d can be: + * bilinear, hermite, sliceHack, lanczos. * @param {String} resizeType * @default */ @@ -21046,22 +24546,169 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Number} scaleX * @default */ - scaleX: 0, + scaleX: 1, /** * Scale factor for resizing, y axis * @param {Number} scaleY * @default */ - scaleY: 0, + scaleY: 1, /** - * LanczosLobes parameter for lanczos filter + * 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 @@ -21069,33 +24716,31 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Number} scaleX * @param {Number} scaleY */ - applyTo: function(canvasEl, scaleX, scaleY) { - if (scaleX === 1 && scaleY === 1) { - return; - } + applyTo2d: function(options) { + var imageData = options.imageData, + scaleX = this.scaleX, + scaleY = this.scaleY; this.rcpScaleX = 1 / scaleX; this.rcpScaleY = 1 / scaleY; - var oW = canvasEl.width, oH = canvasEl.height, + var oW = imageData.width, oH = imageData.height, dW = round(oW * scaleX), dH = round(oH * scaleY), - imageData; + newData; if (this.resizeType === 'sliceHack') { - imageData = this.sliceByTwo(canvasEl, oW, oH, dW, dH); + newData = this.sliceByTwo(options, oW, oH, dW, dH); } - if (this.resizeType === 'hermite') { - imageData = this.hermiteFastResize(canvasEl, oW, oH, dW, dH); + else if (this.resizeType === 'hermite') { + newData = this.hermiteFastResize(options, oW, oH, dW, dH); } - if (this.resizeType === 'bilinear') { - imageData = this.bilinearFiltering(canvasEl, oW, oH, dW, dH); + else if (this.resizeType === 'bilinear') { + newData = this.bilinearFiltering(options, oW, oH, dW, dH); } - if (this.resizeType === 'lanczos') { - imageData = this.lanczosResize(canvasEl, oW, oH, dW, dH); + else if (this.resizeType === 'lanczos') { + newData = this.lanczosResize(options, oW, oH, dW, dH); } - canvasEl.width = dW; - canvasEl.height = dH; - canvasEl.getContext('2d').putImageData(imageData, 0, 0); + options.imageData = newData; }, /** @@ -21107,53 +24752,49 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Number} dH Destination Height * @returns {ImageData} */ - sliceByTwo: function(canvasEl, oW, oH, dW, dH) { - var context = canvasEl.getContext('2d'), imageData, - multW = 0.5, multH = 0.5, signW = 1, signH = 1, - doneW = false, doneH = false, stepW = oW, stepH = oH, - tmpCanvas = fabric.util.createCanvasElement(), - tmpCtx = tmpCanvas.getContext('2d'); + 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); - tmpCanvas.width = max(dW, oW); - tmpCanvas.height = max(dH, oH); - - if (dW > oW) { - multW = 2; - signW = -1; - } - if (dH > oH) { - multH = 2; - signH = -1; - } - imageData = context.getImageData(0, 0, oW, oH); - canvasEl.width = max(dW, oW); - canvasEl.height = max(dH, oH); - context.putImageData(imageData, 0, 0); while (!doneW || !doneH) { oW = stepW; oH = stepH; - if (dW * signW < floor(stepW * multW * signW)) { - stepW = floor(stepW * multW); + if (dW < floor(stepW * mult)) { + stepW = floor(stepW * mult); } else { stepW = dW; doneW = true; } - if (dH * signH < floor(stepH * multH * signH)) { - stepH = floor(stepH * multH); + if (dH < floor(stepH * mult)) { + stepH = floor(stepH * mult); } else { stepH = dH; doneH = true; } - imageData = context.getImageData(0, 0, oW, oH); - tmpCtx.putImageData(imageData, 0, 0); - context.clearRect(0, 0, stepW, stepH); - context.drawImage(tmpCanvas, 0, 0, oW, oH, 0, 0, stepW, stepH); + ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH); + sX = dX; + sY = dY; + dY += stepH; } - return context.getImageData(0, 0, dW, dH); + return ctx.getImageData(sX, sY, dW, dH); }, /** @@ -21165,21 +24806,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Number} dH Destination Height * @returns {ImageData} */ - lanczosResize: function(canvasEl, oW, oH, dW, dH) { - - function lanczosCreate(lobes) { - return function(x) { - if (x > lobes) { - return 0; - } - x *= Math.PI; - if (abs(x) < 1e-16) { - return 1; - } - var xx = x / lobes; - return sin(x) * sin(xx) / x / xx; - }; - } + lanczosResize: function(options, oW, oH, dW, dH) { function process(u) { var v, i, weight, idx, a, red, green, @@ -21232,11 +24859,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { } } - var context = canvasEl.getContext('2d'), - srcImg = context.getImageData(0, 0, oW, oH), - destImg = context.getImageData(0, 0, dW, dH), - srcData = srcImg.data, destData = destImg.data, - lanczos = lanczosCreate(this.lanczosLobes), + 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), @@ -21255,12 +24881,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Number} dH Destination Height * @returns {ImageData} */ - bilinearFiltering: function(canvasEl, oW, oH, dW, dH) { + 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, context = canvasEl.getContext('2d'), - w4 = 4 * (oW - 1), img = context.getImageData(0, 0, oW, oH), - pixels = img.data, destImage = context.getImageData(0, 0, dW, dH), + 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++) { @@ -21293,13 +24919,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Number} dH Destination Height * @returns {ImageData} */ - hermiteFastResize: function(canvasEl, oW, oH, dW, dH) { + hermiteFastResize: function(options, oW, oH, dW, dH) { var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, ratioWHalf = ceil(ratioW / 2), ratioHHalf = ceil(ratioH / 2), - context = canvasEl.getContext('2d'), - img = context.getImageData(0, 0, oW, oH), data = img.data, - img2 = context.getImageData(0, 0, dW, dH), data2 = img2.data; + 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, @@ -21374,116 +24999,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - 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(canvas.renderAll.bind(canvas)); - */ - filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ { - - /** - * Filter type - * @param {String} type - * @default - */ - type: 'ColorMatrix', - - /** - * Constructor - * @memberOf fabric.Image.filters.ColorMatrix.prototype - * @param {Object} [options] Options object - * @param {Array} [options.matrix] Color Matrix to modify the image data with - */ - initialize: function( options ) { - options || ( options = {} ); - this.matrix = options.matrix || [ - 1, 0, 0, 0, 0, - 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0 - ]; - }, - - /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function( canvasEl ) { - var context = canvasEl.getContext( '2d' ), - imageData = context.getImageData( 0, 0, canvasEl.width, canvasEl.height ), - data = imageData.data, - iLen = data.length, - i, - r, - g, - b, - a, - m = this.matrix; - - for ( i = 0; i < iLen; i += 4 ) { - r = data[ i ]; - g = data[ i + 1 ]; - b = data[ i + 2 ]; - a = data[ i + 3 ]; - - data[ i ] = r * m[ 0 ] + g * m[ 1 ] + b * m[ 2 ] + a * m[ 3 ] + m[ 4 ]; - data[ i + 1 ] = r * m[ 5 ] + g * m[ 6 ] + b * m[ 7 ] + a * m[ 8 ] + m[ 9 ]; - data[ i + 2 ] = r * m[ 10 ] + g * m[ 11 ] + b * m[ 12 ] + a * m[ 13 ] + m[ 14 ]; - data[ i + 3 ] = r * m[ 15 ] + g * m[ 16 ] + b * m[ 17 ] + a * m[ 18 ] + m[ 19 ]; - } - - context.putImageData( imageData, 0, 0 ); - }, - - /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - type: this.type, - matrix: this.matrix - }); - } - }); - - /** - * 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 = { }), - extend = fabric.util.object.extend, filters = fabric.Image.filters, createClass = fabric.util.createClass; @@ -21496,10 +25011,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Contrast({ - * contrast: 40 + * contrast: 0.25 * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); */ filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ { @@ -21510,45 +25025,76 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ 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 value, range from -1 to 1. + * @param {Number} contrast + * @default 0 + */ + 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 (-255...255) + * @param {Number} [options.contrast=0] Value to contrast the image up (-1...1) */ - initialize: function(options) { - options = options || { }; - this.contrast = options.contrast || 0; - }, /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - contrastF = 259 * (this.contrast + 255) / (255 * (259 - this.contrast)); + * 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 (var i = 0, len = data.length; i < len; i += 4) { + 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; } - - context.putImageData(imageData, 0, 0); }, /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance + * 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. */ - toObject: function() { - return extend(this.callSuper('toObject'), { - contrast: this.contrast - }); - } + 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); + }, }); /** @@ -21568,73 +25114,108 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, filters = fabric.Image.filters, createClass = fabric.util.createClass; /** * Saturate filter class - * @class fabric.Image.filters.Saturate + * @class fabric.Image.filters.Saturation * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter - * @see {@link fabric.Image.filters.Saturate#initialize} for constructor definition + * @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.Saturate({ - * saturate: 100 + * var filter = new fabric.Image.filters.Saturation({ + * saturation: 1 * }); * object.filters.push(filter); - * object.applyFilters(canvas.renderAll.bind(canvas)); + * object.applyFilters(); */ - filters.Saturate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturate.prototype */ { + filters.Saturation = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturation.prototype */ { /** * Filter type * @param {String} type * @default */ - type: 'Saturate', + 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 value, from -1 to 1. + * Increases/decreases the color saturation. + * A value of 0 has no effect. + * + * @param {Number} saturation + * @default + */ + 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 (-100...100) + * @param {Number} [options.saturate=0] Value to saturate the image (-1...1) */ - initialize: function(options) { - options = options || { }; - this.saturate = options.saturate || 0; - }, /** - * Applies filter to canvas element - * @param {Object} canvasEl Canvas element to apply filter to + * 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. */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - max, adjust = -this.saturate * 0.01; + applyTo2d: function(options) { + if (this.saturation === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.saturation, i, max; - for (var i = 0, len = data.length; i < len; i += 4) { + 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; } - - context.putImageData(imageData, 0, 0); }, /** - * Returns object representation of an instance - * @return {Object} Object representation of an instance + * 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. */ - toObject: function() { - return extend(this.callSuper('toObject'), { - saturate: this.saturate - }); - } + 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); + }, }); /** @@ -21642,9 +25223,673 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @param {Object} object Object to create an instance from * @param {Function} [callback] to be invoked after filter creation - * @return {fabric.Image.filters.Saturate} Instance of fabric.Image.filters.Saturate + * @return {fabric.Image.filters.Saturation} Instance of fabric.Image.filters.Saturate */ - fabric.Image.filters.Saturate.fromObject = fabric.Image.filters.BaseFilter.fromObject; + 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; + + /** + * Vibrance filter class + * @class fabric.Image.filters.Vibrance + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Vibrance#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Vibrance({ + * vibrance: 1 + * }); + * object.filters.push(filter); + * object.applyFilters(); + */ + filters.Vibrance = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Vibrance.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Vibrance', + + fragmentSource: 'precision highp float;\n' + + 'uniform sampler2D uTexture;\n' + + 'uniform float uVibrance;\n' + + 'varying vec2 vTexCoord;\n' + + 'void main() {\n' + + 'vec4 color = texture2D(uTexture, vTexCoord);\n' + + 'float max = max(color.r, max(color.g, color.b));\n' + + 'float avg = (color.r + color.g + color.b) / 3.0;\n' + + 'float amt = (abs(max - avg) * 2.0) * uVibrance;\n' + + 'color.r += max != color.r ? (max - color.r) * amt : 0.00;\n' + + 'color.g += max != color.g ? (max - color.g) * amt : 0.00;\n' + + 'color.b += max != color.b ? (max - color.b) * amt : 0.00;\n' + + 'gl_FragColor = color;\n' + + '}', + + /** + * Vibrance value, from -1 to 1. + * Increases/decreases the saturation of more muted colors with less effect on saturated colors. + * A value of 0 has no effect. + * + * @param {Number} vibrance + * @default + */ + vibrance: 0, + + mainParameter: 'vibrance', + + /** + * Constructor + * @memberOf fabric.Image.filters.Vibrance.prototype + * @param {Object} [options] Options object + * @param {Number} [options.vibrance=0] Vibrance value for the image (between -1 and 1) + */ + + /** + * Apply the Vibrance 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.vibrance === 0) { + return; + } + var imageData = options.imageData, + data = imageData.data, len = data.length, + adjust = -this.vibrance, i, max, avg, amt; + + for (i = 0; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + amt = ((Math.abs(max - avg) * 2 / 255) * adjust); + data[i] += max !== data[i] ? (max - data[i]) * amt : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * amt : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * amt : 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 { + uVibrance: gl.getUniformLocation(program, 'uVibrance'), + }; + }, + + /** + * 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.uVibrance, -this.vibrance); + }, + }); + + /** + * 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.Vibrance} Instance of fabric.Image.filters.Vibrance + */ + fabric.Image.filters.Vibrance.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 between 0 and 1. + * @type Number + * @default + */ + 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({ + * gamma: [1, 0.5, 2.1] + * }); + * 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); @@ -21654,43 +25899,18 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), - toFixed = fabric.util.toFixed, - NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, - MIN_TEXT_WIDTH = 2; + clone = fabric.util.object.clone; if (fabric.Text) { fabric.warn('fabric.Text is already defined'); return; } - var stateProperties = fabric.Object.prototype.stateProperties.concat(); - stateProperties.push( - 'fontFamily', - 'fontWeight', - 'fontSize', - 'text', - 'textDecoration', - 'textAlign', - 'fontStyle', - 'lineHeight', - 'textBackgroundColor', - 'charSpacing' - ); + var additionalProps = + ('fontFamily fontWeight fontSize text underline overline linethrough' + + ' textAlign fontStyle lineHeight textBackgroundColor charSpacing styles' + + ' direction path pathStartOffset pathSide pathAlign').split(' '); - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push( - 'fontFamily', - 'fontWeight', - 'fontSize', - 'text', - 'textDecoration', - 'textAlign', - 'fontStyle', - 'lineHeight', - 'textBackgroundColor', - 'charSpacing', - 'styles' - ); /** * Text class * @class fabric.Text @@ -21703,7 +25923,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * Properties which when set cause object to change dimensions - * @type Object + * @type Array * @private */ _dimensionAffectingProps: [ @@ -21714,7 +25934,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { 'lineHeight', 'text', 'charSpacing', - 'textAlign' + 'textAlign', + 'styles', + 'path', + 'pathStartOffset', + 'pathSide', + 'pathAlign' ], /** @@ -21722,166 +25947,26 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ _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 */ - _reSpacesAndTabs: /[ \t\r]+/g, + _reSpaceAndTab: /[ \t\r]/, /** - * Retrieves object's fontSize - * @method getFontSize - * @memberOf fabric.Text.prototype - * @return {String} Font size (in pixels) - */ - - /** - * Sets object's fontSize - * Does not update the object .width and .height, - * call ._initDimensions() to update the values. - * @method setFontSize - * @memberOf fabric.Text.prototype - * @param {Number} fontSize Font size (in pixels) - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's fontWeight - * @method getFontWeight - * @memberOf fabric.Text.prototype - * @return {(String|Number)} Font weight - */ - - /** - * Sets object's fontWeight - * Does not update the object .width and .height, - * call ._initDimensions() to update the values. - * @method setFontWeight - * @memberOf fabric.Text.prototype - * @param {(Number|String)} fontWeight Font weight - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's fontFamily - * @method getFontFamily - * @memberOf fabric.Text.prototype - * @return {String} Font family - */ - - /** - * Sets object's fontFamily - * Does not update the object .width and .height, - * call ._initDimensions() to update the values. - * @method setFontFamily - * @memberOf fabric.Text.prototype - * @param {String} fontFamily Font family - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's text - * @method getText - * @memberOf fabric.Text.prototype - * @return {String} text - */ - - /** - * Sets object's text - * Does not update the object .width and .height, - * call ._initDimensions() to update the values. - * @method setText - * @memberOf fabric.Text.prototype - * @param {String} text Text - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's textDecoration - * @method getTextDecoration - * @memberOf fabric.Text.prototype - * @return {String} Text decoration - */ - - /** - * Sets object's textDecoration - * @method setTextDecoration - * @memberOf fabric.Text.prototype - * @param {String} textDecoration Text decoration - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's fontStyle - * @method getFontStyle - * @memberOf fabric.Text.prototype - * @return {String} Font style - */ - - /** - * Sets object's fontStyle - * Does not update the object .width and .height, - * call ._initDimensions() to update the values. - * @method setFontStyle - * @memberOf fabric.Text.prototype - * @param {String} fontStyle Font style - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's lineHeight - * @method getLineHeight - * @memberOf fabric.Text.prototype - * @return {Number} Line height - */ - - /** - * Sets object's lineHeight - * @method setLineHeight - * @memberOf fabric.Text.prototype - * @param {Number} lineHeight Line height - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's textAlign - * @method getTextAlign - * @memberOf fabric.Text.prototype - * @return {String} Text alignment - */ - - /** - * Sets object's textAlign - * @method setTextAlign - * @memberOf fabric.Text.prototype - * @param {String} textAlign Text alignment - * @return {fabric.Text} - * @chainable - */ - - /** - * Retrieves object's textBackgroundColor - * @method getTextBackgroundColor - * @memberOf fabric.Text.prototype - * @return {String} Text background color - */ - - /** - * Sets object's textBackgroundColor - * @method setTextBackgroundColor - * @memberOf fabric.Text.prototype - * @param {String} textBackgroundColor Text background color - * @return {fabric.Text} - * @chainable + * 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 @@ -21912,14 +25997,29 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { fontFamily: 'Times New Roman', /** - * Text decoration Possible values: "", "underline", "overline" or "line-through". - * @type String + * Text decoration underline. + * @type Boolean * @default */ - textDecoration: '', + underline: false, /** - * Text alignment. Possible values: "left", "center", "right" or "justify". + * 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 */ @@ -21930,7 +26030,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @type String * @default */ - fontStyle: '', + fontStyle: 'normal', /** * Line height @@ -21939,6 +26039,26 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ 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 @@ -21952,13 +26072,13 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * as well as for history (undo/redo) purposes * @type Array */ - stateProperties: stateProperties, + stateProperties: fabric.Object.prototype.stateProperties.concat(additionalProps), /** * List of properties to consider when checking if cache needs refresh * @type Array */ - cacheProperties: cacheProperties, + cacheProperties: fabric.Object.prototype.cacheProperties.concat(additionalProps), /** * When defined, an object is rendered via stroke and this property specifies its color. @@ -21976,10 +26096,69 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ shadow: null, + /** + * fabric.Path that the text should follow. + * since 4.6.0 the path will be drawn automatically. + * if you want to make the path visible, give it a stroke and strokeWidth or fill value + * if you want it to be hidden, assign visible = false to the path. + * This feature is in BETA, and SVG import/export is not yet supported. + * @type fabric.Path + * @example + * var textPath = new fabric.Text('Text on a path', { + * top: 150, + * left: 150, + * textAlign: 'center', + * charSpacing: -50, + * path: new fabric.Path('M 0 0 C 50 -100 150 -100 200 0', { + * strokeWidth: 1, + * visible: false + * }), + * pathSide: 'left', + * pathStartOffset: 0 + * }); + * @default + */ + path: null, + + /** + * Offset amount for text path starting position + * Only used when text has a path + * @type Number + * @default + */ + pathStartOffset: 0, + + /** + * Which side of the path the text should be drawn on. + * Only used when text has a path + * @type {String} 'left|right' + * @default + */ + pathSide: 'left', + + /** + * How text is aligned to the path. This property determines + * the perpendicular position of each character relative to the path. + * (one of "baseline", "center", "ascender", "descender") + * This feature is in BETA, and its behavior may change + * @type String + * @default + */ + pathAlign: 'baseline', + /** * @private */ - _fontSizeFraction: 0.25, + _fontSizeFraction: 0.222, + + /** + * @private + */ + offsets: { + underline: 0.10, + linethrough: -0.315, + overline: -0.88 + }, /** * Text Line proportion to font Size (in pixels) @@ -21996,6 +26175,85 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ charSpacing: 0, + /** + * Object containing character styles - top-level properties -> line numbers, + * 2nd-level properties - character numbers + * @type Object + * @default + */ + styles: null, + + /** + * Reference to a context to measure text char or couple of chars + * the cacheContext of the canvas will be used or a freshly created one if the object is not on canvas + * once created it will be referenced on fabric._measuringContext to avoid creating a canvas for every + * text object created. + * @type {CanvasRenderingContext2D} + * @default + */ + _measuringContext: null, + + /** + * Baseline shift, styles only, keep at 0 for the main text object + * @type {Number} + * @default + */ + deltaY: 0, + + /** + * WARNING: EXPERIMENTAL. NOT SUPPORTED YET + * determine the direction of the text. + * This has to be set manually together with textAlign and originX for proper + * experience. + * some interesting link for the future + * https://www.w3.org/International/questions/qa-bidi-unicode-controls + * @since 4.5.0 + * @type {String} 'ltr|rtl' + * @default + */ + direction: 'ltr', + + /** + * Array of properties that define a style unit (of 'styles'). + * @type {Array} + * @default + */ + _styleProperties: [ + 'stroke', + 'strokeWidth', + 'fill', + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'underline', + 'overline', + 'linethrough', + 'deltaY', + 'textBackgroundColor', + ], + + /** + * contains characters bounding boxes + */ + __charBounds: [], + + /** + * use this size when measuring text. To avoid IE11 rounding errors + * @type {Number} + * @default + * @readonly + * @private + */ + CACHE_FONT_SIZE: 400, + + /** + * contains the min text width to avoid getting 0 + * @type {Number} + * @default + */ + MIN_TEXT_WIDTH: 2, + /** * Constructor * @param {String} text Text string @@ -22003,36 +26261,136 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @return {fabric.Text} thisArg */ initialize: function(text, options) { - options = options || { }; + this.styles = options ? (options.styles || { }) : { }; this.text = text; this.__skipDimension = true; this.callSuper('initialize', options); + if (this.path) { + this.setPathInfo(); + } this.__skipDimension = false; - this._initDimensions(); + this.initDimensions(); this.setCoords(); this.setupState({ propertySet: '_dimensionAffectingProps' }); }, /** - * Initialize text dimensions. Render all text on given context - * or on a offscreen canvas to get the text width with measureText. + * If text has a path, it will add the extra information needed + * for path and text calculations + * @return {fabric.Text} thisArg + */ + setPathInfo: function() { + var path = this.path; + if (path) { + path.segmentsInfo = fabric.util.getPathSegmentsInfo(path.path); + } + }, + + /** + * Return a context for measurement of text string. + * if created it gets stored for reuse + * this is for internal use, please do not use it + * @private + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Text} thisArg + */ + getMeasuringContext: function() { + // if we did not return we have to measure something. + if (!fabric._measuringContext) { + fabric._measuringContext = this.canvas && this.canvas.contextCache || + fabric.util.createCanvasElement().getContext('2d'); + } + return fabric._measuringContext; + }, + + /** + * @private + * Divides text into lines of text and lines of graphemes. + */ + _splitText: function() { + var newLines = this._splitTextIntoLines(this.text); + this.textLines = newLines.lines; + this._textLines = newLines.graphemeLines; + this._unwrappedTextLines = newLines._unwrappedLines; + this._text = newLines.graphemeText; + return newLines; + }, + + /** + * Initialize or update text dimensions. * Updates this.width and this.height with the proper values. * Does not return dimensions. - * @param {CanvasRenderingContext2D} [ctx] Context to render on - * @private */ - _initDimensions: function(ctx) { + initDimensions: function() { if (this.__skipDimension) { return; } - if (!ctx) { - ctx = fabric.util.createCanvasElement().getContext('2d'); - this._setTextStyles(ctx); - } - this._textLines = this._splitTextIntoLines(); + this._splitText(); this._clearCache(); - this.width = this._getTextWidth(ctx) || this.cursorWidth || MIN_TEXT_WIDTH; - this.height = this._getTextHeight(ctx); + if (this.path) { + this.width = this.path.width; + this.height = this.path.height; + } + else { + this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH; + this.height = this.calcTextHeight(); + } + if (this.textAlign.indexOf('justify') !== -1) { + // once text is measured we need to make space fatter to make justified text. + this.enlargeSpaces(); + } + this.saveState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Enlarge space boxes and shift the others + */ + enlargeSpaces: function() { + var diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (this.textAlign !== 'justify' && (i === len - 1 || this.isEndOfWrapping(i))) { + continue; + } + accumulatedSpace = 0; + line = this._textLines[i]; + currentLineWidth = this.getLineWidth(i); + if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { + numberOfSpaces = spaces.length; + diffSpace = (this.width - currentLineWidth) / numberOfSpaces; + for (var j = 0, jlen = line.length; j <= jlen; j++) { + charBound = this.__charBounds[i][j]; + if (this._reSpaceAndTab.test(line[j])) { + charBound.width += diffSpace; + charBound.kernedWidth += diffSpace; + charBound.left += accumulatedSpace; + accumulatedSpace += diffSpace; + } + else { + charBound.left += accumulatedSpace; + } + } + } + } + }, + + /** + * Detect if the text line is ended with an hard break + * text and itext do not have wrapping, return false + * @return {Boolean} + */ + isEndOfWrapping: function(lineIndex) { + return lineIndex === this._textLines.length - 1; + }, + + /** + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * It return always for text and Itext. + * @return Number + */ + missingNewlineOffset: function() { + return 1; }, /** @@ -22048,17 +26406,19 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * 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 dim = this.callSuper('_getCacheCanvasDimensions'); - var fontSize = this.fontSize * 2; - dim.width += fontSize * dim.zoomX; - dim.height += fontSize * dim.zoomY; - return dim; + var dims = this.callSuper('_getCacheCanvasDimensions'); + var fontSize = this.fontSize; + dims.width += fontSize * dims.zoomX; + dims.height += fontSize * dims.zoomY; + return dims; }, /** @@ -22066,13 +26426,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { + var path = this.path; + path && !path.isNotVisible() && path._render(ctx); this._setTextStyles(ctx); - if (this.group && this.group.type === 'path-group') { - ctx.translate(this.left, this.top); - } this._renderTextLinesBackground(ctx); + this._renderTextDecoration(ctx, 'underline'); this._renderText(ctx); - this._renderTextDecoration(ctx); + this._renderTextDecoration(ctx, 'overline'); + this._renderTextDecoration(ctx, 'linethrough'); }, /** @@ -22080,37 +26441,55 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderText: function(ctx) { - this._renderTextFill(ctx); - this._renderTextStroke(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) { - ctx.textBaseline = 'alphabetic'; - ctx.font = this._getFontDeclaration(); - }, - - /** - * @private - * @return {Number} Height of fabric.Text object - */ - _getTextHeight: function() { - return this._getHeightOfSingleLine() + (this._textLines.length - 1) * this._getHeightOfLine(); + _setTextStyles: function(ctx, charStyle, forMeasuring) { + ctx.textBaseline = 'alphabetical'; + if (this.path) { + switch (this.pathAlign) { + case 'center': + ctx.textBaseline = 'middle'; + break; + case 'ascender': + ctx.textBaseline = 'top'; + break; + case 'descender': + ctx.textBaseline = 'bottom'; + break; + } + } + ctx.font = this._getFontDeclaration(charStyle, forMeasuring); }, /** + * calculate and return the text Width measuring each line. * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @return {Number} Maximum width of fabric.Text object */ - _getTextWidth: function(ctx) { - var maxWidth = this._getLineWidth(ctx, 0); + calcTextWidth: function() { + var maxWidth = this.getLineWidth(0); for (var i = 1, len = this._textLines.length; i < len; i++) { - var currentLineWidth = this._getLineWidth(ctx, i); + var currentLineWidth = this.getLineWidth(i); if (currentLineWidth > maxWidth) { maxWidth = currentLineWidth; } @@ -22118,41 +26497,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { return maxWidth; }, - /** - * @private - * @param {String} method Method name ("fillText" or "strokeText") - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} chars Chars to render - * @param {Number} left Left position of text - * @param {Number} top Top position of text - */ - _renderChars: function(method, ctx, chars, left, top) { - // remove Text word from method var - var shortM = method.slice(0, -4), _char, width; - if (this[shortM].toLive) { - var offsetX = -this.width / 2 + this[shortM].offsetX || 0, - offsetY = -this.height / 2 + this[shortM].offsetY || 0; - ctx.save(); - ctx.translate(offsetX, offsetY); - left -= offsetX; - top -= offsetY; - } - if (this.charSpacing !== 0) { - var additionalSpace = this._getWidthOfCharSpacing(); - chars = chars.split(''); - for (var i = 0, len = chars.length; i < len; i++) { - _char = chars[i]; - width = ctx.measureText(_char).width + additionalSpace; - ctx[method](_char, left, top); - left += width > 0 ? width : 0; - } - } - else { - ctx[method](chars, left, top); - } - this[shortM].toLive && ctx.restore(); - }, - /** * @private * @param {String} method Method name ("fillText" or "strokeText") @@ -22163,49 +26507,341 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Number} lineIndex Index of a line in a text */ _renderTextLine: function(method, ctx, line, left, top, lineIndex) { - // lift the line by quarter of fontSize - top -= this.fontSize * this._fontSizeFraction; + this._renderChars(method, ctx, line, left, top, lineIndex); + }, - // short-circuit - var lineWidth = this._getLineWidth(ctx, lineIndex); - if (this.textAlign !== 'justify' || this.width < lineWidth) { - this._renderChars(method, ctx, line, left, top, lineIndex); + /** + * Renders the text background for lines, taking care of style + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextLinesBackground: function(ctx) { + if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) { return; } + var heightOfLine, + lineLeftOffset, originalFill = ctx.fillStyle, + line, lastColor, + leftOffset = this._getLeftOffset(), + lineTopOffset = this._getTopOffset(), + boxStart = 0, boxWidth = 0, charBox, currentColor, path = this.path, + drawStart; - // stretch the line - var words = line.split(/\s+/), - charOffset = 0, - wordsWidth = this._getWidthOfWords(ctx, words.join(' '), lineIndex, 0), - widthDiff = this.width - wordsWidth, - numSpaces = words.length - 1, - spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, - leftOffset = 0, word; - - for (var i = 0, len = words.length; i < len; i++) { - while (line[charOffset] === ' ' && charOffset < line.length) { - charOffset++; + 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; } - word = words[i]; - this._renderChars(method, ctx, word, left + leftOffset, top, lineIndex, charOffset); - leftOffset += this._getWidthOfWords(ctx, word, lineIndex, charOffset) + spaceWidth; - charOffset += word.length; + line = this._textLines[i]; + lineLeftOffset = this._getLineLeftOffset(i); + boxWidth = 0; + boxStart = 0; + lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillStyle = currentColor; + currentColor && ctx.fillRect( + -charBox.width / 2, + -heightOfLine / this.lineHeight * (1 - this._fontSizeFraction), + charBox.width, + heightOfLine / this.lineHeight + ); + ctx.restore(); + } + else if (currentColor !== lastColor) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = lastColor; + lastColor && ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + boxStart = charBox.left; + boxWidth = charBox.width; + lastColor = currentColor; + } + else { + boxWidth += charBox.kernedWidth; + } + } + if (currentColor && !path) { + drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentColor; + ctx.fillRect( + drawStart, + lineTopOffset, + boxWidth, + heightOfLine / this.lineHeight + ); + } + lineTopOffset += heightOfLine; } + ctx.fillStyle = originalFill; + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); }, /** * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} word + * @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 */ - _getWidthOfWords: function (ctx, word) { - var width = ctx.measureText(word).width, charCount, additionalSpace; - if (this.charSpacing !== 0) { - charCount = word.split('').length; - additionalSpace = charCount * this._getWidthOfCharSpacing(); - width += additionalSpace; + getFontCache: function(decl) { + var fontFamily = decl.fontFamily.toLowerCase(); + if (!fabric.charWidthsCache[fontFamily]) { + fabric.charWidthsCache[fontFamily] = { }; } - return width > 0 ? width : 0; + var cache = fabric.charWidthsCache[fontFamily], + cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase(); + if (!cache[cacheProp]) { + cache[cacheProp] = { }; + } + return cache[cacheProp]; + }, + + /** + * measure and return the width of a single character. + * possibly overridden to accommodate different measure logic or + * to hook some external lib for character measurement + * @private + * @param {String} _char, char to be measured + * @param {Object} charStyle style of char to be measured + * @param {String} [previousChar] previous char + * @param {Object} [prevCharStyle] style of previous char + */ + _measureChar: function(_char, charStyle, previousChar, prevCharStyle) { + // first i try to return from cache + var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle), + previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char, + stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth, + fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth; + + if (previousChar && fontCache[previousChar] !== undefined) { + previousWidth = fontCache[previousChar]; + } + if (fontCache[_char] !== undefined) { + kernedWidth = width = fontCache[_char]; + } + if (stylesAreEqual && fontCache[couple] !== undefined) { + coupleWidth = fontCache[couple]; + kernedWidth = coupleWidth - previousWidth; + } + if (width === undefined || previousWidth === undefined || coupleWidth === undefined) { + var ctx = this.getMeasuringContext(); + // send a TRUE to specify measuring font size CACHE_FONT_SIZE + this._setTextStyles(ctx, charStyle, true); + } + if (width === undefined) { + kernedWidth = width = ctx.measureText(_char).width; + fontCache[_char] = width; + } + if (previousWidth === undefined && stylesAreEqual && previousChar) { + previousWidth = ctx.measureText(previousChar).width; + fontCache[previousChar] = previousWidth; + } + if (stylesAreEqual && coupleWidth === undefined) { + // we can measure the kerning couple and subtract the width of the previous character + coupleWidth = ctx.measureText(couple).width; + fontCache[couple] = coupleWidth; + kernedWidth = coupleWidth - previousWidth; + } + return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier }; + }, + + /** + * Computes height of character at given position + * @param {Number} line the line index number + * @param {Number} _char the character index number + * @return {Number} fontSize of the character + */ + getHeightOfChar: function(line, _char) { + return this.getValueOfPropertyAt(line, _char, 'fontSize'); + }, + + /** + * measure a text line measuring all characters. + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + measureLine: function(lineIndex) { + var lineInfo = this._measureLine(lineIndex); + if (this.charSpacing !== 0) { + lineInfo.width -= this._getWidthOfCharSpacing(); + } + if (lineInfo.width < 0) { + lineInfo.width = 0; + } + return lineInfo; + }, + + /** + * measure every grapheme of a line, populating __charBounds + * @param {Number} lineIndex + * @return {Object} object.width total width of characters + * @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs + */ + _measureLine: function(lineIndex) { + var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme, + graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length), + positionInPath = 0, startingPoint, totalPathLength, path = this.path, + reverse = this.pathSide === 'right'; + + this.__charBounds[lineIndex] = lineBounds; + for (i = 0; i < line.length; i++) { + grapheme = line[i]; + graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme); + lineBounds[i] = graphemeInfo; + width += graphemeInfo.kernedWidth; + prevGrapheme = grapheme; + } + // this latest bound box represent the last character of the line + // to simplify cursor handling in interactive mode. + lineBounds[i] = { + left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0, + width: 0, + kernedWidth: 0, + height: this.fontSize + }; + if (path) { + totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length; + startingPoint = fabric.util.getPointOnPath(path.path, 0, path.segmentsInfo); + startingPoint.x += path.pathOffset.x; + startingPoint.y += path.pathOffset.y; + switch (this.textAlign) { + case 'left': + positionInPath = reverse ? (totalPathLength - width) : 0; + break; + case 'center': + positionInPath = (totalPathLength - width) / 2; + break; + case 'right': + positionInPath = reverse ? 0 : (totalPathLength - width); + break; + //todo - add support for justify + } + positionInPath += this.pathStartOffset * (reverse ? -1 : 1); + for (i = reverse ? line.length - 1 : 0; + reverse ? i >= 0 : i < line.length; + reverse ? i-- : i++) { + graphemeInfo = lineBounds[i]; + if (positionInPath > totalPathLength) { + positionInPath %= totalPathLength; + } + else if (positionInPath < 0) { + positionInPath += totalPathLength; + } + // it would probably much faster to send all the grapheme position for a line + // and calculate path position/angle at once. + this._setGraphemeOnPath(positionInPath, graphemeInfo, startingPoint); + positionInPath += graphemeInfo.kernedWidth; + } + } + return { width: width, numOfSpaces: numOfSpaces }; + }, + + /** + * Calculate the angle and the left,top position of the char that follow a path. + * It appends it to graphemeInfo to be reused later at rendering + * @private + * @param {Number} positionInPath to be measured + * @param {Object} graphemeInfo current grapheme box information + * @param {Object} startingPoint position of the point + */ + _setGraphemeOnPath: function(positionInPath, graphemeInfo, startingPoint) { + var centerPosition = positionInPath + graphemeInfo.kernedWidth / 2, + path = this.path; + + // we are at currentPositionOnPath. we want to know what point on the path is. + var info = fabric.util.getPointOnPath(path.path, centerPosition, path.segmentsInfo); + graphemeInfo.renderLeft = info.x - startingPoint.x; + graphemeInfo.renderTop = info.y - startingPoint.y; + graphemeInfo.angle = info.angle + (this.pathSide === 'right' ? Math.PI : 0); + }, + + /** + * Measure and return the info of a single grapheme. + * needs the the info of previous graphemes already filled + * @private + * @param {String} grapheme to be measured + * @param {Number} lineIndex index of the line where the char is + * @param {Number} charIndex position in the line + * @param {String} [prevGrapheme] character preceding the one to be measured + */ + _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { + var style = this.getCompleteStyleDeclaration(lineIndex, charIndex), + prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { }, + info = this._measureChar(grapheme, style, prevGrapheme, prevStyle), + kernedWidth = info.kernedWidth, + width = info.width, charSpacing; + + if (this.charSpacing !== 0) { + charSpacing = this._getWidthOfCharSpacing(); + width += charSpacing; + kernedWidth += charSpacing; + } + + var box = { + width: width, + left: 0, + height: style.fontSize, + kernedWidth: kernedWidth, + deltaY: style.deltaY, + }; + if (charIndex > 0 && !skipLeft) { + var previousBox = this.__charBounds[lineIndex][charIndex - 1]; + box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width; + } + return box; + }, + + /** + * Calculate height of line at 'lineIndex' + * @param {Number} lineIndex index of line to calculate + * @return {Number} + */ + getHeightOfLine: function(lineIndex) { + if (this.__lineHeights[lineIndex]) { + return this.__lineHeights[lineIndex]; + } + + var line = this._textLines[lineIndex], + // char 0 is measured before the line cycle because it nneds to char + // emptylines + maxHeight = this.getHeightOfChar(lineIndex, 0); + for (var i = 1, len = line.length; i < len; i++) { + maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); + } + + return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; + }, + + /** + * Calculate text box height + */ + calcTextHeight: function() { + var lineHeight, height = 0; + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineHeight = this.getHeightOfLine(i); + height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); + } + return height; }, /** @@ -22213,7 +26849,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @return {Number} Left offset */ _getLeftOffset: function() { - return -this.width / 2; + return this.direction === 'ltr' ? -this.width / 2 : this.width / 2; }, /** @@ -22224,27 +26860,18 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { return -this.height / 2; }, - /** - * Returns true because text has no style - */ - isEmptyStyles: function() { - return true; - }, - /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} method Method name ("fillText" or "strokeText") */ _renderTextCommon: function(ctx, method) { - + ctx.save(); var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(); - for (var i = 0, len = this._textLines.length; i < len; i++) { - var heightOfLine = this._getHeightOfLine(ctx, i), + var heightOfLine = this.getHeightOfLine(i), maxHeight = heightOfLine / this.lineHeight, - lineWidth = this._getLineWidth(ctx, i), - leftOffset = this._getLineLeftOffset(lineWidth); + leftOffset = this._getLineLeftOffset(i); this._renderTextLine( method, ctx, @@ -22255,6 +26882,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { ); lineHeights += heightOfLine; } + ctx.restore(); }, /** @@ -22262,7 +26890,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextFill: function(ctx) { - if (!this.fill && this.isEmptyStyles()) { + if (!this.fill && !this.styleHas('fill')) { return; } @@ -22292,65 +26920,263 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * @private - * @return {Number} height of line - */ - _getHeightOfLine: function() { - return this._getHeightOfSingleLine() * this.lineHeight; - }, - - /** - * @private - * @return {Number} height of line without lineHeight - */ - _getHeightOfSingleLine: function() { - return this.fontSize * this._fontSizeMult; - }, - - /** - * @private + * @param {String} method fillText or strokeText. * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} line Content of the line, splitted in an array by grapheme + * @param {Number} left + * @param {Number} top + * @param {Number} lineIndex */ - _renderTextLinesBackground: function(ctx) { - if (!this.textBackgroundColor) { + _renderChars: function(method, ctx, line, left, top, lineIndex) { + // set proper line offset + var lineHeight = this.getHeightOfLine(lineIndex), + isJustify = this.textAlign.indexOf('justify') !== -1, + actualStyle, + nextStyle, + charsToRender = '', + charBox, + boxWidth = 0, + timeToRender, + path = this.path, + shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, + isLtr = this.direction === 'ltr', sign = this.direction === 'ltr' ? 1 : -1, + drawingLeft, currentDirection = ctx.canvas.getAttribute('dir'); + ctx.save(); + if (currentDirection !== this.direction) { + ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl'); + ctx.direction = isLtr ? 'ltr' : 'rtl'; + ctx.textAlign = isLtr ? 'left' : 'right'; + } + top -= lineHeight * this._fontSizeFraction / this.lineHeight; + if (shortCut) { + // render all the line in one pass without checking + // drawingLeft = isLtr ? left : left - this.getLineWidth(lineIndex); + this._renderChar(method, ctx, lineIndex, 0, line.join(''), left, top, lineHeight); + ctx.restore(); return; } - var lineTopOffset = 0, heightOfLine, - lineWidth, lineLeftOffset, originalFill = ctx.fillStyle; - - ctx.fillStyle = this.textBackgroundColor; - for (var i = 0, len = this._textLines.length; i < len; i++) { - heightOfLine = this._getHeightOfLine(ctx, i); - lineWidth = this._getLineWidth(ctx, i); - if (lineWidth > 0) { - lineLeftOffset = this._getLineLeftOffset(lineWidth); - ctx.fillRect( - this._getLeftOffset() + lineLeftOffset, - this._getTopOffset() + lineTopOffset, - lineWidth, - heightOfLine / this.lineHeight - ); + for (var i = 0, len = line.length - 1; i <= len; i++) { + timeToRender = i === len || this.charSpacing || path; + charsToRender += line[i]; + charBox = this.__charBounds[lineIndex][i]; + if (boxWidth === 0) { + left += sign * (charBox.kernedWidth - charBox.width); + boxWidth += charBox.width; + } + else { + boxWidth += charBox.kernedWidth; + } + if (isJustify && !timeToRender) { + if (this._reSpaceAndTab.test(line[i])) { + timeToRender = true; + } + } + if (!timeToRender) { + // if we have charSpacing, we render char by char + actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); + nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); + timeToRender = fabric.util.hasStyleChanged(actualStyle, nextStyle, false); + } + if (timeToRender) { + if (path) { + ctx.save(); + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0, lineHeight); + ctx.restore(); + } + else { + drawingLeft = left; + this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top, lineHeight); + } + charsToRender = ''; + actualStyle = nextStyle; + left += sign * boxWidth; + boxWidth = 0; } - lineTopOffset += heightOfLine; } - ctx.fillStyle = originalFill; - // if there is text background color no - // other shadows should be casted - this._removeShadow(ctx); + ctx.restore(); + }, + + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {fabric.Gradient} filler a fabric gradient instance + * @return {CanvasPattern} a pattern to use as fill/stroke style + */ + _applyPatternGradientTransformText: function(filler) { + var pCanvas = fabric.util.createCanvasElement(), pCtx, + // TODO: verify compatibility with strokeUniform + width = this.width + this.strokeWidth, height = this.height + this.strokeWidth; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.fillStyle = filler.toLive(pCtx); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fill(); + return pCtx.createPattern(pCanvas, 'no-repeat'); + }, + + handleFiller: function(ctx, property, filler) { + var offsetX, offsetY; + if (filler.toLive) { + if (filler.gradientUnits === 'percentage' || filler.gradientTransform || filler.patternTransform) { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + offsetX = -this.width / 2; + offsetY = -this.height / 2; + ctx.translate(offsetX, offsetY); + ctx[property] = this._applyPatternGradientTransformText(filler); + return { offsetX: offsetX, offsetY: offsetY }; + } + else { + // is a simple gradient or pattern + ctx[property] = filler.toLive(ctx, this); + return this._applyPatternGradientTransform(ctx, filler); + } + } + else { + // is a color + ctx[property] = filler; + } + return { offsetX: 0, offsetY: 0 }; + }, + + _setStrokeStyles: function(ctx, decl) { + ctx.lineWidth = decl.strokeWidth; + ctx.lineCap = this.strokeLineCap; + ctx.lineDashOffset = this.strokeDashOffset; + ctx.lineJoin = this.strokeLineJoin; + ctx.miterLimit = this.strokeMiterLimit; + return this.handleFiller(ctx, 'strokeStyle', decl.stroke); + }, + + _setFillStyles: function(ctx, decl) { + return this.handleFiller(ctx, 'fillStyle', decl.fill); }, /** * @private - * @param {Number} lineWidth Width of text line + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {String} _char + * @param {Number} left Left coordinate + * @param {Number} top Top coordinate + * @param {Number} lineHeight Height of the line + */ + _renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) { + var decl = this._getStyleDeclaration(lineIndex, charIndex), + fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex), + shouldFill = method === 'fillText' && fullDecl.fill, + shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth, + fillOffsets, strokeOffsets; + + if (!shouldStroke && !shouldFill) { + return; + } + ctx.save(); + + shouldFill && (fillOffsets = this._setFillStyles(ctx, fullDecl)); + shouldStroke && (strokeOffsets = this._setStrokeStyles(ctx, fullDecl)); + + ctx.font = this._getFontDeclaration(fullDecl); + + + if (decl && decl.textBackgroundColor) { + this._removeShadow(ctx); + } + if (decl && decl.deltaY) { + top += decl.deltaY; + } + shouldFill && ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY); + shouldStroke && ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY); + ctx.restore(); + }, + + /** + * Turns the character into a 'superior figure' (i.e. 'superscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSuperscript: function(start, end) { + return this._setScript(start, end, this.superscript); + }, + + /** + * Turns the character into an 'inferior figure' (i.e. 'subscript') + * @param {Number} start selection start + * @param {Number} end selection end + * @returns {fabric.Text} thisArg + * @chainable + */ + setSubscript: function(start, end) { + return this._setScript(start, end, this.subscript); + }, + + /** + * Applies 'schema' at given position + * @private + * @param {Number} start selection start + * @param {Number} end selection end + * @param {Number} schema + * @returns {fabric.Text} thisArg + * @chainable + */ + _setScript: function(start, end, schema) { + var loc = this.get2DCursorLocation(start, true), + fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'), + dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'), + style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline }; + this.setSelectionStyles(style, start, end); + return this; + }, + + /** + * @private + * @param {Number} lineIndex index text line * @return {Number} Line left offset */ - _getLineLeftOffset: function(lineWidth) { - if (this.textAlign === 'center') { - return (this.width - lineWidth) / 2; + _getLineLeftOffset: function(lineIndex) { + var lineWidth = this.getLineWidth(lineIndex), + lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, + isEndOfWrapping, leftOffset = 0, isEndOfWrapping = this.isEndOfWrapping(lineIndex); + if (textAlign === 'justify' + || (textAlign === 'justify-center' && !isEndOfWrapping) + || (textAlign === 'justify-right' && !isEndOfWrapping) + || (textAlign === 'justify-left' && !isEndOfWrapping) + ) { + return 0; } - if (this.textAlign === 'right') { - return this.width - lineWidth; + if (textAlign === 'center') { + leftOffset = lineDiff / 2; } - return 0; + if (textAlign === 'right') { + leftOffset = lineDiff; + } + if (textAlign === 'justify-center') { + leftOffset = lineDiff / 2; + } + if (textAlign === 'justify-right') { + leftOffset = lineDiff; + } + if (direction === 'rtl') { + leftOffset -= lineDiff; + } + return leftOffset; }, /** @@ -22359,6 +27185,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { _clearCache: function() { this.__lineWidths = []; this.__lineHeights = []; + this.__charBounds = []; }, /** @@ -22368,39 +27195,27 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { var shouldClear = this._forceClearCache; shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps')); if (shouldClear) { - this.saveState({ propertySet: '_dimensionAffectingProps' }); 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 {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex line number * @return {Number} Line width */ - _getLineWidth: function(ctx, lineIndex) { - if (this.__lineWidths[lineIndex]) { - return this.__lineWidths[lineIndex] === -1 ? this.width : this.__lineWidths[lineIndex]; + getLineWidth: function(lineIndex) { + if (this.__lineWidths[lineIndex] !== undefined) { + return this.__lineWidths[lineIndex]; } - var width, wordCount, line = this._textLines[lineIndex]; - - if (line === '') { - width = 0; - } - else { - width = this._measureLine(ctx, lineIndex); - } + var lineInfo = this.measureLine(lineIndex); + var width = lineInfo.width; this.__lineWidths[lineIndex] = width; - - if (width && this.textAlign === 'justify') { - wordCount = line.split(/\s+/); - if (wordCount.length > 1) { - this.__lineWidths[lineIndex] = -1; - } - } return width; }, @@ -22412,90 +27227,146 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }, /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} lineIndex line number - * @return {Number} Line width + * Retrieves the value of property at given character position + * @param {Number} lineIndex the line number + * @param {Number} charIndex the character number + * @param {String} property the property name + * @returns the value of 'property' */ - _measureLine: function(ctx, lineIndex) { - var line = this._textLines[lineIndex], - width = ctx.measureText(line).width, - additionalSpace = 0, charCount, finalWidth; - if (this.charSpacing !== 0) { - charCount = line.split('').length; - additionalSpace = (charCount - 1) * this._getWidthOfCharSpacing(); + getValueOfPropertyAt: function(lineIndex, charIndex, property) { + var charStyle = this._getStyleDeclaration(lineIndex, charIndex); + if (charStyle && typeof charStyle[property] !== 'undefined') { + return charStyle[property]; } - finalWidth = width + additionalSpace; - return finalWidth > 0 ? finalWidth : 0; + return this[property]; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ - _renderTextDecoration: function(ctx) { - if (!this.textDecoration) { + _renderTextDecoration: function(ctx, type) { + if (!this[type] && !this.styleHas(type)) { return; } - var halfOfVerticalBox = this.height / 2, - _this = this, offsets = []; + var heightOfLine, size, _size, + lineLeftOffset, dy, _dy, + line, lastDecoration, + leftOffset = this._getLeftOffset(), + topOffset = this._getTopOffset(), top, + boxStart, boxWidth, charBox, currentDecoration, + maxHeight, currentFill, lastFill, path = this.path, + charSpacing = this._getWidthOfCharSpacing(), + offsetY = this.offsets[type]; - /** @ignore */ - function renderLinesAtOffset(offsets) { - var i, lineHeight = 0, len, j, oLen, lineWidth, - lineLeftOffset, heightOfLine; - - for (i = 0, len = _this._textLines.length; i < len; i++) { - - lineWidth = _this._getLineWidth(ctx, i); - lineLeftOffset = _this._getLineLeftOffset(lineWidth); - heightOfLine = _this._getHeightOfLine(ctx, i); - - for (j = 0, oLen = offsets.length; j < oLen; j++) { - ctx.fillRect( - _this._getLeftOffset() + lineLeftOffset, - lineHeight + (_this._fontSizeMult - 1 + offsets[j] ) * _this.fontSize - halfOfVerticalBox, - lineWidth, - _this.fontSize / 15); - } - lineHeight += heightOfLine; + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this.getHeightOfLine(i); + if (!this[type] && !this.styleHas(type, i)) { + topOffset += heightOfLine; + continue; } + line = this._textLines[i]; + maxHeight = heightOfLine / this.lineHeight; + lineLeftOffset = this._getLineLeftOffset(i); + boxStart = 0; + boxWidth = 0; + lastDecoration = this.getValueOfPropertyAt(i, 0, type); + lastFill = this.getValueOfPropertyAt(i, 0, 'fill'); + top = topOffset + maxHeight * (1 - this._fontSizeFraction); + size = this.getHeightOfChar(i, 0); + dy = this.getValueOfPropertyAt(i, 0, 'deltaY'); + for (var j = 0, jlen = line.length; j < jlen; j++) { + charBox = this.__charBounds[i][j]; + currentDecoration = this.getValueOfPropertyAt(i, j, type); + currentFill = this.getValueOfPropertyAt(i, j, 'fill'); + _size = this.getHeightOfChar(i, j); + _dy = this.getValueOfPropertyAt(i, j, 'deltaY'); + if (path && currentDecoration && currentFill) { + ctx.save(); + ctx.fillStyle = lastFill; + ctx.translate(charBox.renderLeft, charBox.renderTop); + ctx.rotate(charBox.angle); + ctx.fillRect( + -charBox.kernedWidth / 2, + offsetY * _size + _dy, + charBox.kernedWidth, + this.fontSize / 15 + ); + ctx.restore(); + } + else if ( + (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) + && boxWidth > 0 + ) { + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + if (lastDecoration && lastFill) { + ctx.fillStyle = lastFill; + ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth, + this.fontSize / 15 + ); + } + boxStart = charBox.left; + boxWidth = charBox.width; + lastDecoration = currentDecoration; + lastFill = currentFill; + size = _size; + dy = _dy; + } + else { + boxWidth += charBox.kernedWidth; + } + } + var drawStart = leftOffset + lineLeftOffset + boxStart; + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - boxWidth; + } + ctx.fillStyle = currentFill; + currentDecoration && currentFill && ctx.fillRect( + drawStart, + top + offsetY * size + dy, + boxWidth - charSpacing, + this.fontSize / 15 + ); + topOffset += heightOfLine; } - - if (this.textDecoration.indexOf('underline') > -1) { - offsets.push(0.85); // 1 - 3/16 - } - if (this.textDecoration.indexOf('line-through') > -1) { - offsets.push(0.43); - } - if (this.textDecoration.indexOf('overline') > -1) { - offsets.push(-0.12); - } - if (offsets.length > 0) { - renderLinesAtOffset(offsets); - } + // 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() { + _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" - (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), - (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), - this.fontSize + 'px', - (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + // 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 - * @param {Boolean} noTransform */ - render: function(ctx, noTransform) { + render: function(ctx) { // do not render if object is not visible if (!this.visible) { return; @@ -22504,18 +27375,27 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { return; } if (this._shouldClearDimensionCache()) { - this._setTextStyles(ctx); - this._initDimensions(ctx); + this.initDimensions(); } - this.callSuper('render', ctx, noTransform); + 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() { - return this.text.split(this._reNewline); + _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 }; }, /** @@ -22524,225 +27404,47 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @return {Object} Object representation of an instance */ toObject: function(propertiesToInclude) { - var additionalProperties = [ - 'text', - 'fontSize', - 'fontWeight', - 'fontFamily', - 'fontStyle', - 'lineHeight', - 'textDecoration', - 'textAlign', - 'textBackgroundColor', - 'charSpacing' - ].concat(propertiesToInclude); - return this.callSuper('toObject', additionalProperties); - }, - - /* _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) { - if (!this.ctx) { - this.ctx = fabric.util.createCanvasElement().getContext('2d'); + var allProperties = additionalProps.concat(propertiesToInclude); + var obj = this.callSuper('toObject', allProperties); + obj.styles = fabric.util.stylesToArray(this.styles, this.text); + if (obj.path) { + obj.path = this.path.toObject(); } - var markup = this._createBaseSVGMarkup(), - offsets = this._getSVGLeftTopOffsets(this.ctx), - textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); - this._wrapSVGTextAndBg(markup, textAndBg); - - return reviver ? reviver(markup.join('')) : markup.join(''); + return obj; }, /** - * @private - */ - _getSVGLeftTopOffsets: function(ctx) { - var lineTop = this._getHeightOfLine(ctx, 0), - textLeft = -this.width / 2, - textTop = 0; - - return { - textLeft: textLeft + (this.group && this.group.type === 'path-group' ? this.left : 0), - textTop: textTop + (this.group && this.group.type === 'path-group' ? -this.top : 0), - lineTop: lineTop - }; - }, - - /** - * @private - */ - _wrapSVGTextAndBg: function(markup, textAndBg) { - var noShadow = true, filter = this.getSvgFilter(), - style = filter === '' ? '' : ' style="' + filter + '"'; - - markup.push( - '\t\n', - textAndBg.textBgRects.join(''), - '\t\t\n', - textAndBg.textSpans.join(''), - '\t\t\n', - '\t\n' - ); - }, - - /** - * @private - * @param {Number} textTopOffset Text top offset - * @param {Number} textLeftOffset Text left offset - * @return {Object} - */ - _getSVGTextAndBg: function(textTopOffset, textLeftOffset) { - var textSpans = [], - textBgRects = [], - height = 0; - // bounding-box background - this._setSVGBg(textBgRects); - - // text and text-background - for (var i = 0, len = this._textLines.length; i < len; i++) { - if (this.textBackgroundColor) { - this._setSVGTextLineBg(textBgRects, i, textLeftOffset, textTopOffset, height); - } - this._setSVGTextLineText(i, textSpans, height, textLeftOffset, textTopOffset, textBgRects); - height += this._getHeightOfLine(this.ctx, i); - } - - return { - textSpans: textSpans, - textBgRects: textBgRects - }; - }, - - _setSVGTextLineText: function(i, textSpans, height, textLeftOffset, textTopOffset) { - var yPos = this.fontSize * (this._fontSizeMult - this._fontSizeFraction) - - textTopOffset + height - this.height / 2; - if (this.textAlign === 'justify') { - // i call from here to do not intefere with IText - this._setSVGTextLineJustifed(i, textSpans, yPos, textLeftOffset); - return; - } - textSpans.push( - '\t\t\t elements since setting opacity - // on containing one doesn't work in Illustrator - this._getFillAttributes(this.fill), '>', - fabric.util.string.escapeXml(this._textLines[i]), - '\n' - ); - }, - - _setSVGTextLineJustifed: function(i, textSpans, yPos, textLeftOffset) { - var ctx = fabric.util.createCanvasElement().getContext('2d'); - - this._setTextStyles(ctx); - - var line = this._textLines[i], - words = line.split(/\s+/), - wordsWidth = this._getWidthOfWords(ctx, words.join('')), - widthDiff = this.width - wordsWidth, - numSpaces = words.length - 1, - spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, - word, attributes = this._getFillAttributes(this.fill), - len; - - textLeftOffset += this._getLineLeftOffset(this._getLineWidth(ctx, i)); - - for (i = 0, len = words.length; i < len; i++) { - word = words[i]; - textSpans.push( - '\t\t\t elements since setting opacity - // on containing one doesn't work in Illustrator - attributes, '>', - fabric.util.string.escapeXml(word), - '\n' - ); - textLeftOffset += this._getWidthOfWords(ctx, word) + spaceWidth; - } - }, - - _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, textTopOffset, height) { - textBgRects.push( - '\t\t\n'); - }, - - _setSVGBg: function(textBgRects) { - if (this.backgroundColor) { - textBgRects.push( - '\t\t\n'); - } - }, - - /** - * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values - * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 - * - * @private - * @param {*} value - * @return {String} - */ - _getFillAttributes: function(value) { - var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; - if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { - return 'fill="' + value + '"'; - } - return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; - }, - /* _TO_SVG_END_ */ - - /** - * Sets specified property to a specified value - * @param {String} key - * @param {*} value - * @return {fabric.Text} thisArg + * 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); - - if (this._dimensionAffectingProps.indexOf(key) > -1) { - this._initDimensions(); + set: function(key, value) { + this.callSuper('set', key, value); + var needsDims = false; + var isAddingPath = false; + if (typeof key === 'object') { + for (var _key in key) { + if (_key === 'path') { + this.setPathInfo(); + } + needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1; + isAddingPath = isAddingPath || _key === 'path'; + } + } + else { + needsDims = this._dimensionAffectingProps.indexOf(key) !== -1; + isAddingPath = key === 'path'; + } + if (isAddingPath) { + this.setPathInfo(); + } + if (needsDims) { + this.initDimensions(); this.setCoords(); } + return this; }, /** @@ -22762,7 +27464,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @see: http://www.w3.org/TR/SVG/text.html#TextElement */ fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( - 'x y dx dy font-family font-style font-weight font-size text-decoration text-anchor'.split(' ')); + 'x y dx dy font-family font-style font-weight font-size letter-spacing text-decoration text-anchor'.split(' ')); /** * Default SVG font size @@ -22776,19 +27478,33 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @static * @memberOf fabric.Text * @param {SVGElement} element Element to parse + * @param {Function} callback callback function invoked after parsing * @param {Object} [options] Options object - * @return {fabric.Text} Instance of fabric.Text */ - fabric.Text.fromElement = function(element, options) { + fabric.Text.fromElement = function(element, callback, options) { if (!element) { - return null; + return callback(null); } - var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); - options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); + 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; } @@ -22799,10 +27515,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; } - if (!options.originX) { - options.originX = 'left'; - } - var textContent = ''; // The XML is not properly parsed in IE9 so a workaround to get @@ -22820,30 +27532,32 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { } 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.getHeight() / text.height, + textHeightScaleFactor = text.getScaledHeight() / text.height, lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height, scaledDiff = lineHeightDiff * textHeightScaleFactor, - textHeight = text.getHeight() + scaledDiff, + textHeight = text.getScaledHeight() + scaledDiff, offX = 0; /* Adjust positioning: x/y attributes in SVG correspond to the bottom-left corner of text bounding box - top/left properties in Fabric correspond to center point of text bounding box + fabric output by default at top, left. */ - if (text.originX === 'left') { - offX = text.getWidth() / 2; + if (parsedAnchor === 'center') { + offX = text.getScaledWidth() / 2; } - if (text.originX === 'right') { - offX = -text.getWidth() / 2; + if (parsedAnchor === 'right') { + offX = text.getScaledWidth(); } text.set({ - left: text.getLeft() + offX, - top: text.getTop() - textHeight / 2 + text.fontSize * (0.18 + text._fontSizeFraction) / text.lineHeight /* 0.3 is the old lineHeight */ + left: text.left - offX, + top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight, + strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1, }); - - return text; + callback(text); }; /* _FROM_SVG_END_ */ @@ -22851,23 +27565,369 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * Returns fabric.Text instance from an object representation * @static * @memberOf fabric.Text - * @param {Object} object Object to create an instance from + * @param {Object} object plain js Object to create an instance from * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.Text} Instance of fabric.Text */ - fabric.Text.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Text', object, callback, forceAsync, 'text'); + fabric.Text.fromObject = function(object, callback) { + var objectCopy = clone(object), path = object.path; + delete objectCopy.path; + return fabric.Object._fromObject('Text', objectCopy, function(textInstance) { + textInstance.styles = fabric.util.stylesFromArray(object.styles, object.text); + if (path) { + fabric.Object._fromObject('Path', path, function(pathInstance) { + textInstance.set('path', pathInstance); + callback(textInstance); + }, 'path'); + } + else { + callback(textInstance); + } + }, 'text'); }; - fabric.util.createAccessors(fabric.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; + }, - var clone = fabric.util.object.clone; + /** + * 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. useful 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, selectionStart 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:" @@ -22974,11 +28034,14 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { cursorWidth: 2, /** - * Color of default cursor (when not overwritten by character style) + * Color of text cursor color in editing mode. + * if not set (default) will take color from the text. + * if set to a color value that fabric can understand, it will + * be used instead of the color of the text at the current position. * @type String * @default */ - cursorColor: '#333', + cursorColor: '', /** * Delay between cursor blink (in ms) @@ -22994,14 +28057,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ cursorDuration: 600, - /** - * Object containing character styles - * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) - * @type Object - * @default - */ - styles: null, - /** * Indicates whether internal text char widths can be cached * @type Boolean @@ -23009,6 +28064,16 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ caching: true, + /** + * DOM container to append the hiddenTextarea. + * An alternative to attaching to the document.body. + * Useful to reduce laggish redraw of the full document.body tree and + * also with modals event capturing that won't let the textarea take focus. + * @type HTMLElement + * @default + */ + hiddenTextareaContainer: null, + /** * @private */ @@ -23034,6 +28099,12 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ __widthOfSpace: [], + /** + * Helps determining when the text is in composition, so that the cursor + * rendering is altered. + */ + inCompositionMode: false, + /** * Constructor * @param {String} text Text string @@ -23041,39 +28112,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @return {fabric.IText} thisArg */ initialize: function(text, options) { - this.styles = options ? (options.styles || { }) : { }; this.callSuper('initialize', text, options); this.initBehavior(); }, - /** - * @private - */ - _clearCache: function() { - this.callSuper('_clearCache'); - this.__widthOfSpace = []; - }, - - /** - * Returns true if object has no styling - */ - isEmptyStyles: function() { - if (!this.styles) { - return true; - } - var obj = this.styles; - - 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; - }, - /** * Sets selection start (left boundary of a selection) * @param {Number} index Index to set selection start to @@ -23114,88 +28156,26 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, - /** - * Gets style of a current selection/cursor (at the start position) - * @param {Number} [startIndex] Start index to get styles at - * @param {Number} [endIndex] End index to get styles at - * @return {Object} styles Style object at a specified (or current) index - */ - getSelectionStyles: function(startIndex, endIndex) { - - if (arguments.length === 2) { - var styles = []; - for (var i = startIndex; i < endIndex; i++) { - styles.push(this.getSelectionStyles(i)); - } - return styles; - } - - var loc = this.get2DCursorLocation(startIndex), - style = this._getStyleDeclaration(loc.lineIndex, loc.charIndex); - - return style || {}; - }, - - /** - * Sets style of a current selection - * @param {Object} [styles] Styles object - * @return {fabric.IText} thisArg - * @chainable - */ - setSelectionStyles: function(styles) { - if (this.selectionStart === this.selectionEnd) { - this._extendStyles(this.selectionStart, styles); - } - else { - for (var i = this.selectionStart; i < this.selectionEnd; i++) { - this._extendStyles(i, styles); - } - } - /* not included in _extendStyles to avoid clearing cache more than once */ - this._forceClearCache = true; - return this; - }, - - /** - * @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); - }, - /** * Initialize text dimensions. Render all text on given context * or on a offscreen canvas to get the text width with measureText. * Updates this.width and this.height with the proper values. * Does not return dimensions. - * @param {CanvasRenderingContext2D} [ctx] Context to render on * @private */ - _initDimensions: function(ctx) { - if (!ctx) { - this.clearContextTop(); - } - this.callSuper('_initDimensions', ctx); + initDimensions: function() { + this.isEditing && this.initDelayedCursor(); + this.clearContextTop(); + this.callSuper('initDimensions'); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} noTransform */ - render: function(ctx, noTransform) { + render: function(ctx) { this.clearContextTop(); - this.callSuper('render', ctx, noTransform); + 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 = { }; @@ -23208,55 +28188,38 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ _render: function(ctx) { this.callSuper('_render', ctx); - this.ctx = ctx; }, /** * Prepare and clean the contextTop */ - clearContextTop: function() { - if (!this.active || !this.isEditing) { + clearContextTop: function(skipRestore) { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { return; } - if (this.canvas && this.canvas.contextTop) { - var ctx = this.canvas.contextTop; - ctx.save(); - ctx.transform.apply(ctx, this.canvas.viewportTransform); - this.transform(ctx); - this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix); - this._clearTextArea(ctx); - ctx.restore(); - } + 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._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.active || !this.isEditing) { + if (!this.isEditing || !this.canvas || !this.canvas.contextTop) { return; } - var chars = this.text.split(''), - boundaries, ctx; - if (this.canvas && this.canvas.contextTop) { - ctx = this.canvas.contextTop; - ctx.save(); - ctx.transform.apply(ctx, this.canvas.viewportTransform); - this.transform(ctx); - this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix); - this._clearTextArea(ctx); - } - else { - ctx = this.ctx; - ctx.save(); - } + var boundaries = this._getCursorBoundaries(), + ctx = this.canvas.contextTop; + this.clearContextTop(true); if (this.selectionStart === this.selectionEnd) { - boundaries = this._getCursorBoundaries(chars, 'cursor'); this.renderCursor(boundaries, ctx); } else { - boundaries = this._getCursorBoundaries(chars, 'selection'); - this.renderSelection(chars, boundaries, ctx); + this.renderSelection(boundaries, ctx); } ctx.restore(); }, @@ -23266,73 +28229,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { var width = this.width + 4, height = this.height + 4; ctx.clearRect(-width / 2, -height / 2, width, height); }, - /** - * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) - * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. - */ - get2DCursorLocation: function(selectionStart) { - if (typeof selectionStart === 'undefined') { - selectionStart = this.selectionStart; - } - var len = this._textLines.length; - for (var i = 0; i < len; i++) { - if (selectionStart <= this._textLines[i].length) { - return { - lineIndex: i, - charIndex: selectionStart - }; - } - selectionStart -= this._textLines[i].length + 1; - } - return { - lineIndex: i - 1, - charIndex: this._textLines[i - 1].length < selectionStart ? this._textLines[i - 1].length : selectionStart - }; - }, - - /** - * Returns complete style of char at the current cursor - * @param {Number} lineIndex Line index - * @param {Number} charIndex Char index - * @return {Object} Character style - */ - getCurrentCharStyle: function(lineIndex, charIndex) { - var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); - - return { - fontSize: style && style.fontSize || this.fontSize, - fill: style && style.fill || this.fill, - textBackgroundColor: style && style.textBackgroundColor || this.textBackgroundColor, - textDecoration: style && style.textDecoration || this.textDecoration, - fontFamily: style && style.fontFamily || this.fontFamily, - fontWeight: style && style.fontWeight || this.fontWeight, - fontStyle: style && style.fontStyle || this.fontStyle, - stroke: style && style.stroke || this.stroke, - strokeWidth: style && style.strokeWidth || this.strokeWidth - }; - }, - - /** - * Returns fontSize of char at the current cursor - * @param {Number} lineIndex Line index - * @param {Number} charIndex Char index - * @return {Number} Character font size - */ - getCurrentCharFontSize: function(lineIndex, charIndex) { - var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); - return style && style.fontSize ? style.fontSize : this.fontSize; - }, - - /** - * Returns color (fill) of char at the current cursor - * @param {Number} lineIndex Line index - * @param {Number} charIndex Char index - * @return {String} Character color (fill) - */ - getCurrentCharColor: function(lineIndex, charIndex) { - var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); - return style && style.fill ? style.fill : this.cursorColor; - }, /** * Returns cursor boundaries (left, top, leftOffset, topOffset) @@ -23340,21 +28236,22 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {Array} chars Array of characters * @param {String} typeOfBoundaries */ - _getCursorBoundaries: function(chars, 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 - var left = Math.round(this._getLeftOffset()), + if (typeof position === 'undefined') { + position = this.selectionStart; + } + + var left = this._getLeftOffset(), top = this._getTopOffset(), - - offsets = this._getCursorBoundariesOffsets( - chars, typeOfBoundaries); - + offsets = this._getCursorBoundariesOffsets(position); return { left: left, top: top, - leftOffset: offsets.left + offsets.lineLeft, + leftOffset: offsets.left, topOffset: offsets.top }; }, @@ -23362,44 +28259,35 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * @private */ - _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) { + _getCursorBoundariesOffsets: function(position) { if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) { return this.cursorOffsetCache; } - var lineLeftOffset = 0, - lineIndex = 0, - charIndex = 0, + var lineLeftOffset, + lineIndex, + charIndex, topOffset = 0, leftOffset = 0, - boundaries; - - for (var i = 0; i < this.selectionStart; i++) { - if (chars[i] === '\n') { - leftOffset = 0; - topOffset += this._getHeightOfLine(this.ctx, lineIndex); - - lineIndex++; - charIndex = 0; - } - else { - leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); - charIndex++; - } - - lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)); - } - if (typeOfBoundaries === 'cursor') { - topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, lineIndex) / this.lineHeight - - this.getCurrentCharFontSize(lineIndex, charIndex) * (1 - this._fontSizeFraction); + 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: leftOffset > 0 ? leftOffset : 0, - lineLeft: lineLeftOffset + left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0), }; + if (this.direction === 'rtl') { + boundaries.left *= -1; + } this.cursorOffsetCache = boundaries; return this.cursorOffsetCache; }, @@ -23410,646 +28298,127 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @param {CanvasRenderingContext2D} ctx transformed context to draw on */ renderCursor: function(boundaries, ctx) { - var cursorLocation = this.get2DCursorLocation(), lineIndex = cursorLocation.lineIndex, - charIndex = cursorLocation.charIndex, - charHeight = this.getCurrentCharFontSize(lineIndex, charIndex), - leftOffset = boundaries.leftOffset, + charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0, + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'), multiplier = this.scaleX * this.canvas.getZoom(), - cursorWidth = this.cursorWidth / multiplier; + 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); - ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); + if (this.inCompositionMode) { + this.renderSelection(boundaries, ctx); + } + ctx.fillStyle = this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, 'fill'); ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; - ctx.fillRect( - boundaries.left + leftOffset - cursorWidth / 2, - boundaries.top + boundaries.topOffset, + boundaries.left + boundaries.leftOffset - cursorWidth / 2, + topOffset + boundaries.top + dy, cursorWidth, charHeight); }, /** * Renders text selection - * @param {Array} chars Array of characters * @param {Object} boundaries Object with left/top/leftOffset/topOffset * @param {CanvasRenderingContext2D} ctx transformed context to draw on */ - renderSelection: function(chars, boundaries, ctx) { + renderSelection: function(boundaries, ctx) { - ctx.fillStyle = this.selectionColor; - - var start = this.get2DCursorLocation(this.selectionStart), - end = this.get2DCursorLocation(this.selectionEnd), + 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; + 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(this._getLineWidth(ctx, i)) || 0, - lineHeight = this._getHeightOfLine(this.ctx, i), - realLineHeight = 0, boxWidth = 0, line = this._textLines[i]; + var lineOffset = this._getLineLeftOffset(i) || 0, + lineHeight = this.getHeightOfLine(i), + realLineHeight = 0, boxStart = 0, boxEnd = 0; if (i === startLine) { - for (var j = 0, len = line.length; j < len; j++) { - if (j >= start.charIndex && (i !== endLine || j < end.charIndex)) { - boxWidth += this._getWidthOfChar(ctx, line[j], i, j); - } - if (j < start.charIndex) { - lineOffset += this._getWidthOfChar(ctx, line[j], i, j); - } - } - if (j === line.length) { - boxWidth -= this._getWidthOfCharSpacing(); - } + boxStart = this.__charBounds[startLine][startChar].left; } - else if (i > startLine && i < endLine) { - boxWidth += this._getLineWidth(ctx, i) || 5; + if (i >= startLine && i < endLine) { + boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5; // WTF is this 5? } else if (i === endLine) { - for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) { - boxWidth += this._getWidthOfChar(ctx, line[j2], i, j2); + if (endChar === 0) { + boxEnd = this.__charBounds[endLine][endChar].left; } - if (end.charIndex === line.length) { - boxWidth -= this._getWidthOfCharSpacing(); + 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; } + var drawStart = boundaries.left + lineOffset + boxStart, + drawWidth = boxEnd - boxStart, + drawHeight = lineHeight, extraTop = 0; + if (this.inCompositionMode) { + ctx.fillStyle = this.compositionColor || 'black'; + drawHeight = 1; + extraTop = lineHeight; + } + else { + ctx.fillStyle = this.selectionColor; + } + if (this.direction === 'rtl') { + drawStart = this.width - drawStart - drawWidth; + } ctx.fillRect( - boundaries.left + lineOffset, - boundaries.top + boundaries.topOffset, - boxWidth > 0 ? boxWidth : 0, - lineHeight); - + drawStart, + boundaries.top + boundaries.topOffset + extraTop, + drawWidth, + drawHeight); boundaries.topOffset += realLineHeight; } }, /** - * @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 + * 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 + * Unused from the library, is for the end user + * @return {Number} Character font size */ - _renderChars: function(method, ctx, line, left, top, lineIndex, charOffset) { - - if (this.isEmptyStyles()) { - return this._renderCharsFast(method, ctx, line, left, top); - } - - charOffset = charOffset || 0; - - // set proper line offset - var lineHeight = this._getHeightOfLine(ctx, lineIndex), - prevStyle, - thisStyle, - charsToRender = ''; - - ctx.save(); - top -= lineHeight / this.lineHeight * this._fontSizeFraction; - for (var i = charOffset, len = line.length + charOffset; i <= len; i++) { - prevStyle = prevStyle || this.getCurrentCharStyle(lineIndex, i); - thisStyle = this.getCurrentCharStyle(lineIndex, i + 1); - - if (this._hasStyleChanged(prevStyle, thisStyle) || i === len) { - this._renderChar(method, ctx, lineIndex, i - 1, charsToRender, left, top, lineHeight); - charsToRender = ''; - prevStyle = thisStyle; - } - charsToRender += line[i - charOffset]; - } - ctx.restore(); + getCurrentCharFontSize: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fontSize'); }, /** - * @private - * @param {String} method - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} line Content of the line - * @param {Number} left Left coordinate - * @param {Number} top Top coordinate + * 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 + * if the text object has a pattern or gradient for filler, it will return that. + * Unused by the library, is for the end user + * @return {String | fabric.Gradient | fabric.Pattern} Character color (fill) */ - _renderCharsFast: function(method, ctx, line, left, top) { - - if (method === 'fillText' && this.fill) { - this.callSuper('_renderChars', method, ctx, line, left, top); - } - if (method === 'strokeText' && ((this.stroke && this.strokeWidth > 0) || this.skipFillStrokeCheck)) { - this.callSuper('_renderChars', method, ctx, line, left, top); - } - }, - - /** - * @private - * @param {String} method - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} lineIndex - * @param {Number} i - * @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, i, _char, left, top, lineHeight) { - var charWidth, charHeight, shouldFill, shouldStroke, - decl = this._getStyleDeclaration(lineIndex, i), - offset, textDecoration, chars, additionalSpace, _charWidth; - - if (decl) { - charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i); - shouldStroke = decl.stroke; - shouldFill = decl.fill; - textDecoration = decl.textDecoration; - } - else { - charHeight = this.fontSize; - } - - shouldStroke = (shouldStroke || this.stroke) && method === 'strokeText'; - shouldFill = (shouldFill || this.fill) && method === 'fillText'; - - decl && ctx.save(); - - charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl || null); - textDecoration = textDecoration || this.textDecoration; - - if (decl && decl.textBackgroundColor) { - this._removeShadow(ctx); - } - if (this.charSpacing !== 0) { - additionalSpace = this._getWidthOfCharSpacing(); - chars = _char.split(''); - charWidth = 0; - for (var j = 0, len = chars.length, jChar; j < len; j++) { - jChar = chars[j]; - shouldFill && ctx.fillText(jChar, left + charWidth, top); - shouldStroke && ctx.strokeText(jChar, left + charWidth, top); - _charWidth = ctx.measureText(jChar).width + additionalSpace; - charWidth += _charWidth > 0 ? _charWidth : 0; - } - } - else { - shouldFill && ctx.fillText(_char, left, top); - shouldStroke && ctx.strokeText(_char, left, top); - } - - if (textDecoration || textDecoration !== '') { - offset = this._fontSizeFraction * lineHeight / this.lineHeight; - this._renderCharDecoration(ctx, textDecoration, left, top, offset, charWidth, charHeight); - } - - decl && ctx.restore(); - ctx.translate(charWidth, 0); - }, - - /** - * @private - * @param {Object} prevStyle - * @param {Object} thisStyle - */ - _hasStyleChanged: function(prevStyle, thisStyle) { - return (prevStyle.fill !== thisStyle.fill || - prevStyle.fontSize !== thisStyle.fontSize || - prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor || - prevStyle.textDecoration !== thisStyle.textDecoration || - prevStyle.fontFamily !== thisStyle.fontFamily || - prevStyle.fontWeight !== thisStyle.fontWeight || - prevStyle.fontStyle !== thisStyle.fontStyle || - prevStyle.stroke !== thisStyle.stroke || - prevStyle.strokeWidth !== thisStyle.strokeWidth - ); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderCharDecoration: function(ctx, textDecoration, left, top, offset, charWidth, charHeight) { - - if (!textDecoration) { - return; - } - - var decorationWeight = charHeight / 15, - positions = { - underline: top + charHeight / 10, - 'line-through': top - charHeight * (this._fontSizeFraction + this._fontSizeMult - 1) + decorationWeight, - overline: top - (this._fontSizeMult - this._fontSizeFraction) * charHeight - }, - decorations = ['underline', 'line-through', 'overline'], i, decoration; - - for (i = 0; i < decorations.length; i++) { - decoration = decorations[i]; - if (textDecoration.indexOf(decoration) > -1) { - ctx.fillRect(left, positions[decoration], charWidth , decorationWeight); - } - } - }, - - /** - * @private - * @param {String} method - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} line - * @param {Number} left - * @param {Number} top - * @param {Number} lineIndex - */ - _renderTextLine: function(method, ctx, line, left, top, lineIndex) { - // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine - // the adding 0.03 is just to align text with itext by overlap test - if (!this.isEmptyStyles()) { - top += this.fontSize * (this._fontSizeFraction + 0.03); - } - this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderTextDecoration: function(ctx) { - if (this.isEmptyStyles()) { - return this.callSuper('_renderTextDecoration', ctx); - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderTextLinesBackground: function(ctx) { - this.callSuper('_renderTextLinesBackground', ctx); - - var lineTopOffset = 0, heightOfLine, - lineWidth, lineLeftOffset, - leftOffset = this._getLeftOffset(), - topOffset = this._getTopOffset(), - colorCache = '', - line, _char, style, leftCache, - topCache, widthCache, heightCache; - ctx.save(); - for (var i = 0, len = this._textLines.length; i < len; i++) { - heightOfLine = this._getHeightOfLine(ctx, i); - line = this._textLines[i]; - - if (line === '' || !this.styles || !this._getLineStyle(i)) { - lineTopOffset += heightOfLine; - continue; - } - - lineWidth = this._getLineWidth(ctx, i); - lineLeftOffset = this._getLineLeftOffset(lineWidth); - leftCache = topCache = widthCache = heightCache = 0; - for (var j = 0, jlen = line.length; j < jlen; j++) { - style = this._getStyleDeclaration(i, j) || {}; - - if (colorCache !== style.textBackgroundColor) { - if (heightCache && widthCache) { - ctx.fillStyle = colorCache; - ctx.fillRect(leftCache, topCache, widthCache, heightCache); - } - leftCache = topCache = widthCache = heightCache = 0; - colorCache = style.textBackgroundColor || ''; - } - - if (!style.textBackgroundColor) { - colorCache = ''; - continue; - } - _char = line[j]; - - if (colorCache === style.textBackgroundColor) { - colorCache = style.textBackgroundColor; - if (!leftCache) { - leftCache = leftOffset + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j); - } - topCache = topOffset + lineTopOffset; - widthCache += this._getWidthOfChar(ctx, _char, i, j); - heightCache = heightOfLine / this.lineHeight; - } - } - // if a textBackgroundColor ends on the last character of a line - if (heightCache && widthCache) { - ctx.fillStyle = colorCache; - ctx.fillRect(leftCache, topCache, widthCache, heightCache); - leftCache = topCache = widthCache = heightCache = 0; - } - lineTopOffset += heightOfLine; - } - ctx.restore(); + getCurrentCharColor: function() { + var cp = this._getCurrentCharIndex(); + return this.getValueOfPropertyAt(cp.l, cp.c, 'fill'); }, /** + * Returns the cursor position for the getCurrent.. functions * @private */ - _getCacheProp: function(_char, styleDeclaration) { - return _char + - styleDeclaration.fontSize + - styleDeclaration.fontWeight + - styleDeclaration.fontStyle; - }, - - /** - * @private - * @param {String} fontFamily name - * @return {Object} reference to cache - */ - _getFontCache: function(fontFamily) { - if (!fabric.charWidthsCache[fontFamily]) { - fabric.charWidthsCache[fontFamily] = { }; - } - return fabric.charWidthsCache[fontFamily]; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} _char - * @param {Number} lineIndex - * @param {Number} charIndex - * @param {Object} [decl] - */ - _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { - var charDecl = decl || this._getStyleDeclaration(lineIndex, charIndex), - styleDeclaration = clone(charDecl), - width, cacheProp, charWidthsCache; - - this._applyFontStyles(styleDeclaration); - charWidthsCache = this._getFontCache(styleDeclaration.fontFamily); - cacheProp = this._getCacheProp(_char, styleDeclaration); - - // short-circuit if no styles for this char - // global style from object is always applyed and handled by save and restore - if (!charDecl && charWidthsCache[cacheProp] && this.caching) { - return charWidthsCache[cacheProp]; - } - - if (typeof styleDeclaration.shadow === 'string') { - styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow); - } - - var fill = styleDeclaration.fill || this.fill; - ctx.fillStyle = fill.toLive - ? fill.toLive(ctx, this) - : fill; - - if (styleDeclaration.stroke) { - ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) - ? styleDeclaration.stroke.toLive(ctx, this) - : styleDeclaration.stroke; - } - - ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; - ctx.font = this._getFontDeclaration.call(styleDeclaration); - - //if we want this._setShadow.call to work with styleDeclarion - //we have to add those references - if (styleDeclaration.shadow) { - styleDeclaration.scaleX = this.scaleX; - styleDeclaration.scaleY = this.scaleY; - styleDeclaration.canvas = this.canvas; - styleDeclaration.getObjectScaling = this.getObjectScaling; - this._setShadow.call(styleDeclaration, ctx); - } - - if (!this.caching || !charWidthsCache[cacheProp]) { - width = ctx.measureText(_char).width; - this.caching && (charWidthsCache[cacheProp] = width); - return width; - } - - return charWidthsCache[cacheProp]; - }, - - /** - * @private - * @param {Object} styleDeclaration - */ - _applyFontStyles: function(styleDeclaration) { - if (!styleDeclaration.fontFamily) { - styleDeclaration.fontFamily = this.fontFamily; - } - if (!styleDeclaration.fontSize) { - styleDeclaration.fontSize = this.fontSize; - } - if (!styleDeclaration.fontWeight) { - styleDeclaration.fontWeight = this.fontWeight; - } - if (!styleDeclaration.fontStyle) { - styleDeclaration.fontStyle = this.fontStyle; - } - }, - - /** - * @param {Number} lineIndex - * @param {Number} charIndex - * @param {Boolean} [returnCloneOrEmpty=false] - * @private - */ - _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) { - if (returnCloneOrEmpty) { - return (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) - ? clone(this.styles[lineIndex][charIndex]) - : { }; - } - - return this.styles[lineIndex] && this.styles[lineIndex][charIndex] ? this.styles[lineIndex][charIndex] : null; - }, - - /** - * @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 - * @private - */ - _getLineStyle: function(lineIndex) { - return this.styles[lineIndex]; - }, - - /** - * @param {Number} lineIndex - * @param {Object} style - * @private - */ - _setLineStyle: function(lineIndex, style) { - this.styles[lineIndex] = style; - }, - - /** - * @param {Number} lineIndex - * @private - */ - _deleteLineStyle: function(lineIndex) { - delete this.styles[lineIndex]; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { - if (!this._isMeasuring && this.textAlign === 'justify' && this._reSpacesAndTabs.test(_char)) { - return this._getWidthOfSpace(ctx, lineIndex); - } - ctx.save(); - var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); - if (this.charSpacing !== 0) { - width += this._getWidthOfCharSpacing(); - } - ctx.restore(); - return width > 0 ? width : 0; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} lineIndex - * @param {Number} charIndex - */ - _getHeightOfChar: function(ctx, lineIndex, charIndex) { - var style = this._getStyleDeclaration(lineIndex, charIndex); - return style && style.fontSize ? style.fontSize : this.fontSize; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} lineIndex - * @param {Number} charIndex - */ - _getWidthOfCharsAt: function(ctx, lineIndex, charIndex) { - var width = 0, i, _char; - for (i = 0; i < charIndex; i++) { - _char = this._textLines[lineIndex][i]; - width += this._getWidthOfChar(ctx, _char, lineIndex, i); - } - return width; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} lineIndex line number - * @return {Number} Line width - */ - _measureLine: function(ctx, lineIndex) { - this._isMeasuring = true; - var width = this._getWidthOfCharsAt(ctx, lineIndex, this._textLines[lineIndex].length); - if (this.charSpacing !== 0) { - width -= this._getWidthOfCharSpacing(); - } - this._isMeasuring = false; - return width > 0 ? width : 0; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Number} lineIndex - */ - _getWidthOfSpace: function (ctx, lineIndex) { - if (this.__widthOfSpace[lineIndex]) { - return this.__widthOfSpace[lineIndex]; - } - var line = this._textLines[lineIndex], - wordsWidth = this._getWidthOfWords(ctx, line, lineIndex, 0), - widthDiff = this.width - wordsWidth, - numSpaces = line.length - line.replace(this._reSpacesAndTabs, '').length, - width = Math.max(widthDiff / numSpaces, ctx.measureText(' ').width); - this.__widthOfSpace[lineIndex] = width; - return width; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {String} line - * @param {Number} lineIndex - * @param {Number} charOffset - */ - _getWidthOfWords: function (ctx, line, lineIndex, charOffset) { - var width = 0; - - for (var charIndex = 0; charIndex < line.length; charIndex++) { - var _char = line[charIndex]; - - if (!_char.match(/\s/)) { - width += this._getWidthOfChar(ctx, _char, lineIndex, charIndex + charOffset); - } - } - - return width; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _getHeightOfLine: function(ctx, lineIndex) { - if (this.__lineHeights[lineIndex]) { - return this.__lineHeights[lineIndex]; - } - - var line = this._textLines[lineIndex], - maxHeight = this._getHeightOfChar(ctx, lineIndex, 0); - - for (var i = 1, len = line.length; i < len; i++) { - var currentCharHeight = this._getHeightOfChar(ctx, lineIndex, i); - if (currentCharHeight > maxHeight) { - maxHeight = currentCharHeight; - } - } - this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; - return this.__lineHeights[lineIndex]; - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _getTextHeight: function(ctx) { - var lineHeight, height = 0; - for (var i = 0, len = this._textLines.length; i < len; i++) { - lineHeight = this._getHeightOfLine(ctx, i); - height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); - } - return height; - }, - - /** - * 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 fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { - styles: clone(this.styles, true) - }); + _getCurrentCharIndex: function() { + var cursorPosition = this.get2DCursorLocation(this.selectionStart, true), + charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0; + return { l: cursorPosition.lineIndex, c: charIndex }; } }); @@ -24059,11 +28428,20 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @memberOf fabric.IText * @param {Object} object Object to create an instance from * @param {function} [callback] invoked with new instance as argument - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.IText} instance of fabric.IText */ - fabric.IText.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('IText', object, callback, forceAsync, 'text'); + fabric.IText.fromObject = function(object, callback) { + var styles = fabric.util.stylesFromArray(object.styles, object.text); + //copy object to prevent mutation + var objCopy = Object.assign({}, object, { styles: styles }); + parseDecoration(objCopy); + if (objCopy.styles) { + for (var i in objCopy.styles) { + for (var j in objCopy.styles[i]) { + parseDecoration(objCopy.styles[i][j]); + } + } + } + fabric.Object._fromObject('IText', objCopy, callback, 'text'); }; })(); @@ -24088,7 +28466,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { onDeselect: function() { this.isEditing && this.exitEditing(); this.selected = false; - this.callSuper('onDeselect'); }, /** @@ -24129,13 +28506,13 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @private */ _initCanvasHandlers: function(canvas) { - canvas._mouseUpITextHandler = (function() { + canvas._mouseUpITextHandler = function() { if (canvas._iTextInstances) { canvas._iTextInstances.forEach(function(obj) { obj.__isMousedown = false; }); } - }).bind(this); + }; canvas.on('mouse:up', canvas._mouseUpITextHandler); }, @@ -24221,7 +28598,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * Aborts cursor animation and clears all timeouts */ abortCursorAnimation: function() { - var shouldClear = this._currentTickState || this._currentTickCompleteState; + var shouldClear = this._currentTickState || this._currentTickCompleteState, + canvas = this.canvas; this._currentTickState && this._currentTickState.abort(); this._currentTickCompleteState && this._currentTickCompleteState.abort(); @@ -24231,20 +28609,23 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { this._currentCursorOpacity = 0; // to clear just itext area we need to transform the context // it may not be worth it - if (shouldClear) { - this.canvas && this.canvas.clearContext(this.canvas.contextTop || this.ctx); + 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.selectionEnd = this._text.length; this._fireSelectionChanged(); this._updateTextarea(); + return this; }, /** @@ -24252,25 +28633,25 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * @return {String} */ getSelectedText: function() { - return this.text.slice(this.selectionStart, this.selectionEnd); + 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 Surrent 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.charAt(index))) { - while (this._reSpace.test(this.text.charAt(index))) { + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { offset++; index--; } } - while (/\S/.test(this.text.charAt(index)) && index > -1) { + while (/\S/.test(this._text[index]) && index > -1) { offset++; index--; } @@ -24287,13 +28668,13 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { var offset = 0, index = startFrom; // remove space after cursor first - if (this._reSpace.test(this.text.charAt(index))) { - while (this._reSpace.test(this.text.charAt(index))) { + if (this._reSpace.test(this._text[index])) { + while (this._reSpace.test(this._text[index])) { offset++; index++; } } - while (/\S/.test(this.text.charAt(index)) && index < this.text.length) { + while (/\S/.test(this._text[index]) && index < this._text.length) { offset++; index++; } @@ -24309,7 +28690,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { findLineBoundaryLeft: function(startFrom) { var offset = 0, index = startFrom - 1; - while (!/\n/.test(this.text.charAt(index)) && index > -1) { + while (!/\n/.test(this._text[index]) && index > -1) { offset++; index--; } @@ -24325,7 +28706,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { findLineBoundaryRight: function(startFrom) { var offset = 0, index = startFrom; - while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) { + while (!/\n/.test(this._text[index]) && index < this._text.length) { offset++; index++; } @@ -24333,22 +28714,6 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { return startFrom + offset; }, - /** - * Returns number of newlines in selected text - * @return {Number} Number of newlines in selected text - */ - getNumNewLinesInSelectedText: function() { - var selectedText = this.getSelectedText(), - numNewLines = 0; - - for (var i = 0, len = selectedText.length; i < len; i++) { - if (selectedText[i] === '\n') { - numNewLines++; - } - } - return numNewLines; - }, - /** * Finds index corresponding to beginning or end of a word * @param {Number} selectionStart Index of a character @@ -24356,15 +28721,17 @@ 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), - reNonWord = /[ \n\.,;!\?\-]/; + var text = this._text, + index = this._reSpace.test(text[selectionStart]) ? selectionStart - 1 : selectionStart, + _char = text[index], + // wrong + reNonWord = fabric.reNonWord; - while (!reNonWord.test(_char) && index > 0 && index < this.text.length) { + while (!reNonWord.test(_char) && index > 0 && index < text.length) { index += direction; - _char = this.text.charAt(index); + _char = text[index]; } - if (reNonWord.test(_char) && _char !== '\n') { + if (reNonWord.test(_char)) { index += direction === 1 ? 0 : 1; } return index; @@ -24389,6 +28756,8 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { /** * 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; @@ -24399,6 +28768,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { this.selectionEnd = newSelectionEnd; this._fireSelectionChanged(); this._updateTextarea(); + return this; }, /** @@ -24412,13 +28782,15 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { } if (this.canvas) { + this.canvas.calcOffset(); this.exitEditingOnOthers(this.canvas); } this.isEditing = true; - this.selected = true; + this.initHiddenTextarea(e); this.hiddenTextarea.focus(); + this.hiddenTextarea.value = this.text; this._updateTextarea(); this._saveEditingProps(); this._setEditingProps(); @@ -24432,7 +28804,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { } this.canvas.fire('text:editing:entered', { target: this }); this.initMouseMoveHandler(); - this.canvas.renderAll(); + this.canvas.requestRenderAll(); return this; }, @@ -24462,6 +28834,9 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { return; } + // regain focus + document.activeElement !== this.hiddenTextarea && this.hiddenTextarea.focus(); + var newSelectionStart = this.getSelectionStartFromPointer(options.e), currentStart = this.selectionStart, currentEnd = this.selectionEnd; @@ -24499,27 +28874,84 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { } 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() { - if (!this.hiddenTextarea || this.inCompositionMode) { + 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.hiddenTextarea.value = this.text; - this.hiddenTextarea.selectionStart = this.selectionStart; - this.hiddenTextarea.selectionEnd = this.selectionEnd; + 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; - this.hiddenTextarea.style.fontSize = style.fontSize; } }, @@ -24531,25 +28963,31 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { if (!this.canvas) { return { x: 1, y: 1 }; } - var chars = this.text.split(''), - boundaries = this._getCursorBoundaries(chars, 'cursor'), - cursorLocation = this.get2DCursorLocation(), + var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart, + boundaries = this._getCursorBoundaries(desiredPosition), + cursorLocation = this.get2DCursorLocation(desiredPosition), lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex, - charHeight = this.getCurrentCharFontSize(lineIndex, charIndex), + charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight, leftOffset = boundaries.leftOffset, m = this.calcTransformMatrix(), p = { x: boundaries.left + leftOffset, y: boundaries.top + boundaries.topOffset + charHeight }, + retinaScaling = this.canvas.getRetinaScaling(), upperCanvas = this.canvas.upperCanvasEl, - maxWidth = upperCanvas.width - charHeight, - maxHeight = upperCanvas.height - charHeight; + upperCanvasWidth = upperCanvas.width / retinaScaling, + upperCanvasHeight = upperCanvas.height / retinaScaling, + maxWidth = upperCanvasWidth - charHeight, + maxHeight = upperCanvasHeight - charHeight, + scaleX = upperCanvas.clientWidth / upperCanvasWidth, + scaleY = upperCanvas.clientHeight / upperCanvasHeight; p = fabric.util.transformPoint(p, m); p = fabric.util.transformPoint(p, this.canvas.viewportTransform); - + p.x *= scaleX; + p.y *= scaleY; if (p.x < 0) { p.x = 0; } @@ -24567,7 +29005,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { p.x += this.canvas._offset.left; p.y += this.canvas._offset.top; - return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight }; + return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight }; }, /** @@ -24580,6 +29018,7 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { lockMovementX: this.lockMovementX, lockMovementY: this.lockMovementY, hoverCursor: this.hoverCursor, + selectable: this.selectable, defaultCursor: this.canvas && this.canvas.defaultCursor, moveCursor: this.canvas && this.canvas.moveCursor }; @@ -24593,9 +29032,10 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { return; } - this.hoverCursor = this._savedProps.overCursor; + 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; @@ -24612,22 +29052,24 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { */ exitEditing: function() { var isTextChanged = (this._textBeforeEdit !== this.text); + var hiddenTextarea = this.hiddenTextarea; this.selected = false; this.isEditing = false; - this.selectable = true; this.selectionEnd = this.selectionStart; - if (this.hiddenTextarea) { - this.hiddenTextarea.blur && this.hiddenTextarea.blur(); - this.canvas && this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea); - this.hiddenTextarea = null; + if (hiddenTextarea) { + hiddenTextarea.blur && hiddenTextarea.blur(); + hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(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) { @@ -24650,76 +29092,78 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }, /** - * @private + * 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 ) */ - _removeCharsFromTo: function(start, end) { - while (end !== start) { - this._removeSingleCharAndStyle(start + 1); - end--; - } - this.selectionStart = start; - this.selectionEnd = start; - }, - - _removeSingleCharAndStyle: function(index) { - var isBeginningOfLine = this.text[index - 1] === '\n', - indexStyle = isBeginningOfLine ? index : index - 1; - this.removeStyleObject(isBeginningOfLine, indexStyle); - this.text = this.text.slice(0, index - 1) + - this.text.slice(index); - - this._textLines = this._splitTextIntoLines(); - }, - - /** - * Inserts characters where cursor is (replacing selection if one exists) - * @param {String} _chars Characters to insert - * @param {Boolean} useCopiedStyle use fabric.copiedTextStyle - */ - insertChars: function(_chars, useCopiedStyle) { - var style; - - if (this.selectionEnd - this.selectionStart > 1) { - this._removeCharsFromTo(this.selectionStart, this.selectionEnd); - } - //short circuit for block paste - if (!useCopiedStyle && this.isEmptyStyles()) { - this.insertChar(_chars, false); - return; - } - for (var i = 0, len = _chars.length; i < len; i++) { - if (useCopiedStyle) { - style = fabric.util.object.clone(fabric.copiedTextStyle[i], true); + 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]; + } + } } - this.insertChar(_chars[i], i < len - 1, style); } }, /** - * Inserts a character where cursor is - * @param {String} _char Characters to insert - * @param {Boolean} skipUpdate trigger rendering and updates at the end of text insert - * @param {Object} styleObject Style to be inserted for the new char + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can any number? */ - insertChar: function(_char, skipUpdate, styleObject) { - var isEndOfLine = this.text[this.selectionStart] === '\n'; - this.text = this.text.slice(0, this.selectionStart) + - _char + this.text.slice(this.selectionEnd); - this._textLines = this._splitTextIntoLines(); - this.insertStyleObjects(_char, isEndOfLine, styleObject); - this.selectionStart += _char.length; - this.selectionEnd = this.selectionStart; - if (skipUpdate) { - return; - } - this._updateTextarea(); - this.setCoords(); - this._fireSelectionChanged(); - this.fire('changed'); - this.restartCursorIfNeeded(); - if (this.canvas) { - this.canvas.fire('text:changed', { target: this }); - this.canvas.renderAll(); + 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]; + } + } } }, @@ -24732,42 +29176,63 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { }, /** - * Inserts new style object + * Handle insertion of more consecutive style lines for when one or more + * newlines gets added to the text. Since current style needs to be shifted + * first we shift the current style of the number lines needed, then we add + * new lines from the last to the first. * @param {Number} lineIndex Index of a line * @param {Number} charIndex Index of a char - * @param {Boolean} isEndOfLine True if it's end of line + * @param {Number} qty number of lines to add + * @param {Array} copiedStyle Array of objects styles */ - insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { + insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) { + var currentCharStyle, + newLineStyles = {}, + somethingAdded = false, + isEndOfLine = this._unwrappedTextLines[lineIndex].length === charIndex; - this.shiftLineStyles(lineIndex, +1); - - var currentCharStyle = {}, - newLineStyles = {}; - - if (this.styles[lineIndex] && this.styles[lineIndex][charIndex - 1]) { - currentCharStyle = this.styles[lineIndex][charIndex - 1]; + qty || (qty = 1); + this.shiftLineStyles(lineIndex, qty); + if (this.styles[lineIndex]) { + currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1]; } - - // if there's nothing after cursor, - // we clone current char style onto the next (otherwise empty) line - if (isEndOfLine && currentCharStyle) { - newLineStyles[0] = clone(currentCharStyle); - this.styles[lineIndex + 1] = newLineStyles; - } - // otherwise we clone styles of all chars - // after cursor onto the next line, from the beginning - else { - var somethingAdded = false; - 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 + // 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 + if (!(isEndOfLine && charIndex === 0)) { delete this.styles[lineIndex][index]; } } - somethingAdded && (this.styles[lineIndex + 1] = newLineStyles); + } + var styleCarriedOver = false; + if (somethingAdded && !isEndOfLine) { + // if is end of line, the extra style we copied + // is probably not something we want + this.styles[lineIndex + qty] = newLineStyles; + styleCarriedOver = true; + } + if (styleCarriedOver) { + // skip the last line of since we already prepared it. + qty--; + } + // for the all the lines or all the other lines + // we clone current char style onto the next (otherwise empty) line + while (qty > 0) { + if (copiedStyle && copiedStyle[qty - 1]) { + this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty - 1]) }; + } + else if (currentCharStyle) { + this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) }; + } + else { + delete this.styles[lineIndex + qty]; + } + qty--; } this._forceClearCache = true; }, @@ -24776,147 +29241,100 @@ fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { * Inserts style object for a given line/char index * @param {Number} lineIndex Index of a line * @param {Number} charIndex Index of a char - * @param {Object} [style] Style object to insert, if given + * @param {Number} quantity number Style object to insert, if given + * @param {Array} copiedStyle array of style objects */ - insertCharStyleObject: function(lineIndex, charIndex, style) { - - var currentLineStyles = this.styles[lineIndex], - currentLineStylesCloned = clone(currentLineStyles); - - if (charIndex === 0 && !style) { - charIndex = 1; + insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) { + if (!this.styles) { + this.styles = {}; } + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {}; - // shift all char styles by 1 forward + 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 + 1] = currentLineStylesCloned[numericIndex]; - + currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex]; // only delete the style if there was nothing moved there - if (!currentLineStylesCloned[numericIndex - 1]) { + if (!currentLineStylesCloned[numericIndex - quantity]) { delete currentLineStyles[numericIndex]; } } } - var newStyle = style || clone(currentLineStyles[charIndex - 1]); - newStyle && (this.styles[lineIndex][charIndex] = newStyle); 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 {String} _chars Characters at the location where style is inserted - * @param {Boolean} isEndOfLine True if it's end of line - * @param {Object} [styleObject] Style to insert + * @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. */ - insertStyleObjects: function(_chars, isEndOfLine, styleObject) { - // removed shortcircuit over isEmptyStyles - - var cursorLocation = this.get2DCursorLocation(), - lineIndex = cursorLocation.lineIndex, - charIndex = cursorLocation.charIndex; - - if (!this._getLineStyle(lineIndex)) { - this._setLineStyle(lineIndex, {}); - } - - if (_chars === '\n') { - this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); - } - else { - this.insertCharStyleObject(lineIndex, charIndex, styleObject); - } - }, - - /** - * Shifts line styles up or down - * @param {Number} lineIndex Index of a line - * @param {Number} offset Can be -1 or +1 - */ - shiftLineStyles: function(lineIndex, offset) { - // shift all line styles by 1 upward or downward - var clonedStyles = clone(this.styles); - for (var line in clonedStyles) { - var numericLine = parseInt(line, 10); - if (numericLine <= lineIndex) { - delete clonedStyles[numericLine]; + insertNewStyleBlock: function(insertedText, start, copiedStyle) { + var cursorLoc = this.get2DCursorLocation(start, true), + addedLines = [0], linesLength = 0; + // get an array of how many char per lines are being added. + for (var i = 0; i < insertedText.length; i++) { + if (insertedText[i] === '\n') { + linesLength++; + addedLines[linesLength] = 0; + } + else { + addedLines[linesLength]++; } } - 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]; + // for the first line copy the style from the current char position. + 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 test is required in order to close #6841 + // when a pasted buffer begins with a newline then + // this.styles[cursorLoc.lineIndex + i] and copiedStyle[0] + // may be undefined for some reason + if (this.styles[cursorLoc.lineIndex + i] && copiedStyle[0]) { + this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0]; } } + copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1); } - //TODO: evaluate if delete old style lines with offset -1 - }, - - /** - * Removes style object - * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line - * @param {Number} [index] Optional index. When not given, current selectionStart is used. - */ - removeStyleObject: function(isBeginningOfLine, index) { - - var cursorLocation = this.get2DCursorLocation(index), - lineIndex = cursorLocation.lineIndex, - charIndex = cursorLocation.charIndex; - - this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex); - }, - - _getTextOnPreviousLine: function(lIndex) { - return this._textLines[lIndex - 1]; - }, - - _removeStyleObject: function(isBeginningOfLine, cursorLocation, lineIndex, charIndex) { - - if (isBeginningOfLine) { - var textOnPreviousLine = this._getTextOnPreviousLine(cursorLocation.lineIndex), - newCharIndexOnPrevLine = textOnPreviousLine ? textOnPreviousLine.length : 0; - - if (!this.styles[lineIndex - 1]) { - this.styles[lineIndex - 1] = {}; - } - for (charIndex in this.styles[lineIndex]) { - this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine] - = this.styles[lineIndex][charIndex]; - } - this.shiftLineStyles(cursorLocation.lineIndex, -1); - } - else { - var currentLineStyles = this.styles[lineIndex]; - - if (currentLineStyles) { - delete currentLineStyles[charIndex]; - } - var currentLineStylesCloned = clone(currentLineStyles); - // shift all styles by 1 backwards - for (var i in currentLineStylesCloned) { - var numericIndex = parseInt(i, 10); - if (numericIndex >= charIndex && numericIndex !== 0) { - currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex]; - delete currentLineStyles[numericIndex]; - } - } + // we use i outside the loop to get it like linesLength + if (addedLines[i] > 0) { + this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle); } }, /** - * Inserts new line - */ - insertNewline: function() { - this.insertChars('\n'); - }, - - /** - * Set the selectionStart and selectionEnd according to the ne postion of cursor + * 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) { @@ -24984,23 +29402,23 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this.__lastPointer = { }; - this.on('mousedown', this.onMouseDown.bind(this)); + 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 = this.canvas.getPointer(options.e); - + var newPointer = options.pointer; if (this.isTripleClick(newPointer)) { this.fire('tripleclick', options); this._stopEvent(options.e); } - else if (this.isDoubleClick(newPointer)) { - this.fire('dblclick', options); - this._stopEvent(options.e); - } - this.__lastLastClickTime = this.__lastClickTime; this.__lastClickTime = this.__newClickTime; this.__lastPointer = newPointer; @@ -25008,12 +29426,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this.__lastSelected = this.selected; }, - isDoubleClick: function(newPointer) { - return this.__newClickTime - this.__lastClickTime < 500 && - this.__lastPointer.x === newPointer.x && - this.__lastPointer.y === newPointer.y && this.__lastIsEditing; - }, - isTripleClick: function(newPointer) { return this.__newClickTime - this.__lastClickTime < 500 && this.__lastClickTime - this.__lastLastClickTime < 500 && @@ -25038,76 +29450,128 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this.initClicks(); }, + /** + * Default handler for double click, select a word + */ + doubleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }, + + /** + * Default handler for triple click, select a line + */ + tripleClickHandler: function(options) { + if (!this.isEditing) { + return; + } + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }, + /** * Initializes double and triple click event handlers */ initClicks: function() { - this.on('dblclick', function(options) { - this.selectWord(this.getSelectionStartFromPointer(options.e)); - }); - this.on('tripleclick', function(options) { - this.selectLine(this.getSelectionStartFromPointer(options.e)); - }); + this.on('mousedblclick', this.doubleClickHandler); + this.on('tripleclick', this.tripleClickHandler); + }, + + /** + * Default event handler for the basic functionalities needed on _mouseDown + * can be overridden to do something different. + * Scope of this implementation is: find the click position, set selectionStart + * find selectionEnd, initialize the drawing of either cursor or selection area + * initializing a mousedDown on a text area will cancel fabricjs knowledge of + * current compositionMode. It will be set to false. + */ + _mouseDownHandler: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + + this.__isMousedown = true; + + if (this.selected) { + this.inCompositionMode = false; + this.setCursorByClick(options.e); + } + + if (this.isEditing) { + this.__selectionStartOnMouseDown = this.selectionStart; + if (this.selectionStart === this.selectionEnd) { + this.abortCursorAnimation(); + } + this.renderCursorOrSelection(); + } + }, + + /** + * Default event handler for the basic functionalities needed on mousedown:before + * can be overridden to do something different. + * Scope of this implementation is: verify the object is already selected when mousing down + */ + _mouseDownHandlerBefore: function(options) { + if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) { + return; + } + // we want to avoid that an object that was selected and then becomes unselectable, + // may trigger editing mode in some way. + this.selected = this === this.canvas._activeObject; }, /** * Initializes "mousedown" event handler */ initMousedownHandler: function() { - this.on('mousedown', function(options) { - if (!this.editable) { - return; - } - var pointer = this.canvas.getPointer(options.e); - this.__mousedownX = pointer.x; - this.__mousedownY = pointer.y; - 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(); - } - }); - }, - - /** - * @private - */ - _isObjectMoved: function(e) { - var pointer = this.canvas.getPointer(e); - - return this.__mousedownX !== pointer.x || - this.__mousedownY !== pointer.y; + this.on('mousedown', this._mouseDownHandler); + this.on('mousedown:before', this._mouseDownHandlerBefore); }, /** * Initializes "mouseup" event handler */ initMouseupHandler: function() { - this.on('mouseup', function(options) { - this.__isMousedown = false; - if (!this.editable || this._isObjectMoved(options.e)) { + this.on('mouseup', this.mouseUpHandler); + }, + + /** + * standard handler 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.enterEditing(options.e); - if (this.selectionStart === this.selectionEnd) { - this.initDelayedCursor(true); - } - else { - this.renderCursorOrSelection(); - } + 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; - }); + } }, /** @@ -25141,64 +29605,62 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot width = 0, height = 0, charIndex = 0, - newSelectionStart, + lineIndex = 0, + lineLeftOffset, line; - for (var i = 0, len = this._textLines.length; i < len; i++) { - line = this._textLines[i]; - height += this._getHeightOfLine(this.ctx, i) * this.scaleY; - - var widthOfLine = this._getLineWidth(this.ctx, i), - lineLeftOffset = this._getLineLeftOffset(widthOfLine); - - width = lineLeftOffset * this.scaleX; - - for (var j = 0, jlen = line.length; j < jlen; j++) { - - prevWidth = width; - - width += this._getWidthOfChar(this.ctx, line[j], i, this.flipX ? jlen - j : j) * - this.scaleX; - - if (height <= mouseOffset.y || width <= mouseOffset.x) { - charIndex++; - continue; + 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); } - - return this._getNewSelectionStartFromOffset( - mouseOffset, prevWidth, width, charIndex + i, jlen); } - - if (mouseOffset.y < height) { - //this happens just on end of lines. - return this._getNewSelectionStartFromOffset( - mouseOffset, prevWidth, width, charIndex + i - 1, jlen); + else { + break; } } - - // clicked somewhere after all chars, so set at the end - if (typeof newSelectionStart === 'undefined') { - return this.text.length; + lineLeftOffset = this._getLineLeftOffset(lineIndex); + width = lineLeftOffset * this.scaleX; + line = this._textLines[lineIndex]; + // handling of RTL: in order to get things work correctly, + // we assume RTL writing is mirrored compared to LTR writing. + // so in position detection we mirror the X offset, and when is time + // of rendering it, we mirror it again. + if (this.direction === 'rtl') { + mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width; } + 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 ? 0 : 1, + 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; + if (newSelectionStart > this._text.length) { + newSelectionStart = this._text.length; } return newSelectionStart; @@ -25214,16 +29676,30 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 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(); - this.hiddenTextarea.style.cssText = 'white-space: nowrap; position: absolute; top: ' + style.top + - '; left: ' + style.left + '; opacity: 0; width: 1px; height: 1px; z-index: -999;'; - fabric.document.body.appendChild(this.hiddenTextarea); + // 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 + ';'; + + if (this.hiddenTextareaContainer) { + this.hiddenTextareaContainer.appendChild(this.hiddenTextarea); + } + else { + 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.cut.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)); @@ -25236,13 +29712,19 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * @private + * 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: { - 8: 'removeChars', + keysMap: { 9: 'exitEditing', 27: 'exitEditing', - 13: 'insertNewline', 33: 'moveCursorUp', 34: 'moveCursorDown', 35: 'moveCursorRight', @@ -25251,21 +29733,33 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 38: 'moveCursorUp', 39: 'moveCursorRight', 40: 'moveCursorDown', - 46: 'forwardDelete' + }, + + keysMapRtl: { + 9: 'exitEditing', + 27: 'exitEditing', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorLeft', + 36: 'moveCursorRight', + 37: 'moveCursorRight', + 38: 'moveCursorUp', + 39: 'moveCursorLeft', + 40: 'moveCursorDown', }, /** - * @private + * For functionalities on keyUp + ctrl || cmd */ - _ctrlKeysMapUp: { + ctrlKeysMapUp: { 67: 'copy', 88: 'cut' }, /** - * @private + * For functionalities on keyDown + ctrl || cmd */ - _ctrlKeysMapDown: { + ctrlKeysMapDown: { 65: 'selectAll' }, @@ -25275,18 +29769,20 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Handles keyup event + * Handles keydown event + * only used for arrows and combination of modifier keys. * @param {Event} e Event object */ onKeyDown: function(e) { if (!this.isEditing) { return; } - if (e.keyCode in this._keysMap) { - this[this._keysMap[e.keyCode]](e); + var keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap; + if (e.keyCode in keyMap) { + this[keyMap[e.keyCode]](e); } - else if ((e.keyCode in this._ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) { - this[this._ctrlKeysMapDown[e.keyCode]](e); + else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) { + this[this.ctrlKeysMapDown[e.keyCode]](e); } else { return; @@ -25295,11 +29791,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot e.preventDefault(); if (e.keyCode >= 33 && e.keyCode <= 40) { // if i press an arrow key just update selection + this.inCompositionMode = false; this.clearContextTop(); this.renderCursorOrSelection(); } else { - this.canvas && this.canvas.renderAll(); + this.canvas && this.canvas.requestRenderAll(); } }, @@ -25310,19 +29807,19 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @param {Event} e Event object */ onKeyUp: function(e) { - if (!this.isEditing || this._copyDone) { + 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); + 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.renderAll(); + this.canvas && this.canvas.requestRenderAll(); }, /** @@ -25330,37 +29827,98 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @param {Event} e Event object */ onInput: function(e) { - if (!this.isEditing || this.inCompositionMode) { + var fromPaste = this.fromPaste; + this.fromPaste = false; + e && e.stopPropagation(); + if (!this.isEditing) { return; } - var offset = this.selectionStart || 0, - offsetEnd = this.selectionEnd || 0, - textLength = this.text.length, - newTextLength = this.hiddenTextarea.value.length, - diff, charsToInsert, start; - if (newTextLength > textLength) { - //we added some character - start = this._selectionDirection === 'left' ? offsetEnd : offset; - diff = newTextLength - textLength; - charsToInsert = this.hiddenTextarea.value.slice(start, start + diff); + // decisions about style changes. + var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText, + charCount = this._text.length, + nextCharCount = nextText.length, + removedText, insertedText, + charDiff = nextCharCount - charCount, + selectionStart = this.selectionStart, selectionEnd = this.selectionEnd, + selection = selectionStart !== selectionEnd, + copiedStyle, removeFrom, removeTo; + 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; } - else { - //we selected a portion of text and then input something else. - //Internet explorer does not trigger this else - diff = newTextLength - textLength + offsetEnd - offset; - charsToInsert = this.hiddenTextarea.value.slice(offset, offset + diff); - } - this.insertChars(charsToInsert); - e.stopPropagation(); - }, + var textareaSelection = this.fromStringToGraphemeSelection( + this.hiddenTextarea.selectionStart, + this.hiddenTextarea.selectionEnd, + this.hiddenTextarea.value + ); + var backDelete = selectionStart > textareaSelection.selectionStart; + + if (selection) { + removedText = this._text.slice(selectionStart, selectionEnd); + charDiff += selectionEnd - selectionStart; + } + else if (nextCharCount < charCount) { + if (backDelete) { + removedText = this._text.slice(selectionEnd + charDiff, selectionEnd); + } + else { + removedText = this._text.slice(selectionStart, selectionStart - charDiff); + } + } + insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd); + if (removedText && removedText.length) { + if (insertedText.length) { + // let's copy some style before deleting. + // we want to copy the style before the cursor OR the style at the cursor if selection + // is bigger than 0. + copiedStyle = this.getSelectionStyles(selectionStart, selectionStart + 1, false); + // now duplicate the style one for each inserted text. + copiedStyle = insertedText.map(function() { + // this return an array of references, but that is fine since we are + // copying the style later. + return copiedStyle[0]; + }); + } + if (selection) { + removeFrom = selectionStart; + removeTo = selectionEnd; + } + else if (backDelete) { + // detect differences between forwardDelete and backDelete + removeFrom = selectionEnd - removedText.length; + removeTo = selectionEnd; + } + else { + removeFrom = selectionEnd; + removeTo = selectionEnd + removedText.length; + } + this.removeStyleFromTo(removeFrom, removeTo); + } + if (insertedText.length) { + if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) { + copiedStyle = fabric.copiedTextStyle; + } + this.insertNewStyleBlock(insertedText, selectionStart, copiedStyle); + } + 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; - this.prevCompositionLength = 0; - this.compositionStart = this.selectionStart; }, /** @@ -25370,52 +29928,32 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this.inCompositionMode = false; }, - /** - * Composition update - */ + // /** + // * Composition update + // */ onCompositionUpdate: function(e) { - var data = e.data; - this.selectionStart = this.compositionStart; - this.selectionEnd = this.selectionEnd === this.selectionStart ? - this.compositionStart + this.prevCompositionLength : this.selectionEnd; - this.insertChars(data, false); - this.prevCompositionLength = data.length; - }, - - /** - * Forward delete - */ - forwardDelete: function(e) { - if (this.selectionStart === this.selectionEnd) { - if (this.selectionStart === this.text.length) { - return; - } - this.moveCursorRight(e); - } - this.removeChars(e); + this.compositionStart = e.target.selectionStart; + this.compositionEnd = e.target.selectionEnd; + this.updateTextareaPosition(); }, /** * Copies selected text * @param {Event} e Event object */ - copy: function(e) { + copy: function() { if (this.selectionStart === this.selectionEnd) { //do not cut-copy if no selection return; } - var selectedText = this.getSelectedText(), - clipboardData = this._getClipboardData(e); - // Check for backward compatibility with old browsers - if (clipboardData) { - clipboardData.setData('text', selectedText); + fabric.copiedText = this.getSelectedText(); + if (!fabric.disableStyleCopyPaste) { + fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true); + } + else { + fabric.copiedTextStyle = null; } - - fabric.copiedText = selectedText; - fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd); - e.stopImmediatePropagation(); - e.preventDefault(); this._copyDone = true; }, @@ -25423,40 +29961,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * Pastes text * @param {Event} e Event object */ - paste: function(e) { - var copiedText = null, - clipboardData = this._getClipboardData(e), - useCopiedStyle = true; - - // Check for backward compatibility with old browsers - if (clipboardData) { - copiedText = clipboardData.getData('text').replace(/\r/g, ''); - if (!fabric.copiedTextStyle || fabric.copiedText !== copiedText) { - useCopiedStyle = false; - } - } - else { - copiedText = fabric.copiedText; - } - - if (copiedText) { - this.insertChars(copiedText, useCopiedStyle); - } - e.stopImmediatePropagation(); - e.preventDefault(); - }, - - /** - * Cuts text - * @param {Event} e Event object - */ - cut: function(e) { - if (this.selectionStart === this.selectionEnd) { - return; - } - - this.copy(e); - this.removeChars(e); + paste: function() { + this.fromPaste = true; }, /** @@ -25476,13 +29982,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @return {Number} widthBeforeCursor width before cursor */ _getWidthBeforeCursor: function(lineIndex, charIndex) { - var textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex), - widthOfLine = this._getLineWidth(this.ctx, lineIndex), - widthBeforeCursor = this._getLineLeftOffset(widthOfLine), _char; + var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound; - for (var i = 0, len = textBeforeCursor.length; i < len; i++) { - _char = textBeforeCursor[i]; - widthBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + if (charIndex > 0) { + bound = this.__charBounds[lineIndex][charIndex - 1]; + widthBeforeCursor += bound.left + bound.width; } return widthBeforeCursor; }, @@ -25500,14 +30004,13 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot // 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; + 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 + 2; + return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex); }, /** @@ -25542,49 +30045,42 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot var charIndex = cursorLocation.charIndex, widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), - textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex); + 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; + return -this._textLines[lineIndex - 1].length + + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset); }, /** - * find for a given width it founds the matching character. + * for a given width it founds the matching character. * @private */ _getIndexOnLine: function(lineIndex, width) { - var widthOfLine = this._getLineWidth(this.ctx, lineIndex), - textOnLine = this._textLines[lineIndex], - lineLeftOffset = this._getLineLeftOffset(widthOfLine), + var line = this._textLines[lineIndex], + lineLeftOffset = this._getLineLeftOffset(lineIndex), widthOfCharsOnLine = lineLeftOffset, - indexOnLine = 0, - foundMatch; - - for (var j = 0, jlen = textOnLine.length; j < jlen; j++) { - - var _char = textOnLine[j], - widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); - - widthOfCharsOnLine += widthOfChar; + 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 - widthOfChar, + 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 = textOnLine.length - 1; + indexOnLine = line.length - 1; } return indexOnLine; @@ -25596,7 +30092,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @param {Event} e Event object */ moveCursorDown: function(e) { - if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } this._moveCursorUpOrDown('Down', e); @@ -25645,8 +30141,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ moveCursorWithShift: function(offset) { var newSelection = this._selectionDirection === 'left' - ? this.selectionStart + offset - : this.selectionEnd + offset; + ? this.selectionStart + offset + : this.selectionEnd + offset; this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); return offset !== 0; }, @@ -25694,7 +30190,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot this[prop] += direction === 'Left' ? -1 : 1; return true; } - if (typeof newValue !== undefined && this[prop] !== newValue) { + if (typeof newValue !== 'undefined' && this[prop] !== newValue) { this[prop] = newValue; return true; } @@ -25751,7 +30247,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @param {Event} e Event object */ moveCursorRight: function(e) { - if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) { + if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) { return; } this._moveCursorLeftOrRight('Right', e); @@ -25788,7 +30284,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { return this._moveRight(e, 'selectionStart'); } - else if (this.selectionEnd !== this.text.length) { + else if (this.selectionEnd !== this._text.length) { this._selectionDirection = 'right'; return this._moveRight(e, 'selectionEnd'); } @@ -25813,108 +30309,273 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * Removes characters selected by selection - * @param {Event} e Event object + * 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(e) { - if (this.selectionStart === this.selectionEnd) { - this._removeCharsNearCursor(e); + removeChars: function(start, end) { + if (typeof end === 'undefined') { + end = start + 1; } - else { - this._removeCharsFromTo(this.selectionStart, this.selectionEnd); - } - + this.removeStyleFromTo(start, end); + this._text.splice(start, end - start); + this.text = this._text.join(''); this.set('dirty', true); - this.setSelectionEnd(this.selectionStart); - + if (this._shouldClearDimensionCache()) { + this.initDimensions(); + this.setCoords(); + } this._removeExtraneousStyles(); - - this.canvas && this.canvas.renderAll(); - - this.setCoords(); - this.fire('changed'); - this.canvas && this.canvas.fire('text:changed', { target: this }); }, /** - * @private - * @param {Event} e Event object + * 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 */ - _removeCharsNearCursor: function(e) { - if (this.selectionStart === 0) { - return; + insertChars: function(text, style, start, end) { + if (typeof end === 'undefined') { + end = start; } - if (e.metaKey) { - // remove all till the start of current line - var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); + 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(); + }, - this._removeCharsFromTo(leftLineBoundary, this.selectionStart); - this.setSelectionStart(leftLineBoundary); - } - else if (e.altKey) { - // remove all till the start of current word - var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart); - - this._removeCharsFromTo(leftWordBoundary, this.selectionStart); - this.setSelectionStart(leftWordBoundary); - } - else { - this._removeSingleCharAndStyle(this.selectionStart); - this.setSelectionStart(this.selectionStart - 1); - } - } }); /* _TO_SVG_START_ */ (function() { var toFixed = fabric.util.toFixed, - NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + multipleSpacesRegex = / +/g; - fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ { /** - * @private + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance */ - _setSVGTextLineText: function(lineIndex, textSpans, height, textLeftOffset, textTopOffset, textBgRects) { - if (!this._getLineStyle(lineIndex)) { - fabric.Text.prototype._setSVGTextLineText.call(this, - lineIndex, textSpans, height, textLeftOffset, textTopOffset); - } - else { - this._setSVGTextLineChars( - lineIndex, textSpans, height, textLeftOffset, textBgRects); - } + _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 */ - _setSVGTextLineChars: function(lineIndex, textSpans, height, textLeftOffset, textBgRects) { + _getSVGLeftTopOffsets: function() { + return { + textLeft: -this.width / 2, + textTop: -this.height / 2, + lineTop: this.getHeightOfLine(0) + }; + }, - var chars = this._textLines[lineIndex], - charOffset = 0, - lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)) - this.width / 2, - lineOffset = this._getSVGLineTopOffset(lineIndex), - heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); + /** + * @private + */ + _wrapSVGTextAndBg: function(textAndBg) { + var noShadow = true, + textDecoration = this.getSvgTextDecoration(this); + return [ + textAndBg.textBgRects.join(''), + '\t\t', + textAndBg.textSpans.join(''), + '\n' + ]; + }, - for (var i = 0, len = chars.length; i < len; i++) { - var styleDecl = this._getStyleDeclaration(lineIndex, i) || { }; + /** + * @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); - textSpans.push( - this._createTextCharSpan( - chars[i], styleDecl, lineLeftOffset, lineOffset.lineTop + lineOffset.offset, charOffset)); - - var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); - - if (styleDecl.textBackgroundColor) { - textBgRects.push( - this._createTextCharBg( - styleDecl, lineLeftOffset, lineOffset.lineTop, heightOfLine, charWidth, charOffset)); + // 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); } - - charOffset += charWidth; + 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 = fabric.util.hasStyleChanged(actualStyle, nextStyle, true); + } + 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() + '"'; }, /** @@ -25923,9 +30584,9 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot _getSVGLineTopOffset: function(lineIndex) { var lineTopOffset = 0, lastHeight = 0; for (var j = 0; j < lineIndex; j++) { - lineTopOffset += this._getHeightOfLine(this.ctx, j); + lineTopOffset += this.getHeightOfLine(j); } - lastHeight = this._getHeightOfLine(this.ctx, j); + lastHeight = this.getHeightOfLine(j); return { lineTop: lineTopOffset, offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) @@ -25933,45 +30594,14 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }, /** - * @private + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} */ - _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) { - return [ - '\t\t\n' - ].join(''); + getSvgStyles: function(skipShadow) { + var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow); + return svgStyle + ' white-space: pre;'; }, - - /** - * @private - */ - _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, charOffset) { - - var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ - visible: true, - fill: this.fill, - stroke: this.stroke, - type: 'text', - getSvgFilter: fabric.Object.prototype.getSvgFilter - }, styleDecl)); - - return [ - '\t\t\t', - fabric.util.string.escapeXml(_char), - '\n' - ].join(''); - } }); })(); /* _TO_SVG_END_ */ @@ -26025,11 +30655,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot */ __cachedLines: null, - /** - * Override standard Object class values - */ - lockScalingY: true, - /** * Override standard Object class values */ @@ -26042,53 +30667,54 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot noScaleCache: false, /** - * Constructor. Some scaling related property values are forced. Visibility - * of controls is also fixed; only the rotation and width controls are - * made available. - * @param {String} text Text string - * @param {Object} [options] Options object - * @return {fabric.Textbox} thisArg + * Properties which when set cause object to change dimensions + * @type Object + * @private */ - initialize: function(text, options) { + _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'), - this.callSuper('initialize', text, options); - this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); - this.ctx = this.objectCaching ? this._cacheContext : fabric.util.createCanvasElement().getContext('2d'); - // add width to this list of props that effect line wrapping. - this._dimensionAffectingProps.push('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/japanese + * @type Boolean + * @since 2.6.0 + */ + splitByGrapheme: false, /** * Unlike superclass's version of this function, Textbox does not update * its width. - * @param {CanvasRenderingContext2D} ctx Context to use for measurements * @private * @override */ - _initDimensions: function(ctx) { + initDimensions: function() { if (this.__skipDimension) { return; } - - if (!ctx) { - ctx = fabric.util.createCanvasElement().getContext('2d'); - this._setTextStyles(ctx); - this.clearContextTop(); - } - + 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._textLines = this._splitTextIntoLines(ctx); + 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._clearCache(); - this.height = this._getTextHeight(ctx); + this.height = this.calcTextHeight(); + this.saveState({ propertySet: '_dimensionAffectingProps' }); }, /** @@ -26098,19 +30724,19 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * which is only sufficient for Text / IText * @private */ - _generateStyleMap: function() { + _generateStyleMap: function(textInfo) { var realLineCount = 0, realLineCharCount = 0, charCount = 0, map = {}; - for (var i = 0; i < this._textLines.length; i++) { - if (this.text[charCount] === '\n' && i > 0) { + for (var i = 0; i < textInfo.graphemeLines.length; i++) { + if (textInfo.graphemeText[charCount] === '\n' && i > 0) { realLineCharCount = 0; charCount++; realLineCount++; } - else if (this.text[charCount] === ' ' && i > 0) { + 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++; @@ -26118,29 +30744,77 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot map[i] = { line: realLineCount, offset: realLineCharCount }; - charCount += this._textLines[i].length; - realLineCharCount += this._textLines[i].length; + charCount += textInfo.graphemeLines[i].length; + realLineCharCount += textInfo.graphemeLines[i].length; } return map; }, + /** + * Returns true if object has a style property or has it on a specified line + * @param {Number} lineIndex + * @return {Boolean} + */ + styleHas: function(property, lineIndex) { + if (this._styleMap && !this.isWrapping) { + var map = this._styleMap[lineIndex]; + if (map) { + lineIndex = map.line; + } + } + return fabric.Text.prototype.styleHas.call(this, property, lineIndex); + }, + + /** + * Returns true if object has no styling or no styling in a line + * @param {Number} lineIndex , lineIndex is on wrapped lines. + * @return {Boolean} + */ + isEmptyStyles: function(lineIndex) { + if (!this.styles) { + return true; + } + var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false, + map = this._styleMap[lineIndex], mapNextLine = this._styleMap[lineIndex + 1]; + if (map) { + lineIndex = map.line; + offset = map.offset; + } + if (mapNextLine) { + nextLineIndex = mapNextLine.line; + shouldLimit = nextLineIndex === lineIndex; + nextOffset = mapNextLine.offset; + } + obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] }; + for (var p1 in obj) { + for (var p2 in obj[p1]) { + if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + } + return true; + }, + /** * @param {Number} lineIndex * @param {Number} charIndex - * @param {Boolean} [returnCloneOrEmpty=false] * @private */ - _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) { - if (this._styleMap) { + _getStyleDeclaration: function(lineIndex, charIndex) { + if (this._styleMap && !this.isWrapping) { var map = this._styleMap[lineIndex]; if (!map) { - return returnCloneOrEmpty ? { } : null; + return null; } lineIndex = map.line; charIndex = map.offset + charIndex; } - return this.callSuper('_getStyleDeclaration', lineIndex, charIndex, returnCloneOrEmpty); + return this.callSuper('_getStyleDeclaration', lineIndex, charIndex); }, /** @@ -26166,36 +30840,31 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot 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]; + 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, style) { + _setLineStyle: function(lineIndex) { var map = this._styleMap[lineIndex]; - this.styles[map.line] = style; - }, - - /** - * @param {Number} lineIndex - * @private - */ - _deleteLineStyle: function(lineIndex) { - var map = this._styleMap[lineIndex]; - delete this.styles[map.line]; + this.styles[map.line] = {}; }, /** @@ -26203,23 +30872,23 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * 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 {CanvasRenderingContext2D} ctx Context to use for measurements - * @param {String} text The string of text that is split into lines + * @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(ctx, text) { - var lines = text.split(this._reNewline), wrapped = [], i; - + _wrapText: function(lines, desiredWidth) { + var wrapped = [], i; + this.isWrapping = true; for (i = 0; i < lines.length; i++) { - wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], 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 @@ -26227,48 +30896,57 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @returns {number} * @private */ - _measureText: function(ctx, text, lineIndex, charOffset) { - var width = 0; + _measureWord: function(word, lineIndex, charOffset) { + var width = 0, prevGrapheme, skipLeft = true; charOffset = charOffset || 0; - for (var i = 0, len = text.length; i < len; i++) { - width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset); + 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 {CanvasRenderingContext2D} ctx Context to use for measurements - * @param {String} text The string of text to split into lines + * @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(ctx, text, lineIndex) { - var lineWidth = 0, - lines = [], - line = '', - words = text.split(' '), - word = '', - offset = 0, - infix = ' ', - wordWidth = 0, - infixWidth = 0, + _wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) { + var lineWidth = 0, + splitByGrapheme = this.splitByGrapheme, + graphemeLines = [], + line = [], + // spaces in different languages? + 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 = this._getWidthOfCharSpacing(); - + additionalSpace = 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++) { - word = words[i]; - wordWidth = this._measureText(ctx, word, lineIndex, offset); - + // 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 >= this.width && !lineJustStarted) { - lines.push(line); - line = ''; + if (lineWidth > desiredWidth && !lineJustStarted) { + graphemeLines.push(line); + line = []; lineWidth = wordWidth; lineJustStarted = true; } @@ -26276,12 +30954,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot lineWidth += additionalSpace; } - if (!lineJustStarted) { - line += infix; + if (!lineJustStarted && !splitByGrapheme) { + line.push(infix); } - line += word; + line = line.concat(word); - infixWidth = this._measureText(ctx, infix, lineIndex, offset); + infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset); offset++; lineJustStarted = false; // keep track of largest word @@ -26290,131 +30968,81 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot } } - i && lines.push(line); + i && graphemeLines.push(line); - if (largestWordWidth > this.dynamicMinWidth) { - this.dynamicMinWidth = largestWordWidth - additionalSpace; + if (largestWordWidth + reservedSpace > this.dynamicMinWidth) { + this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace; } - - return lines; - }, - /** - * Gets lines of text to render in the Textbox. This function calculates - * text wrapping on the fly everytime it is called. - * @returns {Array} Array of lines in the Textbox. - * @override - */ - _splitTextIntoLines: function(ctx) { - ctx = ctx || this.ctx; - var originalAlign = this.textAlign; - this._styleMap = null; - ctx.save(); - this._setTextStyles(ctx); - this.textAlign = 'left'; - var lines = this._wrapText(ctx, this.text); - this.textAlign = originalAlign; - ctx.restore(); - this._textLines = lines; - this._styleMap = this._generateStyleMap(); - return lines; + return graphemeLines; }, /** - * When part of a group, we don't want the Textbox's scale to increase if - * the group's increases. That's why we reduce the scale of the Textbox by - * the amount that the group's increases. This is to maintain the effective - * scale of the Textbox at 1, so that font-size values make sense. Otherwise - * the same font-size value would result in different actual size depending - * on the value of the scale. - * @param {String} key - * @param {*} value + * 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} */ - setOnGroup: function(key, value) { - if (key === 'scaleX') { - this.set('scaleX', Math.abs(1 / value)); - this.set('width', (this.get('width') * value) / - (typeof this.__oldScaleX === 'undefined' ? 1 : this.__oldScaleX)); - this.__oldScaleX = value; + 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; }, /** - * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start). - * Overrides the superclass function to take into account text wrapping. - * - * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + * Detect if a line has a linebreak and so we need to account for it when moving + * and counting style. + * @return Number */ - get2DCursorLocation: function(selectionStart) { - if (typeof selectionStart === 'undefined') { - selectionStart = this.selectionStart; + missingNewlineOffset: function(lineIndex) { + if (this.splitByGrapheme) { + return this.isEndOfWrapping(lineIndex) ? 1 : 0; } - - var numLines = this._textLines.length, - removed = 0; - - for (var i = 0; i < numLines; i++) { - var line = this._textLines[i], - lineLen = line.length; - - if (selectionStart <= removed + lineLen) { - return { - lineIndex: i, - charIndex: selectionStart - removed - }; - } - - removed += lineLen; - - if (this.text[removed] === '\n' || this.text[removed] === ' ') { - removed++; - } - } - - return { - lineIndex: numLines - 1, - charIndex: this._textLines[numLines - 1].length - }; + return 1; }, /** - * Overrides superclass function and uses text wrapping data to get cursor - * boundary offsets instead of the array of chars. - * @param {Array} chars Unused - * @param {String} typeOfBoundaries Can be 'cursor' or 'selection' - * @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set. - */ - _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) { - var topOffset = 0, - leftOffset = 0, - cursorLocation = this.get2DCursorLocation(), - lineChars = this._textLines[cursorLocation.lineIndex].split(''), - lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, cursorLocation.lineIndex)); - - for (var i = 0; i < cursorLocation.charIndex; i++) { - leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i); + * 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(''); } - - for (i = 0; i < cursorLocation.lineIndex; i++) { - topOffset += this._getHeightOfLine(this.ctx, i); - } - - if (typeOfBoundaries === 'cursor') { - topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex) - / this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex) - * (1 - this._fontSizeFraction); - } - - return { - top: topOffset, - left: leftOffset, - lineLeft: lineLeftOffset - }; + 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 @@ -26422,7 +31050,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { - return this.callSuper('toObject', ['minWidth'].concat(propertiesToInclude)); + return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)); } }); @@ -26432,419 +31060,128 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot * @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 - * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first - * @return {fabric.Textbox} instance of fabric.Textbox */ - fabric.Textbox.fromObject = function(object, callback, forceAsync) { - return fabric.Object._fromObject('Textbox', object, callback, forceAsync, 'text'); + fabric.Textbox.fromObject = function(object, callback) { + var styles = fabric.util.stylesFromArray(object.styles, object.text); + //copy object to prevent mutation + var objCopy = Object.assign({}, object, { styles: styles }); + return fabric.Object._fromObject('Textbox', objCopy, callback, 'text'); }; - - /** - * Returns the default controls visibility required for Textboxes. - * @returns {Object} - */ - fabric.Textbox.getTextboxControlVisibility = function() { - return { - tl: false, - tr: false, - br: false, - bl: false, - ml: true, - mt: false, - mr: true, - mb: false, - mtr: true - }; - }; - })(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; + var controlsUtils = fabric.controlsUtils, + scaleSkewStyleHandler = controlsUtils.scaleSkewCursorStyleHandler, + scaleStyleHandler = controlsUtils.scaleCursorStyleHandler, + scalingEqually = controlsUtils.scalingEqually, + scalingYOrSkewingX = controlsUtils.scalingYOrSkewingX, + scalingXOrSkewingY = controlsUtils.scalingXOrSkewingY, + scaleOrSkewActionName = controlsUtils.scaleOrSkewActionName, + objectControls = fabric.Object.prototype.controls; - fabric.Canvas.prototype._setObjectScale = function(localMouse, transform, - lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { - - var t = transform.target; - if (t instanceof fabric.Textbox) { - var w = t.width * ((localMouse.x / transform.scaleX) / (t.width + t.strokeWidth)); - if (w >= t.getMinWidth()) { - t.set('width', w); - return true; - } - } - else { - return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, - lockScalingX, lockScalingY, by, lockScalingFlip, _dim); - } - }; - - /** - * Sets controls of this group to the Textbox's special configuration if - * one is present in the group. Deletes _controlsVisibility otherwise, so that - * it gets initialized to default value at runtime. - */ - fabric.Group.prototype._refreshControlsVisibility = function() { - if (typeof fabric.Textbox === 'undefined') { - return; - } - for (var i = this._objects.length; i--;) { - if (this._objects[i] instanceof fabric.Textbox) { - this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); - return; - } - } - }; - - 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]; - } - } - }, - - /** - * Inserts style object for a given line/char index - * @param {Number} lineIndex Index of a line - * @param {Number} charIndex Index of a char - * @param {Object} [style] Style object to insert, if given - */ - insertCharStyleObject: function(lineIndex, charIndex, style) { - // adjust lineIndex and charIndex - var map = this._styleMap[lineIndex]; - lineIndex = map.line; - charIndex = map.offset + charIndex; - - fabric.IText.prototype.insertCharStyleObject.apply(this, [lineIndex, charIndex, style]); - }, - - /** - * Inserts new style object - * @param {Number} lineIndex Index of a line - * @param {Number} charIndex Index of a char - * @param {Boolean} isEndOfLine True if it's end of line - */ - insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { - // adjust lineIndex and charIndex - var map = this._styleMap[lineIndex]; - lineIndex = map.line; - charIndex = map.offset + charIndex; - - fabric.IText.prototype.insertNewlineStyleObject.apply(this, [lineIndex, charIndex, isEndOfLine]); - }, - - /** - * Shifts line styles up or down. This function is slightly different than the one in - * itext_behaviour as it takes into account the styleMap. - * - * @param {Number} lineIndex Index of a line - * @param {Number} offset Can be -1 or +1 - */ - shiftLineStyles: function(lineIndex, offset) { - // shift all line styles by 1 upward - var map = this._styleMap[lineIndex]; - // adjust line index - lineIndex = map.line; - fabric.IText.prototype.shiftLineStyles.call(this, lineIndex, offset); - }, - - /** - * Figure out programatically the text on previous actual line (actual = separated by \n); - * - * @param {Number} lIndex - * @returns {String} - * @private - */ - _getTextOnPreviousLine: function(lIndex) { - var textOnPreviousLine = this._textLines[lIndex - 1]; - - while (this._styleMap[lIndex - 2] && this._styleMap[lIndex - 2].line === this._styleMap[lIndex - 1].line) { - textOnPreviousLine = this._textLines[lIndex - 2] + textOnPreviousLine; - - lIndex--; - } - - return textOnPreviousLine; - }, - - /** - * Removes style object - * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line - * @param {Number} [index] Optional index. When not given, current selectionStart is used. - */ - removeStyleObject: function(isBeginningOfLine, index) { - - var cursorLocation = this.get2DCursorLocation(index), - map = this._styleMap[cursorLocation.lineIndex], - lineIndex = map.line, - charIndex = map.offset + cursorLocation.charIndex; - this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex); - } + objectControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, }); -})(); + objectControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingXOrSkewingY, + getActionName: scaleOrSkewActionName, + }); -(function() { - var override = fabric.IText.prototype._getNewSelectionStartFromOffset; - /** - * Overrides the IText implementation and adjusts character index as there is not always a linebreak - * - * @param {Number} mouseOffset - * @param {Number} prevWidth - * @param {Number} width - * @param {Number} index - * @param {Number} jlen - * @returns {Number} - */ - fabric.IText.prototype._getNewSelectionStartFromOffset = function(mouseOffset, prevWidth, width, index, jlen) { - index = override.call(this, mouseOffset, prevWidth, width, index, jlen); + objectControls.mb = new fabric.Control({ + x: 0, + y: 0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); - // the index passed into the function is padded by the amount of lines from _textLines (to account for \n) - // we need to remove this padding, and pad it by actual lines, and / or spaces that are meant to be there - var tmp = 0, - removed = 0; + objectControls.mt = new fabric.Control({ + x: 0, + y: -0.5, + cursorStyleHandler: scaleSkewStyleHandler, + actionHandler: scalingYOrSkewingX, + getActionName: scaleOrSkewActionName, + }); - // account for removed characters - for (var i = 0; i < this._textLines.length; i++) { - tmp += this._textLines[i].length; + objectControls.tl = new fabric.Control({ + x: -0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); - if (tmp + removed >= index) { - break; - } + objectControls.tr = new fabric.Control({ + x: 0.5, + y: -0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); - if (this.text[tmp + removed] === '\n' || this.text[tmp + removed] === ' ') { - removed++; - } - } + objectControls.bl = new fabric.Control({ + x: -0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); - return index - i + removed; - }; -})(); + objectControls.br = new fabric.Control({ + x: 0.5, + y: 0.5, + cursorStyleHandler: scaleStyleHandler, + actionHandler: scalingEqually + }); + objectControls.mtr = new fabric.Control({ + x: 0, + y: -0.5, + actionHandler: controlsUtils.rotationWithSnapping, + cursorStyleHandler: controlsUtils.rotationStyleHandler, + offsetY: -40, + withConnection: true, + actionName: 'rotate', + }); -(function() { + if (fabric.Textbox) { + // this is breaking the prototype inheritance, no time / ideas to fix it. + // is important to document that if you want to have all objects to have a + // specific custom control, you have to add it to Object prototype and to Textbox + // prototype. The controls are shared as references. So changes to control `tr` + // can still apply to all objects if needed. + var textBoxControls = fabric.Textbox.prototype.controls = { }; - if (typeof document !== 'undefined' && typeof window !== 'undefined') { - return; - } + textBoxControls.mtr = objectControls.mtr; + textBoxControls.tr = objectControls.tr; + textBoxControls.br = objectControls.br; + textBoxControls.tl = objectControls.tl; + textBoxControls.bl = objectControls.bl; + textBoxControls.mt = objectControls.mt; + textBoxControls.mb = objectControls.mb; - var DOMParser = require('xmldom').DOMParser, - URL = require('url'), - HTTP = require('http'), - HTTPS = require('https'), - - Canvas = require('canvas'), - Image = require('canvas').Image; - - /** @private */ - function request(url, encoding, callback) { - var oURL = URL.parse(url); - - // detect if http or https is used - if ( !oURL.port ) { - oURL.port = ( oURL.protocol.indexOf('https:') === 0 ) ? 443 : 80; - } - - // assign request handler based on protocol - var reqHandler = (oURL.protocol.indexOf('https:') === 0 ) ? HTTPS : HTTP, - req = reqHandler.request({ - hostname: oURL.hostname, - port: oURL.port, - path: oURL.path, - method: 'GET' - }, function(response) { - var body = ''; - if (encoding) { - response.setEncoding(encoding); - } - response.on('end', function () { - callback(body); - }); - response.on('data', function (chunk) { - if (response.statusCode === 200) { - body += chunk; - } - }); - }); - - req.on('error', function(err) { - if (err.errno === process.ECONNREFUSED) { - fabric.log('ECONNREFUSED: connection refused to ' + oURL.hostname + ':' + oURL.port); - } - else { - fabric.log(err.message); - } - callback(null); + textBoxControls.mr = new fabric.Control({ + x: 0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', }); - req.end(); - } - - /** @private */ - function requestFs(path, callback) { - var fs = require('fs'); - fs.readFile(path, function (err, data) { - if (err) { - fabric.log(err); - throw err; - } - else { - callback(data); - } + textBoxControls.ml = new fabric.Control({ + x: -0.5, + y: 0, + actionHandler: controlsUtils.changeWidth, + cursorStyleHandler: scaleSkewStyleHandler, + actionName: 'resizing', }); } - - fabric.util.loadImage = function(url, callback, context) { - function createImageAndCallBack(data) { - if (data) { - img.src = new Buffer(data, 'binary'); - // preserving original url, which seems to be lost in node-canvas - img._src = url; - callback && callback.call(context, img); - } - else { - img = null; - callback && callback.call(context, null, true); - } - } - var img = new Image(); - if (url && (url instanceof Buffer || url.indexOf('data') === 0)) { - img.src = img._src = url; - callback && callback.call(context, img); - } - else if (url && url.indexOf('http') !== 0) { - requestFs(url, createImageAndCallBack); - } - else if (url) { - request(url, 'binary', createImageAndCallBack); - } - else { - callback && callback.call(context, url); - } - }; - - fabric.loadSVGFromURL = function(url, callback, reviver) { - url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); - if (url.indexOf('http') !== 0) { - requestFs(url, function(body) { - fabric.loadSVGFromString(body.toString(), callback, reviver); - }); - } - else { - request(url, '', function(body) { - fabric.loadSVGFromString(body, callback, reviver); - }); - } - }; - - fabric.loadSVGFromString = function(string, callback, reviver) { - var doc = new DOMParser().parseFromString(string); - fabric.parseSVGDocument(doc.documentElement, function(results, options) { - callback && callback(results, options); - }, reviver); - }; - - fabric.util.getScript = function(url, callback) { - request(url, '', function(body) { - // eslint-disable-next-line no-eval - eval(body); - callback && callback(); - }); - }; - - // fabric.util.createCanvasElement = function(_, width, height) { - // return new Canvas(width, height); - // } - - /** - * Only available when running fabric on node.js - * @param {Number} width Canvas width - * @param {Number} height Canvas height - * @param {Object} [options] Options to pass to FabricCanvas. - * @param {Object} [nodeCanvasOptions] Options to pass to NodeCanvas. - * @return {Object} wrapped canvas instance - */ - fabric.createCanvasForNode = function(width, height, options, nodeCanvasOptions) { - nodeCanvasOptions = nodeCanvasOptions || options; - - var canvasEl = fabric.document.createElement('canvas'), - nodeCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions), - nodeCacheCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions); - - // jsdom doesn't create style on canvas element, so here be temp. workaround - canvasEl.style = { }; - - canvasEl.width = nodeCanvas.width; - canvasEl.height = nodeCanvas.height; - options = options || { }; - options.nodeCanvas = nodeCanvas; - options.nodeCacheCanvas = nodeCacheCanvas; - var FabricCanvas = fabric.Canvas || fabric.StaticCanvas, - fabricCanvas = new FabricCanvas(canvasEl, options); - fabricCanvas.nodeCanvas = nodeCanvas; - fabricCanvas.nodeCacheCanvas = nodeCacheCanvas; - fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); - fabricCanvas.contextCache = nodeCacheCanvas.getContext('2d'); - fabricCanvas.Font = Canvas.Font; - return fabricCanvas; - }; - - var originaInitStatic = fabric.StaticCanvas.prototype._initStatic; - fabric.StaticCanvas.prototype._initStatic = function(el, options) { - el = el || fabric.document.createElement('canvas'); - this.nodeCanvas = new Canvas(el.width, el.height); - this.nodeCacheCanvas = new Canvas(el.width, el.height); - originaInitStatic.call(this, el, options); - this.contextContainer = this.nodeCanvas.getContext('2d'); - this.contextCache = this.nodeCacheCanvas.getContext('2d'); - this.Font = Canvas.Font; - }; - - /** @ignore */ - fabric.StaticCanvas.prototype.createPNGStream = function() { - return this.nodeCanvas.createPNGStream(); - }; - - fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { - return this.nodeCanvas.createJPEGStream(opts); - }; - - fabric.StaticCanvas.prototype._initRetinaScaling = function() { - if (!this._isRetinaScaling()) { - return; - } - - this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio); - this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio); - this.nodeCanvas.width = this.width * fabric.devicePixelRatio; - this.nodeCanvas.height = this.height * fabric.devicePixelRatio; - this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio); - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype._initRetinaScaling = fabric.StaticCanvas.prototype._initRetinaScaling; - } - - var origSetBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension; - fabric.StaticCanvas.prototype._setBackstoreDimension = function(prop, value) { - origSetBackstoreDimension.call(this, prop, value); - this.nodeCanvas[prop] = value; - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype._setBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension; - } - })(); diff --git a/src/pretix/static/fabric/fabric.min.js b/src/pretix/static/fabric/fabric.min.js index 20c989df9..f679affaa 100644 --- a/src/pretix/static/fabric/fabric.min.js +++ b/src/pretix/static/fabric/fabric.min.js @@ -1,9 +1 @@ -var fabric=fabric||{version:"1.7.11"};"undefined"!=typeof exports&&(exports.fabric=fabric),"undefined"!=typeof document&&"undefined"!=typeof window?(fabric.document=document,fabric.window=window,window.fabric=fabric):(fabric.document=require("jsdom").jsdom(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E")),fabric.document.createWindow?fabric.window=fabric.document.createWindow():fabric.window=fabric.document.parentWindow),fabric.isTouchSupported="ontouchstart"in fabric.document.documentElement,fabric.isLikelyNode="undefined"!=typeof Buffer&&"undefined"==typeof window,fabric.SHARED_ATTRIBUTES=["display","transform","fill","fill-opacity","fill-rule","opacity","stroke","stroke-dasharray","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","id"],fabric.DPI=96,fabric.reNum="(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:e[-+]?\\d+)?)",fabric.fontPaths={},fabric.iMatrix=[1,0,0,1,0,0],fabric.charWidthsCache={},fabric.devicePixelRatio=fabric.window.devicePixelRatio||fabric.window.webkitDevicePixelRatio||fabric.window.mozDevicePixelRatio||1,function(){function t(t,e){if(this.__eventListeners[t]){var i=this.__eventListeners[t];e?i[i.indexOf(e)]=!1:fabric.util.array.fill(i,!1)}}function e(t,e){if(this.__eventListeners||(this.__eventListeners={}),1===arguments.length)for(var i in t)this.on(i,t[i]);else this.__eventListeners[t]||(this.__eventListeners[t]=[]),this.__eventListeners[t].push(e);return this}function i(e,i){if(this.__eventListeners){if(0===arguments.length)for(e in this.__eventListeners)t.call(this,e);else if(1===arguments.length&&"object"==typeof arguments[0])for(var r in e)t.call(this,r,e[r]);else t.call(this,e,i);return this}}function r(t,e){if(this.__eventListeners){var i=this.__eventListeners[t];if(i){for(var r=0,n=i.length;r-1},complexity:function(){return this.getObjects().reduce(function(t,e){return t+=e.complexity?e.complexity():0},0)}},fabric.CommonMethods={_setOptions:function(t){for(var e in t)this.set(e,t[e])},_initGradient:function(t,e){!t||!t.colorStops||t instanceof fabric.Gradient||this.set(e,new fabric.Gradient(t))},_initPattern:function(t,e,i){!t||!t.source||t instanceof fabric.Pattern?i&&i():this.set(e,new fabric.Pattern(t,i))},_initClipping:function(t){if(t.clipTo&&"string"==typeof t.clipTo){var e=fabric.util.getFunctionBody(t.clipTo);"undefined"!=typeof e&&(this.clipTo=new Function("ctx",e))}},_setObject:function(t){for(var e in t)this._set(e,t[e])},set:function(t,e){return"object"==typeof t?this._setObject(t):"function"==typeof e&&"clipTo"!==t?this._set(t,e(this.get(t))):this._set(t,e),this},_set:function(t,e){this[t]=e},toggle:function(t){var e=this.get(t);return"boolean"==typeof e&&this.set(t,!e),this},get:function(t){return this[t]}},function(t){var e=Math.sqrt,i=Math.atan2,r=Math.pow,n=Math.abs,s=Math.PI/180;fabric.util={removeFromArray:function(t,e){var i=t.indexOf(e);return i!==-1&&t.splice(i,1),t},getRandomInt:function(t,e){return Math.floor(Math.random()*(e-t+1))+t},degreesToRadians:function(t){return t*s},radiansToDegrees:function(t){return t/s},rotatePoint:function(t,e,i){t.subtractEquals(e);var r=fabric.util.rotateVector(t,i);return new fabric.Point(r.x,r.y).addEquals(e)},rotateVector:function(t,e){var i=Math.sin(e),r=Math.cos(e),n=t.x*r-t.y*i,s=t.x*i+t.y*r;return{x:n,y:s}},transformPoint:function(t,e,i){return i?new fabric.Point(e[0]*t.x+e[2]*t.y,e[1]*t.x+e[3]*t.y):new fabric.Point(e[0]*t.x+e[2]*t.y+e[4],e[1]*t.x+e[3]*t.y+e[5])},makeBoundingBoxFromPoints:function(t){var e=[t[0].x,t[1].x,t[2].x,t[3].x],i=fabric.util.array.min(e),r=fabric.util.array.max(e),n=Math.abs(i-r),s=[t[0].y,t[1].y,t[2].y,t[3].y],o=fabric.util.array.min(s),a=fabric.util.array.max(s),h=Math.abs(o-a);return{left:i,top:o,width:n,height:h}},invertTransform:function(t){var e=1/(t[0]*t[3]-t[1]*t[2]),i=[e*t[3],-e*t[1],-e*t[2],e*t[0]],r=fabric.util.transformPoint({x:t[4],y:t[5]},i,!0);return i[4]=-r.x,i[5]=-r.y,i},toFixed:function(t,e){return parseFloat(Number(t).toFixed(e))},parseUnit:function(t,e){var i=/\D{0,2}$/.exec(t),r=parseFloat(t);switch(e||(e=fabric.Text.DEFAULT_SVG_FONT_SIZE),i[0]){case"mm":return r*fabric.DPI/25.4;case"cm":return r*fabric.DPI/2.54;case"in":return r*fabric.DPI;case"pt":return r*fabric.DPI/72;case"pc":return r*fabric.DPI/72*12;case"em":return r*e;default:return r}},falseFunction:function(){return!1},getKlass:function(t,e){return t=fabric.util.string.camelize(t.charAt(0).toUpperCase()+t.slice(1)),fabric.util.resolveNamespace(e)[t]},resolveNamespace:function(e){if(!e)return fabric;var i,r=e.split("."),n=r.length,s=t||fabric.window;for(i=0;ir;)r+=a[d++%f],r>l&&(r=l),t[g?"lineTo":"moveTo"](r,0),g=!g;t.restore()},createCanvasElement:function(t){return t||(t=fabric.document.createElement("canvas")),t.getContext||"undefined"==typeof G_vmlCanvasManager||G_vmlCanvasManager.initElement(t),t},createImage:function(){return fabric.isLikelyNode?new(require("canvas").Image):fabric.document.createElement("img")},createAccessors:function(t){var e,i,r,n,s,o=t.prototype;for(e=o.stateProperties.length;e--;)i=o.stateProperties[e],r=i.charAt(0).toUpperCase()+i.slice(1),n="set"+r,s="get"+r,o[s]||(o[s]=function(t){return new Function('return this.get("'+t+'")')}(i)),o[n]||(o[n]=function(t){return new Function("value",'return this.set("'+t+'", value)')}(i))},clipContext:function(t,e){e.save(),e.beginPath(),t.clipTo(e),e.clip()},multiplyTransformMatrices:function(t,e,i){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],i?0:t[0]*e[4]+t[2]*e[5]+t[4],i?0:t[1]*e[4]+t[3]*e[5]+t[5]]},qrDecompose:function(t){var n=i(t[1],t[0]),o=r(t[0],2)+r(t[1],2),a=e(o),h=(t[0]*t[3]-t[2]*t[1])/a,c=i(t[0]*t[2]+t[1]*t[3],o);return{angle:n/s,scaleX:a,scaleY:h,skewX:c/s,skewY:0,translateX:t[4],translateY:t[5]}},customTransformMatrix:function(t,e,i){var r=[1,0,n(Math.tan(i*s)),1],o=[n(t),0,0,n(e)];return fabric.util.multiplyTransformMatrices(o,r,!0)},resetObjectTransform:function(t){t.scaleX=1,t.scaleY=1,t.skewX=0,t.skewY=0,t.flipX=!1,t.flipY=!1,t.setAngle(0)},getFunctionBody:function(t){return(String(t).match(/function[^{]*\{([\s\S]*)\}/)||{})[1]},isTransparent:function(t,e,i,r){r>0&&(e>r?e-=r:e=0,i>r?i-=r:i=0);var n,s,o=!0,a=t.getImageData(e,i,2*r||1,2*r||1),h=a.data.length;for(n=3;n0?P-=2*f:1===c&&P<0&&(P+=2*f);for(var I=Math.ceil(Math.abs(P/f*2)),E=[],L=P/I,F=8/3*Math.sin(L/4)*Math.sin(L/4)/Math.sin(L/2),B=A+L,R=0;R=n?s-n:2*Math.PI-(n-s)}function r(t,e,i,r,n,s,h,c){var l=a.call(arguments);if(o[l])return o[l];var u,f,d,g,p,v,b,m,y=Math.sqrt,_=Math.min,x=Math.max,C=Math.abs,S=[],w=[[],[]];f=6*t-12*i+6*n,u=-3*t+9*i-9*n+3*h,d=3*i-3*t;for(var O=0;O<2;++O)if(O>0&&(f=6*e-12*r+6*s,u=-3*e+9*r-9*s+3*c,d=3*r-3*e),C(u)<1e-12){if(C(f)<1e-12)continue;g=-d/f,0=e})}function i(t,e){return n(t,e,function(t,e){return t>>0;if(0===i)return-1;var r=0;if(arguments.length>0&&(r=Number(arguments[1]),r!==r?r=0:0!==r&&r!==Number.POSITIVE_INFINITY&&r!==Number.NEGATIVE_INFINITY&&(r=(r>0||-1)*Math.floor(Math.abs(r)))),r>=i)return-1;for(var n=r>=0?r:Math.max(i-Math.abs(r),0);n>>0;i>>0;r>>0;i>>0;i>>0;n>>0,r=0;if(arguments.length>1)e=arguments[1];else for(;;){if(r in this){e=this[r++];break}if(++r>=i)throw new TypeError}for(;r/g,">")}String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\xA0]+/,"").replace(/[\s\xA0]+$/,"")}),fabric.util.string={camelize:t,capitalize:e,escapeXml:i}}(),function(){var t=Array.prototype.slice,e=Function.prototype.apply,i=function(){};Function.prototype.bind||(Function.prototype.bind=function(r){var n,s=this,o=t.call(arguments,1);return n=o.length?function(){return e.call(s,this instanceof i?this:r,o.concat(t.call(arguments)))}:function(){return e.call(s,this instanceof i?this:r,arguments)},i.prototype=this.prototype,n.prototype=new i,n})}(),function(){function t(){}function e(t){for(var e=null,i=this;i.constructor.superclass;){var n=i.constructor.superclass.prototype[t];if(i[t]!==n){e=n;break}i=i.constructor.superclass.prototype}return e?arguments.length>1?e.apply(this,r.call(arguments,1)):e.call(this):console.log("tried to callSuper "+t+", method not found in prototype chain",this)}function i(){function i(){this.initialize.apply(this,arguments)}var s=null,a=r.call(arguments,0);"function"==typeof a[0]&&(s=a.shift()),i.superclass=s,i.subclasses=[],s&&(t.prototype=s.prototype,i.prototype=new t,s.subclasses.push(i));for(var h=0,c=a.length;h-1?t.prototype[r]=function(t){return function(){var r=this.constructor.superclass;this.constructor.superclass=i;var n=e[t].apply(this,arguments);if(this.constructor.superclass=r,"initialize"!==t)return n}}(r):t.prototype[r]=e[r],s&&(e.toString!==Object.prototype.toString&&(t.prototype.toString=e.toString),e.valueOf!==Object.prototype.valueOf&&(t.prototype.valueOf=e.valueOf))};fabric.util.createClass=i}(),function(){function t(t){var e,i,r=Array.prototype.slice.call(arguments,1),n=r.length;for(i=0;i-1?s(t,e.match(/opacity:\s*(\d?\.?\d*)/)[1]):t;for(var r in e)if("opacity"===r)s(t,e[r]);else{var n="float"===r||"cssFloat"===r?"undefined"==typeof i.styleFloat?"cssFloat":"styleFloat":r;i[n]=e[r]}return t}var e=fabric.document.createElement("div"),i="string"==typeof e.style.opacity,r="string"==typeof e.style.filter,n=/alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,s=function(t){return t};i?s=function(t,e){return t.style.opacity=e,t}:r&&(s=function(t,e){var i=t.style;return t.currentStyle&&!t.currentStyle.hasLayout&&(i.zoom=1),n.test(i.filter)?(e=e>=.9999?"":"alpha(opacity="+100*e+")",i.filter=i.filter.replace(n,e)):i.filter+=" alpha(opacity="+100*e+")",t}),fabric.util.setStyle=t}(),function(){function t(t){return"string"==typeof t?fabric.document.getElementById(t):t}function e(t,e){var i=fabric.document.createElement(t);for(var r in e)"class"===r?i.className=e[r]:"for"===r?i.htmlFor=e[r]:i.setAttribute(r,e[r]);return i}function i(t,e){t&&(" "+t.className+" ").indexOf(" "+e+" ")===-1&&(t.className+=(t.className?" ":"")+e)}function r(t,i,r){return"string"==typeof i&&(i=e(i,r)),t.parentNode&&t.parentNode.replaceChild(i,t),i.appendChild(t),i}function n(t){for(var e=0,i=0,r=fabric.document.documentElement,n=fabric.document.body||{scrollLeft:0,scrollTop:0};t&&(t.parentNode||t.host)&&(t=t.parentNode||t.host,t===fabric.document?(e=n.scrollLeft||r.scrollLeft||0,i=n.scrollTop||r.scrollTop||0):(e+=t.scrollLeft||0,i+=t.scrollTop||0),1!==t.nodeType||"fixed"!==fabric.util.getElementStyle(t,"position")););return{left:e,top:i}}function s(t){var e,i,r=t&&t.ownerDocument,s={left:0,top:0},o={left:0,top:0},a={borderLeftWidth:"left",borderTopWidth:"top",paddingLeft:"left",paddingTop:"top"};if(!r)return o;for(var h in a)o[a[h]]+=parseInt(c(t,h),10)||0;return e=r.documentElement,"undefined"!=typeof t.getBoundingClientRect&&(s=t.getBoundingClientRect()),i=n(t),{left:s.left+i.left-(e.clientLeft||0)+o.left,top:s.top+i.top-(e.clientTop||0)+o.top}}var o,a=Array.prototype.slice,h=function(t){return a.call(t,0)};try{o=h(fabric.document.childNodes)instanceof Array}catch(t){}o||(h=function(t){for(var e=new Array(t.length),i=t.length;i--;)e[i]=t[i];return e});var c;c=fabric.document.defaultView&&fabric.document.defaultView.getComputedStyle?function(t,e){var i=fabric.document.defaultView.getComputedStyle(t,null);return i?i[e]:void 0}:function(t,e){var i=t.style[e];return!i&&t.currentStyle&&(i=t.currentStyle[e]),i},function(){function t(t){return"undefined"!=typeof t.onselectstart&&(t.onselectstart=fabric.util.falseFunction),r?t.style[r]="none":"string"==typeof t.unselectable&&(t.unselectable="on"),t}function e(t){return"undefined"!=typeof t.onselectstart&&(t.onselectstart=null),r?t.style[r]="":"string"==typeof t.unselectable&&(t.unselectable=""),t}var i=fabric.document.documentElement.style,r="userSelect"in i?"userSelect":"MozUserSelect"in i?"MozUserSelect":"WebkitUserSelect"in i?"WebkitUserSelect":"KhtmlUserSelect"in i?"KhtmlUserSelect":"";fabric.util.makeElementUnselectable=t,fabric.util.makeElementSelectable=e}(),function(){function t(t,e){var i=fabric.document.getElementsByTagName("head")[0],r=fabric.document.createElement("script"),n=!0;r.onload=r.onreadystatechange=function(t){if(n){if("string"==typeof this.readyState&&"loaded"!==this.readyState&&"complete"!==this.readyState)return;n=!1,e(t||fabric.window.event),r=r.onload=r.onreadystatechange=null}},r.src=t,i.appendChild(r)}fabric.util.getScript=t}(),fabric.util.getById=t,fabric.util.toArray=h,fabric.util.makeElement=e,fabric.util.addClass=i,fabric.util.wrapElement=r,fabric.util.getScrollLeftTop=n,fabric.util.getElementOffset=s,fabric.util.getElementStyle=c}(),function(){function t(t,e){return t+(/\?/.test(t)?"&":"?")+e}function e(){}function i(i,n){n||(n={});var s=n.method?n.method.toUpperCase():"GET",o=n.onComplete||function(){},a=r(),h=n.body||n.parameters;return a.onreadystatechange=function(){4===a.readyState&&(o(a),a.onreadystatechange=e)},"GET"===s&&(h=null,"string"==typeof n.parameters&&(i=t(i,n.parameters))),a.open(s,i,!0),"POST"!==s&&"PUT"!==s||a.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),a.send(h),a}var r=function(){for(var t=[function(){return new ActiveXObject("Microsoft.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP.3.0")},function(){return new XMLHttpRequest}],e=t.length;e--;)try{var i=t[e]();if(i)return t[e]}catch(t){}}();fabric.util.request=i}(),fabric.log=function(){},fabric.warn=function(){},"undefined"!=typeof console&&["log","warn"].forEach(function(t){"undefined"!=typeof console[t]&&"function"==typeof console[t].apply&&(fabric[t]=function(){return console[t].apply(console,arguments)})}),function(){function t(t){e(function(i){t||(t={});var r,n=i||+new Date,s=t.duration||500,o=n+s,a=t.onChange||function(){},h=t.abort||function(){return!1},c=t.easing||function(t,e,i,r){return-i*Math.cos(t/r*(Math.PI/2))+i+e},l="startValue"in t?t.startValue:0,u="endValue"in t?t.endValue:100,f=t.byValue||u-l;t.onStart&&t.onStart(),function i(u){r=u||+new Date;var d=r>o?s:r-n;return h()?void(t.onComplete&&t.onComplete()):(a(c(d,l,f,s)),r>o?void(t.onComplete&&t.onComplete()):void e(i))}(n)})}function e(){return i.apply(fabric.window,arguments)}var i=fabric.window.requestAnimationFrame||fabric.window.webkitRequestAnimationFrame||fabric.window.mozRequestAnimationFrame||fabric.window.oRequestAnimationFrame||fabric.window.msRequestAnimationFrame||function(t){fabric.window.setTimeout(t,1e3/60)};fabric.util.animate=t,fabric.util.requestAnimFrame=e}(),function(){function t(t,e,i){var r="rgba("+parseInt(t[0]+i*(e[0]-t[0]),10)+","+parseInt(t[1]+i*(e[1]-t[1]),10)+","+parseInt(t[2]+i*(e[2]-t[2]),10);return r+=","+(t&&e?parseFloat(t[3]+i*(e[3]-t[3])):1),r+=")"}function e(e,i,r,n){var s=new fabric.Color(e).getSource(),o=new fabric.Color(i).getSource();n=n||{},fabric.util.animate(fabric.util.object.extend(n,{duration:r||500,startValue:s,endValue:o,byValue:o,easing:function(e,i,r,s){var o=n.colorEasing?n.colorEasing(e,s):1-Math.cos(e/s*(Math.PI/2));return t(i,r,o)}}))}fabric.util.animateColor=e}(),function(){function t(t,e,i,r){return ta?a:o),1===o&&1===a&&0===h&&0===c&&0===f&&0===d)return _;if((f||d)&&(x=" translate("+y(f)+" "+y(d)+") "),r=x+" matrix("+o+" 0 0 "+a+" "+h*o+" "+c*a+") ", -"svg"===t.nodeName){for(n=t.ownerDocument.createElement("g");t.firstChild;)n.appendChild(t.firstChild);t.appendChild(n)}else n=t,r=n.getAttribute("transform")+r;return n.setAttribute("transform",r),_}function g(t,e){for(;t&&(t=t.parentNode);)if(t.nodeName&&e.test(t.nodeName.replace("svg:",""))&&!t.getAttribute("instantiated_by_use"))return!0;return!1}var p=t.fabric||(t.fabric={}),v=p.util.object.extend,b=p.util.object.clone,m=p.util.toFixed,y=p.util.parseUnit,_=p.util.multiplyTransformMatrices,x=/^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/i,C=/^(symbol|image|marker|pattern|view|svg)$/i,S=/^(?:pattern|defs|symbol|metadata|clipPath|mask)$/i,w=/^(symbol|g|a|svg)$/i,O={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","stroke-dasharray":"strokeDashArray","stroke-linecap":"strokeLineCap","stroke-linejoin":"strokeLineJoin","stroke-miterlimit":"strokeMiterLimit","stroke-opacity":"strokeOpacity","stroke-width":"strokeWidth","text-decoration":"textDecoration","text-anchor":"originX",opacity:"opacity"},T={stroke:"strokeOpacity",fill:"fillOpacity"};p.cssRules={},p.gradientDefs={},p.parseTransformAttribute=function(){function t(t,e){var i=Math.cos(e[0]),r=Math.sin(e[0]),n=0,s=0;3===e.length&&(n=e[1],s=e[2]),t[0]=i,t[1]=r,t[2]=-r,t[3]=i,t[4]=n-(i*n-r*s),t[5]=s-(r*n+i*s)}function e(t,e){var i=e[0],r=2===e.length?e[1]:e[0];t[0]=i,t[3]=r}function i(t,e,i){t[i]=Math.tan(p.util.degreesToRadians(e[0]))}function r(t,e){t[4]=e[0],2===e.length&&(t[5]=e[1])}var n=[1,0,0,1,0,0],s=p.reNum,o="(?:\\s+,?\\s*|,\\s*)",a="(?:(skewX)\\s*\\(\\s*("+s+")\\s*\\))",h="(?:(skewY)\\s*\\(\\s*("+s+")\\s*\\))",c="(?:(rotate)\\s*\\(\\s*("+s+")(?:"+o+"("+s+")"+o+"("+s+"))?\\s*\\))",l="(?:(scale)\\s*\\(\\s*("+s+")(?:"+o+"("+s+"))?\\s*\\))",u="(?:(translate)\\s*\\(\\s*("+s+")(?:"+o+"("+s+"))?\\s*\\))",f="(?:(matrix)\\s*\\(\\s*("+s+")"+o+"("+s+")"+o+"("+s+")"+o+"("+s+")"+o+"("+s+")"+o+"("+s+")\\s*\\))",d="(?:"+f+"|"+u+"|"+l+"|"+c+"|"+a+"|"+h+")",g="(?:"+d+"(?:"+o+"*"+d+")*)",v="^\\s*(?:"+g+"?)\\s*$",b=new RegExp(v),m=new RegExp(d,"g");return function(s){var o=n.concat(),a=[];if(!s||s&&!b.test(s))return o;s.replace(m,function(s){var h=new RegExp(d).exec(s).filter(function(t){return!!t}),c=h[1],l=h.slice(2).map(parseFloat);switch(c){case"translate":r(o,l);break;case"rotate":l[0]=p.util.degreesToRadians(l[0]),t(o,l);break;case"scale":e(o,l);break;case"skewX":i(o,l,2);break;case"skewY":i(o,l,1);break;case"matrix":o=l}a.push(o.concat()),o=n.concat()});for(var h=a[0];a.length>1;)a.shift(),h=p.util.multiplyTransformMatrices(h,a[0]);return h}}();var j=new RegExp("^\\s*("+p.reNum+"+)\\s*,?\\s*("+p.reNum+"+)\\s*,?\\s*("+p.reNum+"+)\\s*,?\\s*("+p.reNum+"+)\\s*$");p.parseSVGDocument=function(t,e,i,r){if(t){f(t);var n=p.Object.__uid++,s=d(t),o=p.util.toArray(t.getElementsByTagName("*"));if(s.crossOrigin=r&&r.crossOrigin,s.svgUid=n,0===o.length&&p.isLikelyNode){o=t.selectNodes('//*[name(.)!="svg"]');for(var a=[],h=0,c=o.length;h/i,""))),n&&n.documentElement||e&&e(null),p.parseSVGDocument(n.documentElement,function(t,i){e&&e(t,i)},i,r)}t=t.replace(/^\n\s*/,"").trim(),new p.util.request(t,{method:"get",onComplete:n})},loadSVGFromString:function(t,e,i,r){t=t.trim();var n;if("undefined"!=typeof DOMParser){var s=new DOMParser;s&&s.parseFromString&&(n=s.parseFromString(t,"text/xml"))}else p.window.ActiveXObject&&(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t.replace(//i,"")));p.parseSVGDocument(n.documentElement,function(t,i){e(t,i)},i,r)}})}("undefined"!=typeof exports?exports:this),fabric.ElementsParser=function(t,e,i,r,n){this.elements=t,this.callback=e,this.options=i,this.reviver=r,this.svgUid=i&&i.svgUid||0,this.parsingOptions=n},fabric.ElementsParser.prototype.parse=function(){this.instances=new Array(this.elements.length),this.numElements=this.elements.length,this.createObjects()},fabric.ElementsParser.prototype.createObjects=function(){for(var t=0,e=this.elements.length;tt.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,i){return"undefined"==typeof i&&(i=.5),i=Math.max(Math.min(1,i),0),new e(this.x+(t.x-this.x)*i,this.y+(t.y-this.y)*i)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new e(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new e(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new e(this.x,this.y)}}))}("undefined"!=typeof exports?exports:this),function(t){"use strict";function e(t){this.status=t,this.points=[]}var i=t.fabric||(t.fabric={});return i.Intersection?void i.warn("fabric.Intersection is already defined"):(i.Intersection=e,i.Intersection.prototype={constructor:e,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},i.Intersection.intersectLineLine=function(t,r,n,s){var o,a=(s.x-n.x)*(t.y-n.y)-(s.y-n.y)*(t.x-n.x),h=(r.x-t.x)*(t.y-n.y)-(r.y-t.y)*(t.x-n.x),c=(s.y-n.y)*(r.x-t.x)-(s.x-n.x)*(r.y-t.y);if(0!==c){var l=a/c,u=h/c;0<=l&&l<=1&&0<=u&&u<=1?(o=new e("Intersection"),o.appendPoint(new i.Point(t.x+l*(r.x-t.x),t.y+l*(r.y-t.y)))):o=new e}else o=new e(0===a||0===h?"Coincident":"Parallel");return o},i.Intersection.intersectLinePolygon=function(t,i,r){for(var n,s,o,a=new e,h=r.length,c=0;c0&&(a.status="Intersection"),a},i.Intersection.intersectPolygonPolygon=function(t,i){for(var r=new e,n=t.length,s=0;s0&&(r.status="Intersection"),r},void(i.Intersection.intersectPolygonRectangle=function(t,r,n){var s=r.min(n),o=r.max(n),a=new i.Point(o.x,s.y),h=new i.Point(s.x,o.y),c=e.intersectLinePolygon(s,a,t),l=e.intersectLinePolygon(a,o,t),u=e.intersectLinePolygon(o,h,t),f=e.intersectLinePolygon(h,s,t),d=new e;return d.appendPoints(c.points),d.appendPoints(l.points),d.appendPoints(u.points),d.appendPoints(f.points),d.points.length>0&&(d.status="Intersection"),d}))}("undefined"!=typeof exports?exports:this),function(t){"use strict";function e(t){t?this._tryParsingColor(t):this.setSource([0,0,0,1])}function i(t,e,i){return i<0&&(i+=1),i>1&&(i-=1),i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}var r=t.fabric||(t.fabric={});return r.Color?void r.warn("fabric.Color is already defined."):(r.Color=e,r.Color.prototype={_tryParsingColor:function(t){var i;t in e.colorNameMap&&(t=e.colorNameMap[t]),"transparent"===t&&(i=[255,255,255,0]),i||(i=e.sourceFromHex(t)),i||(i=e.sourceFromRgb(t)),i||(i=e.sourceFromHsl(t)),i||(i=[0,0,0,1]),i&&this.setSource(i)},_rgbToHsl:function(t,e,i){t/=255,e/=255,i/=255;var n,s,o,a=r.util.array.max([t,e,i]),h=r.util.array.min([t,e,i]);if(o=(a+h)/2,a===h)n=s=0;else{var c=a-h;switch(s=o>.5?c/(2-a-h):c/(a+h),a){case t:n=(e-i)/c+(e1?1:s,n){var o=n.split(/\s*;\s*/);""===o[o.length-1]&&o.pop();for(var a=o.length;a--;){var h=o[a].split(/\s*:\s*/),c=h[0].trim(),l=h[1].trim();"stop-color"===c?e=l:"stop-opacity"===c&&(r=l)}}return e||(e=t.getAttribute("stop-color")||"rgb(0,0,0)"),r||(r=t.getAttribute("stop-opacity")),e=new fabric.Color(e),i=e.getAlpha(),r=isNaN(parseFloat(r))?1:parseFloat(r),r*=i,{offset:s,color:e.toRgb(),opacity:r}}function e(t){return{x1:t.getAttribute("x1")||0,y1:t.getAttribute("y1")||0,x2:t.getAttribute("x2")||"100%",y2:t.getAttribute("y2")||0}}function i(t){return{x1:t.getAttribute("fx")||t.getAttribute("cx")||"50%",y1:t.getAttribute("fy")||t.getAttribute("cy")||"50%",r1:0,x2:t.getAttribute("cx")||"50%",y2:t.getAttribute("cy")||"50%",r2:t.getAttribute("r")||"50%"}}function r(t,e,i){var r,n=0,s=1,o="";for(var a in e)"Infinity"===e[a]?e[a]=1:"-Infinity"===e[a]&&(e[a]=0),r=parseFloat(e[a],10),s="string"==typeof e[a]&&/^\d+%$/.test(e[a])?.01:1,"x1"===a||"x2"===a||"r2"===a?(s*="objectBoundingBox"===i?t.width:1,n="objectBoundingBox"===i?t.left||0:0):"y1"!==a&&"y2"!==a||(s*="objectBoundingBox"===i?t.height:1,n="objectBoundingBox"===i?t.top||0:0),e[a]=r*s+n;if("ellipse"===t.type&&null!==e.r2&&"objectBoundingBox"===i&&t.rx!==t.ry){var h=t.ry/t.rx;o=" scale(1, "+h+")",e.y1&&(e.y1/=h),e.y2&&(e.y2/=h)}return o}var n=fabric.util.object.clone;fabric.Gradient=fabric.util.createClass({offsetX:0,offsetY:0,initialize:function(t){t||(t={});var e={};this.id=fabric.Object.__uid++,this.type=t.type||"linear",e={x1:t.coords.x1||0,y1:t.coords.y1||0,x2:t.coords.x2||0,y2:t.coords.y2||0},"radial"===this.type&&(e.r1=t.coords.r1||0,e.r2=t.coords.r2||0),this.coords=e,this.colorStops=t.colorStops.slice(),t.gradientTransform&&(this.gradientTransform=t.gradientTransform),this.offsetX=t.offsetX||this.offsetX,this.offsetY=t.offsetY||this.offsetY},addColorStop:function(t){for(var e in t){var i=new fabric.Color(t[e]);this.colorStops.push({offset:parseFloat(e),color:i.toRgb(),opacity:i.getAlpha()})}return this},toObject:function(t){var e={type:this.type,coords:this.coords,colorStops:this.colorStops,offsetX:this.offsetX,offsetY:this.offsetY,gradientTransform:this.gradientTransform?this.gradientTransform.concat():this.gradientTransform};return fabric.util.populateWithProperties(this,e,t),e},toSVG:function(t){var e,i,r=n(this.coords,!0),s=n(this.colorStops,!0),o=r.r1>r.r2;if(s.sort(function(t,e){return t.offset-e.offset}),!t.group||"path-group"!==t.group.type)for(var a in r)"x1"===a||"x2"===a?r[a]+=this.offsetX-t.width/2:"y1"!==a&&"y2"!==a||(r[a]+=this.offsetY-t.height/2);if(i='id="SVGID_'+this.id+'" gradientUnits="userSpaceOnUse"',this.gradientTransform&&(i+=' gradientTransform="matrix('+this.gradientTransform.join(" ")+')" '),"linear"===this.type?e=["\n']:"radial"===this.type&&(e=["\n']),"radial"===this.type){if(o){s=s.concat(),s.reverse();for(var h=0;h0)for(var l=Math.max(r.r1,r.r2),u=c/l,h=0;h\n')}return e.push("linear"===this.type?"\n":"\n"),e.join("")},toLive:function(t,e){var i,r,n=fabric.util.object.clone(this.coords);if(this.type){if(e.group&&"path-group"===e.group.type)for(r in n)"x1"===r||"x2"===r?n[r]+=-this.offsetX+e.width/2:"y1"!==r&&"y2"!==r||(n[r]+=-this.offsetY+e.height/2);"linear"===this.type?i=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(i=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2));for(var s=0,o=this.colorStops.length;s\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e="function"==typeof this.source?this.source():this.source;if(!e)return"";if("undefined"!=typeof e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}(),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.toFixed;return e.Shadow?void e.warn("fabric.Shadow is already defined."):(e.Shadow=e.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,initialize:function(t){"string"==typeof t&&(t=this._parseShadow(t));for(var i in t)this[i]=t[i];this.id=e.Object.__uid++},_parseShadow:function(t){var i=t.trim(),r=e.Shadow.reOffsetsAndBlur.exec(i)||[],n=i.replace(e.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)";return{color:n.trim(),offsetX:parseInt(r[1],10)||0,offsetY:parseInt(r[2],10)||0,blur:parseInt(r[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var r=40,n=40,s=e.Object.NUM_FRACTION_DIGITS,o=e.util.rotateVector({x:this.offsetX,y:this.offsetY},e.util.degreesToRadians(-t.angle)),a=20;return t.width&&t.height&&(r=100*i((Math.abs(o.x)+this.blur)/t.width,s)+a,n=100*i((Math.abs(o.y)+this.blur)/t.height,s)+a),t.flipX&&(o.x*=-1),t.flipY&&(o.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke};var t={},i=e.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke"].forEach(function(e){this[e]!==i[e]&&(t[e]=this[e])},this),t}}),void(e.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/))}("undefined"!=typeof exports?exports:this),function(){"use strict";if(fabric.StaticCanvas)return void fabric.warn("fabric.StaticCanvas is already defined.");var t=fabric.util.object.extend,e=fabric.util.getElementOffset,i=fabric.util.removeFromArray,r=fabric.util.toFixed,n=fabric.util.transformPoint,s=fabric.util.invertTransform,o=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e||(e={}),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,clipTo:null,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,onBeforeScaleRotate:function(){},enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!1,_initStatic:function(t,e){var i=fabric.StaticCanvas.prototype.renderAll.bind(this);this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this._setImageSmoothing(),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1!==fabric.devicePixelRatio&&this.enableRetinaScaling},getRetinaScaling:function(){return this._isRetinaScaling()?fabric.devicePixelRatio:1},_initRetinaScaling:function(){this._isRetinaScaling()&&(this.lowerCanvasEl.setAttribute("width",this.width*fabric.devicePixelRatio),this.lowerCanvasEl.setAttribute("height",this.height*fabric.devicePixelRatio),this.contextContainer.scale(fabric.devicePixelRatio,fabric.devicePixelRatio))},calcOffset:function(){return this._offset=e(this.lowerCanvasEl),this},setOverlayImage:function(t,e,i){return this.__setBgOverlayImage("overlayImage",t,e,i)},setBackgroundImage:function(t,e,i){return this.__setBgOverlayImage("backgroundImage",t,e,i)},setOverlayColor:function(t,e){return this.__setBgOverlayColor("overlayColor",t,e)},setBackgroundColor:function(t,e){return this.__setBgOverlayColor("backgroundColor",t,e)},_setImageSmoothing:function(){var t=this.getContext();t.imageSmoothingEnabled=t.imageSmoothingEnabled||t.webkitImageSmoothingEnabled||t.mozImageSmoothingEnabled||t.msImageSmoothingEnabled||t.oImageSmoothingEnabled,t.imageSmoothingEnabled=this.imageSmoothingEnabled},__setBgOverlayImage:function(t,e,i,r){return"string"==typeof e?fabric.util.loadImage(e,function(e){e&&(this[t]=new fabric.Image(e,r)),i&&i(e)},this,r&&r.crossOrigin):(r&&e.setOptions(r),this[t]=e,i&&i(e)),this},__setBgOverlayColor:function(t,e,i){return this[t]=e,this._initGradient(e,t),this._initPattern(e,t,i),this},_createCanvasElement:function(t){var e=fabric.util.createCanvasElement(t);if(e.style||(e.style={}),!e)throw o;if("undefined"==typeof e.getContext)throw o;return e},_initOptions:function(t){this._setOptions(t),this.width=this.width||parseInt(this.lowerCanvasEl.width,10)||0,this.height=this.height||parseInt(this.lowerCanvasEl.height,10)||0,this.lowerCanvasEl.style&&(this.lowerCanvasEl.width=this.width,this.lowerCanvasEl.height=this.height,this.lowerCanvasEl.style.width=this.width+"px",this.lowerCanvasEl.style.height=this.height+"px",this.viewportTransform=this.viewportTransform.slice())},_createLowerCanvas:function(t){this.lowerCanvasEl=fabric.util.getById(t)||this._createCanvasElement(t),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(t,e){return this.setDimensions({width:t},e)},setHeight:function(t,e){return this.setDimensions({height:t},e)},setDimensions:function(t,e){var i;e=e||{};for(var r in t)i=t[r],e.cssOnly||(this._setBackstoreDimension(r,t[r]),i+="px"),e.backstoreOnly||this._setCssDimension(r,i);return this._initRetinaScaling(),this._setImageSmoothing(),this.calcOffset(),e.cssOnly||this.renderAll(),this},_setBackstoreDimension:function(t,e){return this.lowerCanvasEl[t]=e,this.upperCanvasEl&&(this.upperCanvasEl[t]=e),this.cacheCanvasEl&&(this.cacheCanvasEl[t]=e),this[t]=e,this},_setCssDimension:function(t,e){return this.lowerCanvasEl.style[t]=e,this.upperCanvasEl&&(this.upperCanvasEl.style[t]=e),this.wrapperEl&&(this.wrapperEl.style[t]=e),this},getZoom:function(){return this.viewportTransform[0]},setViewportTransform:function(t){var e,i=this._activeGroup,r=!1,n=!0;this.viewportTransform=t;for(var s=0,o=this._objects.length;s"),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,n=e.width||this.width,s=e.height||this.height,o='viewBox="0 0 '+this.width+" "+this.height+'" ',a=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?o='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,o='viewBox="'+r(-i[4]/i[0],a)+" "+r(-i[5]/i[3],a)+" "+r(this.width/i[0],a)+" "+r(this.height/i[3],a)+'" '),t.push("\n',"Created with Fabric.js ",fabric.version,"\n","\n",this.createSVGFontFacesMarkup(),this.createSVGRefElementsMarkup(),"\n")},createSVGRefElementsMarkup:function(){var t=this,e=["backgroundColor","overlayColor"].map(function(e){var i=t[e];if(i&&i.toLive)return i.toSVG(t,!1)});return e.join("")},createSVGFontFacesMarkup:function(){for(var t,e,i,r,n,s,o,a="",h={},c=fabric.fontPaths,l=this.getObjects(),u=0,f=l.length;u',"\n",a,"","\n"].join("")),a},_setSVGObjects:function(t,e){for(var i,r=0,n=this.getObjects(),s=n.length;r\n")}else t.push('\n")},sendToBack:function(t){if(!t)return this;var e,r,n,s=this._activeGroup;if(t===s)for(n=s._objects,e=n.length;e--;)r=n[e],i(this._objects,r),this._objects.unshift(r);else i(this._objects,t),this._objects.unshift(t);return this.renderAll&&this.renderAll()},bringToFront:function(t){if(!t)return this;var e,r,n,s=this._activeGroup;if(t===s)for(n=s._objects,e=0;e=0;--n){var s=t.intersectsWithObject(this._objects[n])||t.isContainedWithinObject(this._objects[n])||this._objects[n].isContainedWithinObject(t);if(s){r=n;break}}}else r=e-1;return r},bringForward:function(t,e){if(!t)return this;var r,n,s,o,a,h=this._activeGroup;if(t===h)for(a=h._objects,r=a.length;r--;)n=a[r],s=this._objects.indexOf(n),s!==this._objects.length-1&&(o=s+1,i(this._objects,n),this._objects.splice(o,0,n));else s=this._objects.indexOf(t),s!==this._objects.length-1&&(o=this._findNewUpperIndex(t,s,e),i(this._objects,t),this._objects.splice(o,0,t));return this.renderAll&&this.renderAll(),this},_findNewUpperIndex:function(t,e,i){var r;if(i){r=e;for(var n=e+1;n"}}),t(fabric.StaticCanvas.prototype,fabric.Observable),t(fabric.StaticCanvas.prototype,fabric.Collection),t(fabric.StaticCanvas.prototype,fabric.DataURLExporter),t(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(t){var e=fabric.util.createCanvasElement();if(!e||!e.getContext)return null;var i=e.getContext("2d");if(!i)return null;switch(t){case"getImageData":return"undefined"!=typeof i.getImageData;case"setLineDash":return"undefined"!=typeof i.setLineDash;case"toDataURL":return"undefined"!=typeof e.toDataURL;case"toDataURLWithQuality":try{return e.toDataURL("image/jpeg",0),!0}catch(t){}return!1;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",strokeDashArray:null,setShadow:function(t){return this.shadow=new fabric.Shadow(t),this},_setBrushStyles:function(){var t=this.canvas.contextTop;t.strokeStyle=this.color,t.lineWidth=this.width,t.lineCap=this.strokeLineCap,t.lineJoin=this.strokeLineJoin,this.strokeDashArray&&fabric.StaticCanvas.supports("setLineDash")&&t.setLineDash(this.strokeDashArray)},_setShadow:function(){if(this.shadow){var t=this.canvas.contextTop,e=this.canvas.getZoom();t.shadowColor=this.shadow.color,t.shadowBlur=this.shadow.blur*e,t.shadowOffsetX=this.shadow.offsetX*e,t.shadowOffsetY=this.shadow.offsetY*e}},_resetShadow:function(){var t=this.canvas.contextTop;t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0}}),function(){fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{initialize:function(t){this.canvas=t,this._points=[]},onMouseDown:function(t){this._prepareForDrawing(t),this._captureDrawingPath(t),this._render()},onMouseMove:function(t){this._captureDrawingPath(t),this.canvas.clearContext(this.canvas.contextTop),this._render()},onMouseUp:function(){this._finalizeAndAddPath()},_prepareForDrawing:function(t){var e=new fabric.Point(t.x,t.y);this._reset(),this._addPoint(e),this.canvas.contextTop.moveTo(e.x,e.y)},_addPoint:function(t){this._points.push(t)},_reset:function(){this._points.length=0,this._setBrushStyles(),this._setShadow()},_captureDrawingPath:function(t){var e=new fabric.Point(t.x,t.y);this._addPoint(e)},_render:function(){var t=this.canvas.contextTop,e=this.canvas.viewportTransform,i=this._points[0],r=this._points[1];t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5]),t.beginPath(),2===this._points.length&&i.x===r.x&&i.y===r.y&&(i.x-=.5,r.x+=.5),t.moveTo(i.x,i.y);for(var n=1,s=this._points.length;n0?1:-1,"y"===i&&(s=e.target.skewY,o="top",a="bottom",r="originY"),n[-1]=o,n[1]=a,e.target.flipX&&(c*=-1),e.target.flipY&&(c*=-1),0===s?(e.skewSign=-h*t*c,e[r]=n[-t]):(s=s>0?1:-1,e.skewSign=s,e[r]=n[s*h*c])},_skewObject:function(t,e,i){var r=this._currentTransform,n=r.target,s=!1,o=n.get("lockSkewingX"),a=n.get("lockSkewingY");if(o&&"x"===i||a&&"y"===i)return!1;var h,c,l=n.getCenterPoint(),u=n.toLocalPoint(new fabric.Point(t,e),"center","center")[i],f=n.toLocalPoint(new fabric.Point(r.lastX,r.lastY),"center","center")[i],d=n._getTransformedDimensions();return this._changeSkewTransformOrigin(u-f,r,i),h=n.toLocalPoint(new fabric.Point(t,e),r.originX,r.originY)[i],c=n.translateToOriginPoint(l,r.originX,r.originY),s=this._setObjectSkew(h,r,i,d),r.lastX=t,r.lastY=e,n.setPositionByOrigin(c,r.originX,r.originY),s},_setObjectSkew:function(t,e,i,r){var n,s,o,a,h,c,l,u,f,d=e.target,g=!1,p=e.skewSign;return"x"===i?(a="y",h="Y",c="X",u=0,f=d.skewY):(a="x",h="X",c="Y",u=d.skewX,f=0),o=d._getTransformedDimensions(u,f),l=2*Math.abs(t)-o[i],l<=2?n=0:(n=p*Math.atan(l/d["scale"+c]/(o[a]/d["scale"+h])),n=fabric.util.radiansToDegrees(n)),g=d["skew"+c]!==n,d.set("skew"+c,n),0!==d["skew"+h]&&(s=d._getTransformedDimensions(),n=r[a]/s[a]*d["scale"+h],d.set("scale"+h,n)),g},_scaleObject:function(t,e,i){var r=this._currentTransform,n=r.target,s=n.get("lockScalingX"),o=n.get("lockScalingY"),a=n.get("lockScalingFlip");if(s&&o)return!1;var h=n.translateToOriginPoint(n.getCenterPoint(),r.originX,r.originY),c=n.toLocalPoint(new fabric.Point(t,e),r.originX,r.originY),l=n._getTransformedDimensions(),u=!1;return this._setLocalMouse(c,r),u=this._setObjectScale(c,r,s,o,i,a,l),n.setPositionByOrigin(h,r.originX,r.originY),u},_setObjectScale:function(t,e,i,r,n,s,o){var a,h,c,l,u=e.target,f=!1,d=!1,g=!1;return c=t.x*u.scaleX/o.x,l=t.y*u.scaleY/o.y,a=u.scaleX!==c,h=u.scaleY!==l,s&&c<=0&&cs?t.x<0?t.x+=s:t.x-=s:t.x=0,n(t.y)>s?t.y<0?t.y+=s:t.y-=s:t.y=0},_rotateObject:function(t,e){var n=this._currentTransform;if(n.target.get("lockRotation"))return!1;var s=r(n.ey-n.top,n.ex-n.left),o=r(e-n.top,t-n.left),a=i(o-s+n.theta),h=!0;if(n.target.snapAngle>0){var c=n.target.snapAngle,l=n.target.snapThreshold||c,u=Math.ceil(a/c)*c,f=Math.floor(a/c)*c;Math.abs(a-f)0?0:-i),e.ey-(r>0?0:-r),a,h)),this.selectionLineWidth&&this.selectionBorderColor)if(t.lineWidth=this.selectionLineWidth,t.strokeStyle=this.selectionBorderColor,this.selectionDashArray.length>1&&!s){var c=e.ex+o-(i>0?0:a),l=e.ey+o-(r>0?0:h);t.beginPath(),fabric.util.drawDashedLine(t,c,l,c+a,l,this.selectionDashArray),fabric.util.drawDashedLine(t,c,l+h-1,c+a,l+h-1,this.selectionDashArray),fabric.util.drawDashedLine(t,c,l,c,l+h,this.selectionDashArray),fabric.util.drawDashedLine(t,c+a-1,l,c+a-1,l+h,this.selectionDashArray),t.closePath(),t.stroke()}else fabric.Object.prototype._setLineDash.call(this,t,this.selectionDashArray),t.strokeRect(e.ex+o-(i>0?0:a),e.ey+o-(r>0?0:h),a,h)},findTarget:function(t,e){if(!this.skipTargetFind){var i,r=!0,n=this.getPointer(t,r),s=this.getActiveGroup(),o=this.getActiveObject();if(s&&!e&&s===this._searchPossibleTargets([s],n))return this._fireOverOutEvents(s,t),s;if(o&&o._findTargetCorner(n))return this._fireOverOutEvents(o,t),o;if(o&&o===this._searchPossibleTargets([o],n)){if(!this.preserveObjectStacking)return this._fireOverOutEvents(o,t),o;i=o}this.targets=[];var a=this._searchPossibleTargets(this._objects,n);return t[this.altSelectionKey]&&a&&i&&a!==i&&(a=i),this._fireOverOutEvents(a,t),a}},_fireOverOutEvents:function(t,e){t?this._hoveredTarget!==t&&(this._hoveredTarget&&(this.fire("mouse:out",{target:this._hoveredTarget,e:e}),this._hoveredTarget.fire("mouseout",{e:e})),this.fire("mouse:over",{target:t,e:e}),t.fire("mouseover",{e:e}),this._hoveredTarget=t):this._hoveredTarget&&(this.fire("mouse:out",{target:this._hoveredTarget,e:e}),this._hoveredTarget.fire("mouseout",{e:e}),this._hoveredTarget=null)},_checkTarget:function(t,e){if(e&&e.visible&&e.evented&&this.containsPoint(null,e,t)){if(!this.perPixelTargetFind&&!e.perPixelTargetFind||e.isEditing)return!0;var i=this.isTargetTransparent(e,t.x,t.y);if(!i)return!0}},_searchPossibleTargets:function(t,e){for(var i,r,n,s=t.length;s--;)if(this._checkTarget(e,t[s])){i=t[s],"group"===i.type&&i.subTargetCheck&&(r=this._normalizePointer(i,e),n=this._searchPossibleTargets(i._objects,r),n&&this.targets.push(n));break}return i},restorePointerVpt:function(t){return fabric.util.transformPoint(t,fabric.util.invertTransform(this.viewportTransform))},getPointer:function(e,i,r){r||(r=this.upperCanvasEl);var n,s=t(e),o=r.getBoundingClientRect(),a=o.width||0,h=o.height||0;return a&&h||("top"in o&&"bottom"in o&&(h=Math.abs(o.top-o.bottom)),"right"in o&&"left"in o&&(a=Math.abs(o.right-o.left))),this.calcOffset(),s.x=s.x-this._offset.left,s.y=s.y-this._offset.top,i||(s=this.restorePointerVpt(s)),n=0===a||0===h?{width:1,height:1}:{width:r.width/a,height:r.height/h},{x:s.x*n.width,y:s.y*n.height}},_createUpperCanvas:function(){var t=this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/,"");this.upperCanvasEl=this._createCanvasElement(),fabric.util.addClass(this.upperCanvasEl,"upper-canvas "+t),this.wrapperEl.appendChild(this.upperCanvasEl),this._copyCanvasStyle(this.lowerCanvasEl,this.upperCanvasEl),this._applyCanvasStyle(this.upperCanvasEl),this.contextTop=this.upperCanvasEl.getContext("2d")},_createCacheCanvas:function(){this.cacheCanvasEl=this._createCanvasElement(),this.cacheCanvasEl.setAttribute("width",this.width),this.cacheCanvasEl.setAttribute("height",this.height),this.contextCache=this.cacheCanvasEl.getContext("2d")},_initWrapperElement:function(){this.wrapperEl=fabric.util.wrapElement(this.lowerCanvasEl,"div",{class:this.containerClass}),fabric.util.setStyle(this.wrapperEl,{width:this.getWidth()+"px",height:this.getHeight()+"px",position:"relative"}),fabric.util.makeElementUnselectable(this.wrapperEl)},_applyCanvasStyle:function(t){var e=this.getWidth()||t.width,i=this.getHeight()||t.height;fabric.util.setStyle(t,{position:"absolute",width:e+"px",height:i+"px",left:0,top:0,"touch-action":"none"}),t.width=e,t.height=i,fabric.util.makeElementUnselectable(t)},_copyCanvasStyle:function(t,e){e.style.cssText=t.style.cssText},getSelectionContext:function(){return this.contextTop},getSelectionElement:function(){return this.upperCanvasEl},_setActiveObject:function(t){var e=this._activeObject;e&&(e.set("active",!1),t!==e&&e.onDeselect&&"function"==typeof e.onDeselect&&e.onDeselect()),this._activeObject=t,t.set("active",!0)},setActiveObject:function(t,e){var i=this.getActiveObject();return i&&i!==t&&i.fire("deselected",{e:e}),this._setActiveObject(t),this.renderAll(),this.fire("object:selected",{target:t,e:e}),t.fire("selected",{e:e}),this},getActiveObject:function(){return this._activeObject},_onObjectRemoved:function(t){this.getActiveObject()===t&&(this.fire("before:selection:cleared",{target:t}),this._discardActiveObject(),this.fire("selection:cleared",{target:t}),t.fire("deselected")),this._hoveredTarget===t&&(this._hoveredTarget=null),this.callSuper("_onObjectRemoved",t)},_discardActiveObject:function(){var t=this._activeObject;t&&(t.set("active",!1),t.onDeselect&&"function"==typeof t.onDeselect&&t.onDeselect()),this._activeObject=null},discardActiveObject:function(t){var e=this._activeObject;return e&&(this.fire("before:selection:cleared",{target:e,e:t}),this._discardActiveObject(),this.fire("selection:cleared",{e:t}),e.fire("deselected",{e:t})),this},_setActiveGroup:function(t){this._activeGroup=t,t&&t.set("active",!0)},setActiveGroup:function(t,e){return this._setActiveGroup(t),t&&(this.fire("object:selected",{target:t,e:e}),t.fire("selected",{e:e})),this},getActiveGroup:function(){return this._activeGroup},_discardActiveGroup:function(){var t=this.getActiveGroup();t&&t.destroy(),this.setActiveGroup(null)},discardActiveGroup:function(t){var e=this.getActiveGroup();return e&&(this.fire("before:selection:cleared",{e:t,target:e}),this._discardActiveGroup(),this.fire("selection:cleared",{e:t})),this},deactivateAll:function(){for(var t,e=this.getObjects(),i=0,r=e.length;i1)){var r=this._groupSelector;r?(i=this.getPointer(t,!0),r.left=i.x-r.ex,r.top=i.y-r.ey,this.renderTop()):this._currentTransform?this._transformObject(t):(e=this.findTarget(t),this._setCursorFromEvent(t,e)),this._handleEvent(t,"move",e?e:null)}},__onMouseWheel:function(t){this._handleEvent(t,"wheel")},_transformObject:function(t){var e=this.getPointer(t),i=this._currentTransform;i.reset=!1,i.target.isMoving=!0,i.shiftKey=t.shiftKey,i.altKey=t[this.centeredKey],this._beforeScaleTransform(t,i),this._performTransformAction(t,i,e),i.actionPerformed&&this.renderAll()},_performTransformAction:function(t,e,i){var r=i.x,n=i.y,s=e.target,o=e.action,a=!1;"rotate"===o?(a=this._rotateObject(r,n))&&this._fire("rotating",s,t):"scale"===o?(a=this._onScale(t,e,r,n))&&this._fire("scaling",s,t):"scaleX"===o?(a=this._scaleObject(r,n,"x"))&&this._fire("scaling",s,t):"scaleY"===o?(a=this._scaleObject(r,n,"y"))&&this._fire("scaling",s,t):"skewX"===o?(a=this._skewObject(r,n,"x"))&&this._fire("skewing",s,t):"skewY"===o?(a=this._skewObject(r,n,"y"))&&this._fire("skewing",s,t):(a=this._translateObject(r,n),a&&(this._fire("moving",s,t),this.setCursor(s.moveCursor||this.moveCursor))),e.actionPerformed=e.actionPerformed||a},_fire:function(t,e,i){this.fire("object:"+t,{target:e,e:i}),e.fire(t,{e:i})},_beforeScaleTransform:function(t,e){if("scale"===e.action||"scaleX"===e.action||"scaleY"===e.action){var i=this._shouldCenterTransform(e.target);(i&&("center"!==e.originX||"center"!==e.originY)||!i&&"center"===e.originX&&"center"===e.originY)&&(this._resetCurrentTransform(),e.reset=!0)}},_onScale:function(t,e,i,r){return!t[this.uniScaleKey]&&!this.uniScaleTransform||e.target.get("lockUniScaling")?(e.reset||"scale"!==e.currentAction||this._resetCurrentTransform(),e.currentAction="scaleEqually",this._scaleObject(i,r,"equally")):(e.currentAction="scale",this._scaleObject(i,r))},_setCursorFromEvent:function(t,e){if(!e||!e.selectable)return this.setCursor(this.defaultCursor),!1;var i=e.hoverCursor||this.hoverCursor,r=this.getActiveGroup(),n=e._findTargetCorner&&(!r||!r.contains(e))&&e._findTargetCorner(this.getPointer(t,!0));return n?this._setCornerCursor(n,e,t):this.setCursor(i),!0},_setCornerCursor:function(e,i,r){if(e in t)this.setCursor(this._getRotatedCornerCursor(e,i,r));else{if("mtr"!==e||!i.hasRotatingPoint)return this.setCursor(this.defaultCursor),!1;this.setCursor(this.rotationCursor)}},_getRotatedCornerCursor:function(e,i,r){var n=Math.round(i.getAngle()%360/45);return n<0&&(n+=8),n+=t[e],r[this.altActionKey]&&t[e]%2===0&&(n+=2),n%=8,this.cursorMap[n]}})}(),function(){var t=Math.min,e=Math.max;fabric.util.object.extend(fabric.Canvas.prototype,{_shouldGroup:function(t,e){var i=this.getActiveObject();return t[this.selectionKey]&&e&&e.selectable&&(this.getActiveGroup()||i&&i!==e)&&this.selection},_handleGrouping:function(t,e){var i=this.getActiveGroup();(e!==i||(e=this.findTarget(t,!0)))&&(i?this._updateActiveGroup(e,t):this._createActiveGroup(e,t),this._activeGroup&&this._activeGroup.saveCoords())},_updateActiveGroup:function(t,e){var i=this.getActiveGroup();if(i.contains(t)){if(i.removeWithUpdate(t),t.set("active",!1),1===i.size())return this.discardActiveGroup(e),void this.setActiveObject(i.item(0),e)}else i.addWithUpdate(t);this.fire("selection:created",{target:i,e:e}),i.set("active",!0)},_createActiveGroup:function(t,e){if(this._activeObject&&t!==this._activeObject){var i=this._createGroup(t);i.addWithUpdate(),this.setActiveGroup(i,e),this._activeObject=null,this.fire("selection:created",{target:i,e:e})}t.set("active",!0)},_createGroup:function(t){var e=this.getObjects(),i=e.indexOf(this._activeObject)1&&(e=new fabric.Group(e.reverse(),{canvas:this}),e.addWithUpdate(),this.setActiveGroup(e,t),e.saveCoords(),this.fire("selection:created",{target:e,e:t}),this.renderAll())},_collectObjects:function(){for(var i,r=[],n=this._groupSelector.ex,s=this._groupSelector.ey,o=n+this._groupSelector.left,a=s+this._groupSelector.top,h=new fabric.Point(t(n,o),t(s,a)),c=new fabric.Point(e(n,o),e(s,a)),l=n===o&&s===a,u=this._objects.length;u--&&(i=this._objects[u],!(i&&i.selectable&&i.visible&&(i.intersectsWithRect(h,c)||i.isContainedWithinRect(h,c)||i.containsPoint(h)||i.containsPoint(c))&&(i.set("active",!0),r.push(i),l))););return r},_maybeGroupObjects:function(t){this.selection&&this._groupSelector&&this._groupSelectedObjects(t);var e=this.getActiveGroup();e&&(e.setObjectsCoords().setCoords(),e.isMoving=!1,this.setCursor(this.defaultCursor)),this._groupSelector=null,this._currentTransform=null}})}(),function(){var t=fabric.StaticCanvas.supports("toDataURLWithQuality");fabric.util.object.extend(fabric.StaticCanvas.prototype,{toDataURL:function(t){t||(t={});var e=t.format||"png",i=t.quality||1,r=t.multiplier||1,n={left:t.left||0,top:t.top||0,width:t.width||0,height:t.height||0};return this.__toDataURLWithMultiplier(e,i,n,r)},__toDataURLWithMultiplier:function(t,e,i,r){var n=this.getWidth(),s=this.getHeight(),o=(i.width||this.getWidth())*r,a=(i.height||this.getHeight())*r,h=this.getZoom(),c=h*r,l=this.viewportTransform,u=(l[4]-i.left)*r,f=(l[5]-i.top)*r,d=[c,0,0,c,u,f],g=this.interactive;this.viewportTransform=d,this.interactive&&(this.interactive=!1),n!==o||s!==a?this.setDimensions({width:o,height:a}):this.renderAll();var p=this.__toDataURL(t,e,i);return g&&(this.interactive=g),this.viewportTransform=l,this.setDimensions({width:n,height:s}),p},__toDataURL:function(e,i){var r=this.contextContainer.canvas;"jpg"===e&&(e="jpeg");var n=t?r.toDataURL("image/"+e,i):r.toDataURL("image/"+e);return n},toDataURLWithMultiplier:function(t,e,i){return this.toDataURL({format:t,multiplier:e,quality:i})}})}(),fabric.util.object.extend(fabric.StaticCanvas.prototype,{loadFromDatalessJSON:function(t,e,i){return this.loadFromJSON(t,e,i)},loadFromJSON:function(t,e,i){if(t){var r="string"==typeof t?JSON.parse(t):fabric.util.object.clone(t);this.clear();var n=this;return this._enlivenObjects(r.objects,function(){n._setBgOverlay(r,function(){delete r.objects,delete r.backgroundImage,delete r.overlayImage,delete r.background,delete r.overlay,n._setOptions(r),e&&e()})},i),this}},_setBgOverlay:function(t,e){var i=this,r={backgroundColor:!1,overlayColor:!1,backgroundImage:!1,overlayImage:!1};if(!(t.backgroundImage||t.overlayImage||t.background||t.overlay))return void(e&&e());var n=function(){r.backgroundImage&&r.overlayImage&&r.backgroundColor&&r.overlayColor&&(i.renderAll(),e&&e())};this.__setBgOverlay("backgroundImage",t.backgroundImage,r,n),this.__setBgOverlay("overlayImage",t.overlayImage,r,n),this.__setBgOverlay("backgroundColor",t.background,r,n),this.__setBgOverlay("overlayColor",t.overlay,r,n)},__setBgOverlay:function(t,e,i,r){var n=this;return e?void("backgroundImage"===t||"overlayImage"===t?fabric.util.enlivenObjects([e],function(e){n[t]=e[0],i[t]=!0,r&&r()}):this["set"+fabric.util.string.capitalize(t,!0)](e,function(){i[t]=!0,r&&r()})):(i[t]=!0,void(r&&r()))},_enlivenObjects:function(t,e,i){var r=this;if(!t||0===t.length)return void(e&&e());var n=this.renderOnAddRemove;this.renderOnAddRemove=!1,fabric.util.enlivenObjects(t,function(t){t.forEach(function(t,e){r.insertAt(t,e)}),r.renderOnAddRemove=n,e&&e()},null,i)},_toDataURL:function(t,e){this.clone(function(i){e(i.toDataURL(t))})},_toDataURLWithMultiplier:function(t,e,i){this.clone(function(r){i(r.toDataURLWithMultiplier(t,e))})},clone:function(t,e){var i=JSON.stringify(this.toJSON(e));this.cloneWithoutData(function(e){e.loadFromJSON(i,function(){t&&t(e)})})},cloneWithoutData:function(t){var e=fabric.document.createElement("canvas");e.width=this.getWidth(),e.height=this.getHeight();var i=new fabric.Canvas(e);i.clipTo=this.clipTo,this.backgroundImage?(i.setBackgroundImage(this.backgroundImage.src,function(){i.renderAll(),t&&t(i)}),i.backgroundImageOpacity=this.backgroundImageOpacity,i.backgroundImageStretch=this.backgroundImageStretch):t&&t(i)}}),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.object.clone,n=e.util.toFixed,s=e.util.string.capitalize,o=e.util.degreesToRadians,a=e.StaticCanvas.supports("setLineDash"),h=!e.isLikelyNode;e.Object||(e.Object=e.util.createClass(e.CommonMethods,{type:"object",originX:"left",originY:"top",top:0,left:0,width:0,height:0,scaleX:1,scaleY:1,flipX:!1,flipY:!1,opacity:1,angle:0,skewX:0,skewY:0,cornerSize:13,transparentCorners:!0,hoverCursor:null,moveCursor:null,padding:0,borderColor:"rgba(102,153,255,0.75)",borderDashArray:null,cornerColor:"rgba(102,153,255,0.5)",cornerStrokeColor:null,cornerStyle:"rect",cornerDashArray:null,centeredScaling:!1,centeredRotation:!0,fill:"rgb(0,0,0)",fillRule:"nonzero",globalCompositeOperation:"source-over",backgroundColor:"",selectionBackgroundColor:"",stroke:null,strokeWidth:1,strokeDashArray:null,strokeLineCap:"butt",strokeLineJoin:"miter",strokeMiterLimit:10,shadow:null,borderOpacityWhenMoving:.4,borderScaleFactor:1,transformMatrix:null,minScaleLimit:.01,selectable:!0,evented:!0,visible:!0,hasControls:!0,hasBorders:!0,hasRotatingPoint:!0,rotatingPointOffset:40,perPixelTargetFind:!1,includeDefaultValues:!0,clipTo:null,lockMovementX:!1,lockMovementY:!1,lockRotation:!1,lockScalingX:!1,lockScalingY:!1,lockUniScaling:!1,lockSkewingX:!1,lockSkewingY:!1,lockScalingFlip:!1,excludeFromExport:!1,objectCaching:h,statefullCache:!1,noScaleCache:!0,dirty:!0,needsItsOwnCache:!1,stateProperties:"top left width height scaleX scaleY flipX flipY originX originY transformMatrix stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor skewX skewY".split(" "),cacheProperties:"fill stroke strokeWidth strokeDashArray width height stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit fillRule backgroundColor".split(" "),initialize:function(t){t=t||{},t&&this.setOptions(t),this.objectCaching&&this._createCacheCanvas()},_createCacheCanvas:function(){this._cacheProperties={},this._cacheCanvas=e.document.createElement("canvas"),this._cacheContext=this._cacheCanvas.getContext("2d"),this._updateCacheCanvas()},_getCacheCanvasDimensions:function(){var t=this.canvas&&this.canvas.getZoom()||1,i=this.getObjectScaling(),r=this._getNonTransformedDimensions(),n=this.canvas&&this.canvas._isRetinaScaling()?e.devicePixelRatio:1,s=i.scaleX*t*n,o=i.scaleY*t*n,a=r.x*s,h=r.y*o;return{width:a+2,height:h+2,zoomX:s,zoomY:o}},_updateCacheCanvas:function(){if(this.noScaleCache&&this.canvas&&this.canvas._currentTransform){var t=this.canvas._currentTransform.action;if("scale"===t.slice(0,5))return!1}var e=this._getCacheCanvasDimensions(),i=e.width,r=e.height,n=e.zoomX,s=e.zoomY;return(i!==this.cacheWidth||r!==this.cacheHeight)&&(this._cacheCanvas.width=Math.ceil(i),this._cacheCanvas.height=Math.ceil(r),this._cacheContext.translate(i/2,r/2),this._cacheContext.scale(n,s),this.cacheWidth=i,this.cacheHeight=r,this.zoomX=n,this.zoomY=s,!0)},setOptions:function(t){this._setOptions(t),this._initGradient(t.fill,"fill"),this._initGradient(t.stroke,"stroke"),this._initClipping(t),this._initPattern(t.fill,"fill"),this._initPattern(t.stroke,"stroke")},transform:function(t,e){this.group&&!this.group._transformDone&&this.group===this.canvas._activeGroup&&this.group.transform(t);var i=e?this._getLeftTopCoords():this.getCenterPoint();t.translate(i.x,i.y),this.angle&&t.rotate(o(this.angle)),t.scale(this.scaleX*(this.flipX?-1:1),this.scaleY*(this.flipY?-1:1)),this.skewX&&t.transform(1,0,Math.tan(o(this.skewX)),1,0,0),this.skewY&&t.transform(1,Math.tan(o(this.skewY)),0,1,0,0)},toObject:function(t){var i=e.Object.NUM_FRACTION_DIGITS,r={type:this.type,originX:this.originX,originY:this.originY,left:n(this.left,i),top:n(this.top,i),width:n(this.width,i),height:n(this.height,i),fill:this.fill&&this.fill.toObject?this.fill.toObject():this.fill,stroke:this.stroke&&this.stroke.toObject?this.stroke.toObject():this.stroke,strokeWidth:n(this.strokeWidth,i),strokeDashArray:this.strokeDashArray?this.strokeDashArray.concat():this.strokeDashArray,strokeLineCap:this.strokeLineCap,strokeLineJoin:this.strokeLineJoin,strokeMiterLimit:n(this.strokeMiterLimit,i),scaleX:n(this.scaleX,i),scaleY:n(this.scaleY,i),angle:n(this.getAngle(),i),flipX:this.flipX,flipY:this.flipY,opacity:n(this.opacity,i),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,globalCompositeOperation:this.globalCompositeOperation,transformMatrix:this.transformMatrix?this.transformMatrix.concat():null,skewX:n(this.skewX,i),skewY:n(this.skewY,i)};return e.util.populateWithProperties(this,r,t),this.includeDefaultValues||(r=this._removeDefaultValues(r)),r},toDatalessObject:function(t){return this.toObject(t)},_removeDefaultValues:function(t){var i=e.util.getKlass(t.type).prototype,r=i.stateProperties;return r.forEach(function(e){t[e]===i[e]&&delete t[e];var r="[object Array]"===Object.prototype.toString.call(t[e])&&"[object Array]"===Object.prototype.toString.call(i[e]);r&&0===t[e].length&&0===i[e].length&&delete t[e]}),t},toString:function(){return"#"},getObjectScaling:function(){var t=this.scaleX,e=this.scaleY;if(this.group){var i=this.group.getObjectScaling();t*=i.scaleX,e*=i.scaleY}return{scaleX:t,scaleY:e}},_set:function(t,i){var r="scaleX"===t||"scaleY"===t;return r&&(i=this._constrainScale(i)),"scaleX"===t&&i<0?(this.flipX=!this.flipX,i*=-1):"scaleY"===t&&i<0?(this.flipY=!this.flipY,i*=-1):"shadow"!==t||!i||i instanceof e.Shadow?"dirty"===t&&this.group&&this.group.set("dirty",i):i=new e.Shadow(i),this[t]=i,this.cacheProperties.indexOf(t)>-1&&(this.group&&this.group.set("dirty",!0),this.dirty=!0),this.group&&this.stateProperties.indexOf(t)>-1&&this.group.set("dirty",!0),"width"!==t&&"height"!==t||(this.minScaleLimit=Math.min(.1,1/Math.max(this.width,this.height))),this},setOnGroup:function(){},setSourcePath:function(t){return this.sourcePath=t,this},getViewportTransform:function(){return this.canvas&&this.canvas.viewportTransform?this.canvas.viewportTransform:e.iMatrix.concat()},render:function(t,i){0===this.width&&0===this.height||!this.visible||this.canvas&&this.canvas.skipOffscreen&&!this.group&&!this.isOnScreen()||(t.save(),this._setupCompositeOperation(t),this.drawSelectionBackground(t),i||this.transform(t),this._setOpacity(t),this._setShadow(t),this.transformMatrix&&t.transform.apply(t,this.transformMatrix),this.clipTo&&e.util.clipContext(this,t),this.shouldCache()?(this._cacheCanvas||this._createCacheCanvas(),this.isCacheDirty(i)&&(this.statefullCache&&this.saveState({propertySet:"cacheProperties"}),this.drawObject(this._cacheContext,i),this.dirty=!1),this.drawCacheOnCanvas(t)):(this.drawObject(t,i),i&&this.objectCaching&&this.statefullCache&&this.saveState({propertySet:"cacheProperties"})),this.clipTo&&t.restore(),t.restore())},shouldCache:function(){return this.objectCaching&&(!this.group||this.needsItsOwnCache||!this.group.isCaching())},willDrawShadow:function(){return!!this.shadow},drawObject:function(t,e){this._renderBackground(t),this._setStrokeStyles(t),this._setFillStyles(t),this._render(t,e)},drawCacheOnCanvas:function(t){t.scale(1/this.zoomX,1/this.zoomY),t.drawImage(this._cacheCanvas,-this.cacheWidth/2,-this.cacheHeight/2)},isCacheDirty:function(t){if(!t&&this._updateCacheCanvas())return!0;if(this.dirty||this.statefullCache&&this.hasStateChanged("cacheProperties")){if(!t){var e=this.cacheWidth/this.zoomX,i=this.cacheHeight/this.zoomY;this._cacheContext.clearRect(-e/2,-i/2,e,i)}return!0}return!1},_renderBackground:function(t){if(this.backgroundColor){var e=this._getNonTransformedDimensions();t.fillStyle=this.backgroundColor,t.fillRect(-e.x/2,-e.y/2,e.x,e.y),this._removeShadow(t)}},_setOpacity:function(t){t.globalAlpha*=this.opacity},_setStrokeStyles:function(t){this.stroke&&(t.lineWidth=this.strokeWidth,t.lineCap=this.strokeLineCap,t.lineJoin=this.strokeLineJoin,t.miterLimit=this.strokeMiterLimit,t.strokeStyle=this.stroke.toLive?this.stroke.toLive(t,this):this.stroke)},_setFillStyles:function(t){this.fill&&(t.fillStyle=this.fill.toLive?this.fill.toLive(t,this):this.fill)},_setLineDash:function(t,e,i){e&&(1&e.length&&e.push.apply(e,e),a?t.setLineDash(e):i&&i(t))},_renderControls:function(t){if(this.active&&(!this.group||this.group===this.canvas.getActiveGroup())){var i,r=this.getViewportTransform(),n=this.calcTransformMatrix();n=e.util.multiplyTransformMatrices(r,n),i=e.util.qrDecompose(n),t.save(),t.translate(i.translateX,i.translateY),t.lineWidth=1*this.borderScaleFactor,this.group||(t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1),this.group&&this.group===this.canvas.getActiveGroup()?(t.rotate(o(i.angle)),this.drawBordersInGroup(t,i)):(t.rotate(o(this.angle)),this.drawBorders(t)),this.drawControls(t),t.restore()}},_setShadow:function(t){if(this.shadow){var i=this.canvas&&this.canvas.viewportTransform[0]||1,r=this.canvas&&this.canvas.viewportTransform[3]||1,n=this.getObjectScaling();this.canvas&&this.canvas._isRetinaScaling()&&(i*=e.devicePixelRatio,r*=e.devicePixelRatio),t.shadowColor=this.shadow.color,t.shadowBlur=this.shadow.blur*(i+r)*(n.scaleX+n.scaleY)/4,t.shadowOffsetX=this.shadow.offsetX*i*n.scaleX,t.shadowOffsetY=this.shadow.offsetY*r*n.scaleY}},_removeShadow:function(t){this.shadow&&(t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0)},_applyPatternGradientTransform:function(t,e){if(e.toLive){var i=e.gradientTransform||e.patternTransform;i&&t.transform.apply(t,i);var r=-this.width/2+e.offsetX||0,n=-this.height/2+e.offsetY||0;t.translate(r,n)}},_renderFill:function(t){this.fill&&(t.save(),this._applyPatternGradientTransform(t,this.fill),"evenodd"===this.fillRule?t.fill("evenodd"):t.fill(),t.restore())},_renderStroke:function(t){this.stroke&&0!==this.strokeWidth&&(this.shadow&&!this.shadow.affectStroke&&this._removeShadow(t),t.save(),this._setLineDash(t,this.strokeDashArray,this._renderDashedStroke),this._applyPatternGradientTransform(t,this.stroke),t.stroke(),t.restore())},clone:function(t,i){return this.constructor.fromObject?this.constructor.fromObject(this.toObject(i),t):new e.Object(this.toObject(i))},cloneAsImage:function(t,i){var r=this.toDataURL(i);return e.util.loadImage(r,function(i){t&&t(new e.Image(i))}),this},toDataURL:function(t){t||(t={});var i=e.util.createCanvasElement(),r=this.getBoundingRect();i.width=r.width,i.height=r.height,e.util.wrapElement(i,"div");var n=new e.StaticCanvas(i,{enableRetinaScaling:t.enableRetinaScaling});"jpg"===t.format&&(t.format="jpeg"),"jpeg"===t.format&&(n.backgroundColor="#fff");var s={active:this.get("active"),left:this.getLeft(),top:this.getTop()};this.set("active",!1),this.setPositionByOrigin(new e.Point(n.getWidth()/2,n.getHeight()/2),"center","center");var o=this.canvas;n.add(this);var a=n.toDataURL(t);return this.set(s).setCoords(),this.canvas=o,n.dispose(),n=null,a},isType:function(t){return this.type===t},complexity:function(){return 1},toJSON:function(t){return this.toObject(t)},setGradient:function(t,i){i||(i={});var r={colorStops:[]};return r.type=i.type||(i.r1||i.r2?"radial":"linear"),r.coords={x1:i.x1,y1:i.y1,x2:i.x2,y2:i.y2},(i.r1||i.r2)&&(r.coords.r1=i.r1,r.coords.r2=i.r2),r.gradientTransform=i.gradientTransform,e.Gradient.prototype.addColorStop.call(r,i.colorStops),this.set(t,e.Gradient.forObject(this,r))},setPatternFill:function(t){return this.set("fill",new e.Pattern(t))},setShadow:function(t){return this.set("shadow",t?new e.Shadow(t):null)},setColor:function(t){return this.set("fill",t),this},setAngle:function(t){var e=("center"!==this.originX||"center"!==this.originY)&&this.centeredRotation;return e&&this._setOriginToCenter(),this.set("angle",t),e&&this._resetOrigin(),this},centerH:function(){return this.canvas&&this.canvas.centerObjectH(this),this},viewportCenterH:function(){return this.canvas&&this.canvas.viewportCenterObjectH(this),this},centerV:function(){return this.canvas&&this.canvas.centerObjectV(this),this},viewportCenterV:function(){return this.canvas&&this.canvas.viewportCenterObjectV(this),this},center:function(){return this.canvas&&this.canvas.centerObject(this),this},viewportCenter:function(){return this.canvas&&this.canvas.viewportCenterObject(this),this},remove:function(){return this.canvas&&this.canvas.remove(this),this},getLocalPointer:function(t,i){i=i||this.canvas.getPointer(t);var r=new e.Point(i.x,i.y),n=this._getLeftTopCoords();return this.angle&&(r=e.util.rotatePoint(r,n,o(-this.angle))),{x:r.x-n.x,y:r.y-n.y}},_setupCompositeOperation:function(t){this.globalCompositeOperation&&(t.globalCompositeOperation=this.globalCompositeOperation)}}),e.util.createAccessors(e.Object),e.Object.prototype.rotate=e.Object.prototype.setAngle,i(e.Object.prototype,e.Observable),e.Object.NUM_FRACTION_DIGITS=2,e.Object._fromObject=function(t,i,n,s,o){var a=e[t];if(i=r(i,!0),!s){var h=o?new a(i[o],i):new a(i);return n&&n(h),h}e.util.enlivenPatterns([i.fill,i.stroke],function(t){"undefined"!=typeof t[0]&&(i.fill=t[0]),"undefined"!=typeof t[1]&&(i.stroke=t[1]);var e=o?new a(i[o],i):new a(i);n&&n(e)})},e.Object.__uid=0)}("undefined"!=typeof exports?exports:this),function(){var t=fabric.util.degreesToRadians,e={left:-.5,center:0,right:.5},i={top:-.5,center:0,bottom:.5};fabric.util.object.extend(fabric.Object.prototype,{translateToGivenOrigin:function(t,r,n,s,o){var a,h,c,l=t.x,u=t.y;return"string"==typeof r?r=e[r]:r-=.5,"string"==typeof s?s=e[s]:s-=.5,a=s-r,"string"==typeof n?n=i[n]:n-=.5,"string"==typeof o?o=i[o]:o-=.5,h=o-n,(a||h)&&(c=this._getTransformedDimensions(),l=t.x+a*c.x,u=t.y+h*c.y),new fabric.Point(l,u)},translateToCenterPoint:function(e,i,r){var n=this.translateToGivenOrigin(e,i,r,"center","center");return this.angle?fabric.util.rotatePoint(n,e,t(this.angle)):n},translateToOriginPoint:function(e,i,r){var n=this.translateToGivenOrigin(e,"center","center",i,r);return this.angle?fabric.util.rotatePoint(n,e,t(this.angle)):n},getCenterPoint:function(){var t=new fabric.Point(this.left,this.top); -return this.translateToCenterPoint(t,this.originX,this.originY)},getPointByOrigin:function(t,e){var i=this.getCenterPoint();return this.translateToOriginPoint(i,t,e)},toLocalPoint:function(e,i,r){var n,s,o=this.getCenterPoint();return n="undefined"!=typeof i&&"undefined"!=typeof r?this.translateToGivenOrigin(o,"center","center",i,r):new fabric.Point(this.left,this.top),s=new fabric.Point(e.x,e.y),this.angle&&(s=fabric.util.rotatePoint(s,o,-t(this.angle))),s.subtractEquals(n)},setPositionByOrigin:function(t,e,i){var r=this.translateToCenterPoint(t,e,i),n=this.translateToOriginPoint(r,this.originX,this.originY);this.set("left",n.x),this.set("top",n.y)},adjustPosition:function(i){var r,n,s=t(this.angle),o=this.getWidth(),a=Math.cos(s)*o,h=Math.sin(s)*o;r="string"==typeof this.originX?e[this.originX]:this.originX-.5,n="string"==typeof i?e[i]:i-.5,this.left+=a*(n-r),this.top+=h*(n-r),this.setCoords(),this.originX=i},_setOriginToCenter:function(){this._originalOriginX=this.originX,this._originalOriginY=this.originY;var t=this.getCenterPoint();this.originX="center",this.originY="center",this.left=t.x,this.top=t.y},_resetOrigin:function(){var t=this.translateToOriginPoint(this.getCenterPoint(),this._originalOriginX,this._originalOriginY);this.originX=this._originalOriginX,this.originY=this._originalOriginY,this.left=t.x,this.top=t.y,this._originalOriginX=null,this._originalOriginY=null},_getLeftTopCoords:function(){return this.translateToOriginPoint(this.getCenterPoint(),"left","top")},onDeselect:function(){}})}(),function(){function t(t){return[new fabric.Point(t.tl.x,t.tl.y),new fabric.Point(t.tr.x,t.tr.y),new fabric.Point(t.br.x,t.br.y),new fabric.Point(t.bl.x,t.bl.y)]}var e=fabric.util.degreesToRadians,i=fabric.util.multiplyTransformMatrices;fabric.util.object.extend(fabric.Object.prototype,{oCoords:null,aCoords:null,getCoords:function(e,i){this.oCoords||this.setCoords();var r=e?this.aCoords:this.oCoords;return t(i?this.calcCoords(e):r)},intersectsWithRect:function(t,e,i,r){var n=this.getCoords(i,r),s=fabric.Intersection.intersectPolygonRectangle(n,t,e);return"Intersection"===s.status},intersectsWithObject:function(t,e,i){var r=fabric.Intersection.intersectPolygonPolygon(this.getCoords(e,i),t.getCoords(e,i));return"Intersection"===r.status||t.isContainedWithinObject(this,e,i)||this.isContainedWithinObject(t,e,i)},isContainedWithinObject:function(t,e,i){for(var r=this.getCoords(e,i),n=0,s=t._getImageLines(i?t.calcCoords(e):e?t.aCoords:t.oCoords);n<4;n++)if(!t.containsPoint(r[n],s))return!1;return!0},isContainedWithinRect:function(t,e,i,r){var n=this.getBoundingRect(i,r);return n.left>=t.x&&n.left+n.width<=e.x&&n.top>=t.y&&n.top+n.height<=e.y},containsPoint:function(t,e,i,r){var e=e||this._getImageLines(r?this.calcCoords(i):i?this.aCoords:this.oCoords),n=this._findCrossPoints(t,e);return 0!==n&&n%2===1},isOnScreen:function(t){if(!this.canvas)return!1;for(var e,i=this.canvas.vptCoords.tl,r=this.canvas.vptCoords.br,n=this.getCoords(!0,t),s=0;s<4;s++)if(e=n[s],e.x<=r.x&&e.x>=i.x&&e.y<=r.y&&e.y>=i.y)return!0;if(this.intersectsWithRect(i,r,!0))return!0;var o={x:(i.x+r.x)/2,y:(i.y+r.y)/2};return!!this.containsPoint(o,null,!0)},_getImageLines:function(t){return{topline:{o:t.tl,d:t.tr},rightline:{o:t.tr,d:t.br},bottomline:{o:t.br,d:t.bl},leftline:{o:t.bl,d:t.tl}}},_findCrossPoints:function(t,e){var i,r,n,s,o,a,h=0;for(var c in e)if(a=e[c],!(a.o.y=t.y&&a.d.y>=t.y||(a.o.x===a.d.x&&a.o.x>=t.x?o=a.o.x:(i=0,r=(a.d.y-a.o.y)/(a.d.x-a.o.x),n=t.y-i*t.x,s=a.o.y-r*a.o.x,o=-(n-s)/(i-r)),o>=t.x&&(h+=1),2!==h)))break;return h},getBoundingRectWidth:function(){return this.getBoundingRect().width},getBoundingRectHeight:function(){return this.getBoundingRect().height},getBoundingRect:function(t,e){var i=this.getCoords(t,e);return fabric.util.makeBoundingBoxFromPoints(i)},getWidth:function(){return this._getTransformedDimensions().x},getHeight:function(){return this._getTransformedDimensions().y},_constrainScale:function(t){return Math.abs(t)0?Math.atan(o/s):0,l=s/Math.cos(c)/2,u=Math.cos(c+i)*l,f=Math.sin(c+i)*l,d=this.getCenterPoint(),g=t?d:fabric.util.transformPoint(d,r),p=new fabric.Point(g.x-u,g.y-f),v=new fabric.Point(p.x+s*h,p.y+s*a),b=new fabric.Point(p.x-o*a,p.y+o*h),m=new fabric.Point(g.x+u,g.y+f);if(!t)var y=new fabric.Point((p.x+b.x)/2,(p.y+b.y)/2),_=new fabric.Point((v.x+p.x)/2,(v.y+p.y)/2),x=new fabric.Point((m.x+v.x)/2,(m.y+v.y)/2),C=new fabric.Point((m.x+b.x)/2,(m.y+b.y)/2),S=new fabric.Point(_.x+a*this.rotatingPointOffset,_.y-h*this.rotatingPointOffset);var g={tl:p,tr:v,br:m,bl:b};return t||(g.ml=y,g.mt=_,g.mr=x,g.mb=C,g.mtr=S),g},setCoords:function(t,e){return this.oCoords=this.calcCoords(t),e||(this.aCoords=this.calcCoords(!0)),t||this._setCornerCoords&&this._setCornerCoords(),this},_calcRotateMatrix:function(){if(this.angle){var t=e(this.angle),i=Math.cos(t),r=Math.sin(t);return 6.123233995736766e-17!==i&&i!==-1.8369701987210297e-16||(i=0),[i,r,-r,i,0,0]}return fabric.iMatrix.concat()},calcTransformMatrix:function(t){var e=this.getCenterPoint(),r=[1,0,0,1,e.x,e.y],n=this._calcRotateMatrix(),s=this._calcDimensionsTransformMatrix(this.skewX,this.skewY,!0),o=this.group&&!t?this.group.calcTransformMatrix():fabric.iMatrix.concat();return o=i(o,r),o=i(o,n),o=i(o,s)},_calcDimensionsTransformMatrix:function(t,r,n){var s=[1,0,Math.tan(e(t)),1],o=[1,Math.tan(e(r)),0,1],a=this.scaleX*(n&&this.flipX?-1:1),h=this.scaleY*(n&&this.flipY?-1:1),c=[a,0,0,h],l=i(c,s,!0);return i(l,o,!0)},_getNonTransformedDimensions:function(){var t=this.strokeWidth,e=this.width+t,i=this.height+t;return{x:e,y:i}},_getTransformedDimensions:function(t,e){"undefined"==typeof t&&(t=this.skewX),"undefined"==typeof e&&(e=this.skewY);var i,r,n=this._getNonTransformedDimensions(),s=n.x/2,o=n.y/2,a=[{x:-s,y:-o},{x:s,y:-o},{x:-s,y:o},{x:s,y:o}],h=this._calcDimensionsTransformMatrix(t,e,!1);for(i=0;i\n'),t?t(e.join("")):e.join("")}}),i.Line.ATTRIBUTE_NAMES=i.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),i.Line.fromElement=function(t,e){e=e||{};var n=i.parseAttributes(t,i.Line.ATTRIBUTE_NAMES),s=[n.x1||0,n.y1||0,n.x2||0,n.y2||0];return e.originX="left",e.originY="top",new i.Line(s,r(n,e))},i.Line.fromObject=function(t,e,r){function s(t){delete t.points,e&&e(t)}var o=n(t,!0);o.points=[t.x1,t.y1,t.x2,t.y2];var a=i.Object._fromObject("Line",o,s,r,"points");return a&&delete a.points,a}}("undefined"!=typeof exports?exports:this),function(t){"use strict";function e(t){return"radius"in t&&t.radius>=0}var i=t.fabric||(t.fabric={}),r=Math.PI,n=i.util.object.extend;if(i.Circle)return void i.warn("fabric.Circle is already defined.");var s=i.Object.prototype.cacheProperties.concat();s.push("radius"),i.Circle=i.util.createClass(i.Object,{type:"circle",radius:0,startAngle:0,endAngle:2*r,cacheProperties:s,initialize:function(t){this.callSuper("initialize",t),this.set("radius",t&&t.radius||0)},_set:function(t,e){return this.callSuper("_set",t,e),"radius"===t&&this.setRadius(e),this},toObject:function(t){return this.callSuper("toObject",["radius","startAngle","endAngle"].concat(t))},toSVG:function(t){var e=this._createBaseSVGMarkup(),i=0,n=0,s=(this.endAngle-this.startAngle)%(2*r);if(0===s)this.group&&"path-group"===this.group.type&&(i=this.left+this.radius,n=this.top+this.radius),e.push("\n');else{var o=Math.cos(this.startAngle)*this.radius,a=Math.sin(this.startAngle)*this.radius,h=Math.cos(this.endAngle)*this.radius,c=Math.sin(this.endAngle)*this.radius,l=s>r?"1":"0";e.push('\n')}return t?t(e.join("")):e.join("")},_render:function(t,e){t.beginPath(),t.arc(e?this.left+this.radius:0,e?this.top+this.radius:0,this.radius,this.startAngle,this.endAngle,!1),this._renderFill(t),this._renderStroke(t)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(t){return this.radius=t,this.set("width",2*t).set("height",2*t)}}),i.Circle.ATTRIBUTE_NAMES=i.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),i.Circle.fromElement=function(t,r){r||(r={});var s=i.parseAttributes(t,i.Circle.ATTRIBUTE_NAMES);if(!e(s))throw new Error("value of `r` attribute is required and can not be negative");s.left=s.left||0,s.top=s.top||0;var o=new i.Circle(n(s,r));return o.left-=o.radius,o.top-=o.radius,o},i.Circle.fromObject=function(t,e,r){return i.Object._fromObject("Circle",t,e,r)}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var e=t.fabric||(t.fabric={});return e.Triangle?void e.warn("fabric.Triangle is already defined"):(e.Triangle=e.util.createClass(e.Object,{type:"triangle",initialize:function(t){this.callSuper("initialize",t),this.set("width",t&&t.width||100).set("height",t&&t.height||100)},_render:function(t){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,i),t.lineTo(0,-i),t.lineTo(e,i),t.closePath(),this._renderFill(t),this._renderStroke(t)},_renderDashedStroke:function(t){var i=this.width/2,r=this.height/2;t.beginPath(),e.util.drawDashedLine(t,-i,r,0,-r,this.strokeDashArray),e.util.drawDashedLine(t,0,-r,i,r,this.strokeDashArray),e.util.drawDashedLine(t,i,r,-i,r,this.strokeDashArray),t.closePath()},toSVG:function(t){var e=this._createBaseSVGMarkup(),i=this.width/2,r=this.height/2,n=[-i+" "+r,"0 "+-r,i+" "+r].join(",");return e.push("'),t?t(e.join("")):e.join("")}}),void(e.Triangle.fromObject=function(t,i,r){return e.Object._fromObject("Triangle",t,i,r)}))}("undefined"!=typeof exports?exports:this),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=2*Math.PI,r=e.util.object.extend;if(e.Ellipse)return void e.warn("fabric.Ellipse is already defined.");var n=e.Object.prototype.cacheProperties.concat();n.push("rx","ry"),e.Ellipse=e.util.createClass(e.Object,{type:"ellipse",rx:0,ry:0,cacheProperties:n,initialize:function(t){this.callSuper("initialize",t),this.set("rx",t&&t.rx||0),this.set("ry",t&&t.ry||0)},_set:function(t,e){switch(this.callSuper("_set",t,e),t){case"rx":this.rx=e,this.set("width",2*e);break;case"ry":this.ry=e,this.set("height",2*e)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},toSVG:function(t){var e=this._createBaseSVGMarkup(),i=0,r=0;return this.group&&"path-group"===this.group.type&&(i=this.left+this.rx,r=this.top+this.ry),e.push("\n'),t?t(e.join("")):e.join("")},_render:function(t,e){t.beginPath(),t.save(),t.transform(1,0,0,this.ry/this.rx,0,0),t.arc(e?this.left+this.rx:0,e?(this.top+this.ry)*this.rx/this.ry:0,this.rx,0,i,!1),t.restore(),this._renderFill(t),this._renderStroke(t)}}),e.Ellipse.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),e.Ellipse.fromElement=function(t,i){i||(i={});var n=e.parseAttributes(t,e.Ellipse.ATTRIBUTE_NAMES);n.left=n.left||0,n.top=n.top||0;var s=new e.Ellipse(r(n,i));return s.top-=s.ry,s.left-=s.rx,s},e.Ellipse.fromObject=function(t,i,r){return e.Object._fromObject("Ellipse",t,i,r)}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend;if(e.Rect)return void e.warn("fabric.Rect is already defined");var r=e.Object.prototype.stateProperties.concat();r.push("rx","ry");var n=e.Object.prototype.cacheProperties.concat();n.push("rx","ry"),e.Rect=e.util.createClass(e.Object,{stateProperties:r,type:"rect",rx:0,ry:0,cacheProperties:n,initialize:function(t){this.callSuper("initialize",t),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(t,e){if(1===this.width&&1===this.height)return void t.fillRect(-.5,-.5,1,1);var i=this.rx?Math.min(this.rx,this.width/2):0,r=this.ry?Math.min(this.ry,this.height/2):0,n=this.width,s=this.height,o=e?this.left:-this.width/2,a=e?this.top:-this.height/2,h=0!==i||0!==r,c=.4477152502;t.beginPath(),t.moveTo(o+i,a),t.lineTo(o+n-i,a),h&&t.bezierCurveTo(o+n-c*i,a,o+n,a+c*r,o+n,a+r),t.lineTo(o+n,a+s-r),h&&t.bezierCurveTo(o+n,a+s-c*r,o+n-c*i,a+s,o+n-i,a+s),t.lineTo(o+i,a+s),h&&t.bezierCurveTo(o+c*i,a+s,o,a+s-c*r,o,a+s-r),t.lineTo(o,a+r),h&&t.bezierCurveTo(o,a+c*r,o+c*i,a,o+i,a),t.closePath(),this._renderFill(t),this._renderStroke(t)},_renderDashedStroke:function(t){var i=-this.width/2,r=-this.height/2,n=this.width,s=this.height;t.beginPath(),e.util.drawDashedLine(t,i,r,i+n,r,this.strokeDashArray),e.util.drawDashedLine(t,i+n,r,i+n,r+s,this.strokeDashArray),e.util.drawDashedLine(t,i+n,r+s,i,r+s,this.strokeDashArray),e.util.drawDashedLine(t,i,r+s,i,r,this.strokeDashArray),t.closePath()},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},toSVG:function(t){var e=this._createBaseSVGMarkup(),i=this.left,r=this.top;return this.group&&"path-group"===this.group.type||(i=-this.width/2,r=-this.height/2),e.push("\n'),t?t(e.join("")):e.join("")}}),e.Rect.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),e.Rect.fromElement=function(t,r){if(!t)return null;r=r||{};var n=e.parseAttributes(t,e.Rect.ATTRIBUTE_NAMES);n.left=n.left||0,n.top=n.top||0;var s=new e.Rect(i(r?e.util.object.clone(r):{},n));return s.visible=s.visible&&s.width>0&&s.height>0,s},e.Rect.fromObject=function(t,i,r){return e.Object._fromObject("Rect",t,i,r)}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.array.min,n=e.util.array.max,s=e.util.toFixed,o=e.Object.NUM_FRACTION_DIGITS;if(e.Polyline)return void e.warn("fabric.Polyline is already defined");var a=e.Object.prototype.cacheProperties.concat();a.push("points"),e.Polyline=e.util.createClass(e.Object,{type:"polyline",points:null,minX:0,minY:0,cacheProperties:a,initialize:function(t,e){e=e||{},this.points=t||[],this.callSuper("initialize",e),this._calcDimensions(),"top"in e||(this.top=this.minY),"left"in e||(this.left=this.minX),this.pathOffset={x:this.minX+this.width/2,y:this.minY+this.height/2}},_calcDimensions:function(){var t=this.points,e=r(t,"x"),i=r(t,"y"),s=n(t,"x"),o=n(t,"y");this.width=s-e||0,this.height=o-i||0,this.minX=e||0,this.minY=i||0},toObject:function(t){return i(this.callSuper("toObject",t),{points:this.points.concat()})},toSVG:function(t){var e,i,r=[],n=this._createBaseSVGMarkup();this.group&&"path-group"===this.group.type||(e=this.pathOffset.x,i=this.pathOffset.y);for(var a=0,h=this.points.length;a\n'),t?t(n.join("")):n.join("")},commonRender:function(t,e){var i,r=this.points.length,n=e?0:this.pathOffset.x,s=e?0:this.pathOffset.y;if(!r||isNaN(this.points[r-1].y))return!1;t.beginPath(),t.moveTo(this.points[0].x-n,this.points[0].y-s);for(var o=0;o"},toObject:function(t){var e=n(this.callSuper("toObject",["sourcePath","pathOffset"].concat(t)),{path:this.path.map(function(t){return t.slice()}),top:this.top,left:this.left});return e},toDatalessObject:function(t){var e=this.toObject(t);return this.sourcePath&&(e.path=this.sourcePath),delete e.sourcePath,e},toSVG:function(t){for(var e=[],i=this._createBaseSVGMarkup(),r="",n=0,s=this.path.length;n\n"),t?t(i.join("")):i.join("")},complexity:function(){return this.path.length},_parsePath:function(){for(var t,e,i,r,n,s=[],o=[],c=/([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/gi,l=0,u=this.path.length;lp)for(var b=1,m=n.length;b\n");for(var s=0,o=e.length;s\n"),t?t(n.join("")):n.join("")},toString:function(){return"#"},isSameColor:function(){var t=this.getObjects()[0].get("fill")||"";return"string"==typeof t&&(t=t.toLowerCase(),this.getObjects().every(function(e){var i=e.get("fill")||"";return"string"==typeof i&&i.toLowerCase()===t}))},complexity:function(){return this.paths.reduce(function(t,e){return t+(e&&e.complexity?e.complexity():0)},0)},getObjects:function(){return this.paths}}),e.PathGroup.fromObject=function(t,i){var r=t.paths;delete t.paths,"string"==typeof r?e.loadSVGFromURL(r,function(n){var s=r,o=e.util.groupSVGElements(n,t,s);t.paths=r,i(o)}):e.util.enlivenObjects(r,function(n){var s=new e.PathGroup(n,t);t.paths=r,i(s)})},void(e.PathGroup.async=!0))}("undefined"!=typeof exports?exports:this),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.array.min,n=e.util.array.max;if(!e.Group){var s={lockMovementX:!0,lockMovementY:!0,lockRotation:!0,lockScalingX:!0,lockScalingY:!0,lockUniScaling:!0};e.Group=e.util.createClass(e.Object,e.Collection,{type:"group",strokeWidth:0,subTargetCheck:!1,initialize:function(t,e,i){e=e||{},this._objects=[],i&&this.callSuper("initialize",e),this._objects=t||[];for(var r=this._objects.length;r--;)this._objects[r].group=this;e.originX&&(this.originX=e.originX),e.originY&&(this.originY=e.originY),i?this._updateObjectsCoords(!0):(this._calcBounds(),this._updateObjectsCoords(),this.callSuper("initialize",e)),this.setCoords(),this.saveCoords()},_updateObjectsCoords:function(t){for(var e=this.getCenterPoint(),i=this._objects.length;i--;)this._updateObjectCoords(this._objects[i],e,t)},_updateObjectCoords:function(t,e,i){if(t.__origHasControls=t.hasControls,t.hasControls=!1,!i){var r=t.getLeft(),n=t.getTop(),s=!0,o=!0;t.set({left:r-e.x,top:n-e.y}),t.setCoords(s,o)}},toString:function(){return"#"},addWithUpdate:function(t){return this._restoreObjectsState(),e.util.resetObjectTransform(this),t&&(this._objects.push(t),t.group=this,t._set("canvas",this.canvas)),this.forEachObject(this._setObjectActive,this),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,this},_setObjectActive:function(t){t.set("active",!0),t.group=this},removeWithUpdate:function(t){return this._restoreObjectsState(),e.util.resetObjectTransform(this),this.forEachObject(this._setObjectActive,this),this.remove(t),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,this},_onObjectAdded:function(t){this.dirty=!0,t.group=this,t._set("canvas",this.canvas)},_onObjectRemoved:function(t){this.dirty=!0,delete t.group,t.set("active",!1)},delegatedProperties:{fill:!0,stroke:!0,strokeWidth:!0,fontFamily:!0,fontWeight:!0,fontSize:!0,fontStyle:!0,lineHeight:!0,textDecoration:!0,textAlign:!0,backgroundColor:!0},_set:function(t,e){var i=this._objects.length;if(this.delegatedProperties[t]||"canvas"===t)for(;i--;)this._objects[i].set(t,e);else for(;i--;)this._objects[i].setOnGroup(t,e);this.callSuper("_set",t,e)},toObject:function(t){var e=this.getObjects().map(function(e){var i=e.includeDefaultValues;e.includeDefaultValues=e.group.includeDefaultValues;var r=e.toObject(t);return e.includeDefaultValues=i,r});return i(this.callSuper("toObject",t),{objects:e})},toDatalessObject:function(t){var e=this.getObjects().map(function(e){var i=e.includeDefaultValues;e.includeDefaultValues=e.group.includeDefaultValues;var r=e.toDatalessObject(t);return e.includeDefaultValues=i,r});return i(this.callSuper("toDatalessObject",t),{objects:e})},render:function(t){this._transformDone=!0,this.callSuper("render",t),this._transformDone=!1},shouldCache:function(){var t=this.objectCaching&&(!this.group||this.needsItsOwnCache||!this.group.isCaching());if(this.caching=t,t)for(var e=0,i=this._objects.length;e\n');for(var i=0,r=this._objects.length;i\n"),t?t(e.join("")):e.join("")},get:function(t){if(t in s){if(this[t])return this[t];for(var e=0,i=this._objects.length;e\n',"\n"),this.stroke||this.strokeDashArray){var o=this.fill;this.fill=null,e.push("\n'),this.fill=o}return e.push("\n"),t?t(e.join("")):e.join("")},getSrc:function(t){var e=t?this._element:this._originalElement;return e?fabric.isLikelyNode?e._src:e.src:this.src||""},setSrc:function(t,e,i){fabric.util.loadImage(t,function(t){return this.setElement(t,e,i)},this,i&&i.crossOrigin)},toString:function(){return'#'},applyFilters:function(t,e,i,r){if(e=e||this.filters,i=i||this._originalElement){var n,s,o=fabric.util.createImage(),a=this.canvas?this.canvas.getRetinaScaling():fabric.devicePixelRatio,h=this.minimumScaleTrigger/a,c=this;if(0===e.length)return this._element=i,t&&t(this),i;var l=fabric.util.createCanvasElement();return l.width=i.width,l.height=i.height,l.getContext("2d").drawImage(i,0,0,i.width,i.height),e.forEach(function(t){t&&(r?(n=c.scaleX0?90*Math.round((t-1)/90):90*Math.round(t/90)},straighten:function(){return this.setAngle(this._getAngleValueForStraighten()),this},fxStraighten:function(t){t=t||{};var e=function(){},i=t.onComplete||e,r=t.onChange||e,n=this;return fabric.util.animate({startValue:this.get("angle"),endValue:this._getAngleValueForStraighten(),duration:this.FX_DURATION,onChange:function(t){n.setAngle(t),r()},onComplete:function(){n.setCoords(),i()},onStart:function(){n.set("active",!1)}}),this}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{straightenObject:function(t){return t.straighten(),this.renderAll(),this},fxStraightenObject:function(t){return t.fxStraighten({onChange:this.renderAll.bind(this)}),this}}),fabric.Image.filters=fabric.Image.filters||{},fabric.Image.filters.BaseFilter=fabric.util.createClass({type:"BaseFilter",initialize:function(t){t&&this.setOptions(t)},setOptions:function(t){for(var e in t)this[e]=t[e]},toObject:function(){return{type:this.type}},toJSON:function(){return this.toObject()}}),fabric.Image.filters.BaseFilter.fromObject=function(t,e){var i=new fabric.Image.filters[t.type](t);return e&&e(i),i},function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.Image.filters,n=e.util.createClass;r.Brightness=n(r.BaseFilter,{type:"Brightness",initialize:function(t){t=t||{},this.brightness=t.brightness||0},applyTo:function(t){for(var e=t.getContext("2d"),i=e.getImageData(0,0,t.width,t.height),r=i.data,n=this.brightness,s=0,o=r.length;sb||o<0||o>v||(h=4*(a*v+o),c=l[S*d+w],e+=p[h]*c,i+=p[h+1]*c,r+=p[h+2]*c,n+=p[h+3]*c);y[s]=e,y[s+1]=i,y[s+2]=r,y[s+3]=n+_*(255-n)}u.putImageData(m,0,0)},toObject:function(){return i(this.callSuper("toObject"),{opaque:this.opaque,matrix:this.matrix})}}),e.Image.filters.Convolute.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.Image.filters,n=e.util.createClass;r.GradientTransparency=n(r.BaseFilter,{type:"GradientTransparency",initialize:function(t){t=t||{},this.threshold=t.threshold||100},applyTo:function(t){for(var e=t.getContext("2d"),i=e.getImageData(0,0,t.width,t.height),r=i.data,n=this.threshold,s=r.length,o=0,a=r.length;o-1?t.channel:0},applyTo:function(t){if(this.mask){var i,r=t.getContext("2d"),n=r.getImageData(0,0,t.width,t.height),s=n.data,o=this.mask.getElement(),a=e.util.createCanvasElement(),h=this.channel,c=n.width*n.height*4;a.width=t.width,a.height=t.height,a.getContext("2d").drawImage(o,0,0,t.width,t.height);var l=a.getContext("2d").getImageData(0,0,t.width,t.height),u=l.data;for(i=0;ic&&i>c&&r>c&&l(e-i)i&&(l=2,f=-1),a>n&&(u=2,d=-1),h=c.getImageData(0,0,i,n),t.width=o(s,i),t.height=o(a,n),c.putImageData(h,0,0);!g||!p;)i=v,n=b,s*ft)return 0;if(e*=Math.PI,s(e)<1e-16)return 1;var i=e/t;return h(e)*h(i)/e/i}}function f(t){var h,c,u,d,g,k,M,D,A,P,I;for(T.x=(t+.5)*y,j.x=r(T.x),h=0;h=e)){P=r(1e3*s(c-T.x)),O[P]||(O[P]={});for(var E=j.y-w;E<=j.y+w;E++)E<0||E>=o||(I=r(1e3*s(E-T.y)),O[P][I]||(O[P][I]=m(n(i(P*x,2)+i(I*C,2))/1e3)),u=O[P][I],u>0&&(d=4*(E*e+c),g+=u,k+=u*v[d],M+=u*v[d+1],D+=u*v[d+2],A+=u*v[d+3]))}d=4*(h*a+t),b[d]=k/g,b[d+1]=M/g,b[d+2]=D/g,b[d+3]=A/g}return++t1&&L<-1||(x=2*L*L*L-3*L*L+1,x>0&&(E=4*(I+M*e),j+=x*p[E+3],S+=x,p[E+3]<255&&(x=x*p[E+3]/250),w+=x*p[E],O+=x*p[E+1],T+=x*p[E+2],C+=x))}b[_]=w/C,b[_+1]=O/C,b[_+2]=T/C,b[_+3]=j/S}return v},toObject:function(){return{type:this.type,scaleX:this.scaleX,scaleY:this.scaleY,resizeType:this.resizeType,lanczosLobes:this.lanczosLobes}}}),e.Image.filters.Resize.fromObject=e.Image.filters.BaseFilter.fromObject}("undefined"!=typeof exports?exports:this),function(t){"use strict";var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.Image.filters,n=e.util.createClass;r.ColorMatrix=n(r.BaseFilter,{type:"ColorMatrix",initialize:function(t){t||(t={}),this.matrix=t.matrix||[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0]},applyTo:function(t){var e,i,r,n,s,o=t.getContext("2d"),a=o.getImageData(0,0,t.width,t.height),h=a.data,c=h.length,l=this.matrix;for(e=0;e'},_getCacheCanvasDimensions:function(){var t=this.callSuper("_getCacheCanvasDimensions"),e=2*this.fontSize;return t.width+=e*t.zoomX,t.height+=e*t.zoomY,t},_render:function(t){this._setTextStyles(t),this.group&&"path-group"===this.group.type&&t.translate(this.left,this.top),this._renderTextLinesBackground(t),this._renderText(t),this._renderTextDecoration(t)},_renderText:function(t){this._renderTextFill(t),this._renderTextStroke(t)},_setTextStyles:function(t){t.textBaseline="alphabetic",t.font=this._getFontDeclaration()},_getTextHeight:function(){return this._getHeightOfSingleLine()+(this._textLines.length-1)*this._getHeightOfLine()},_getTextWidth:function(t){for(var e=this._getLineWidth(t,0),i=1,r=this._textLines.length;ie&&(e=n)}return e},_renderChars:function(t,e,i,r,n){var s,o,a=t.slice(0,-4);if(this[a].toLive){var h=-this.width/2+this[a].offsetX||0,c=-this.height/2+this[a].offsetY||0;e.save(),e.translate(h,c),r-=h,n-=c}if(0!==this.charSpacing){var l=this._getWidthOfCharSpacing();i=i.split("");for(var u=0,f=i.length;u0?o:0}else e[t](i,r,n);this[a].toLive&&e.restore()},_renderTextLine:function(t,e,i,r,n,s){n-=this.fontSize*this._fontSizeFraction;var o=this._getLineWidth(e,s);if("justify"!==this.textAlign||this.width0?u/f:0,g=0,p=0,v=h.length;p0?n:0},_getLeftOffset:function(){return-this.width/2},_getTopOffset:function(){return-this.height/2},isEmptyStyles:function(){return!0},_renderTextCommon:function(t,e){for(var i=0,r=this._getLeftOffset(),n=this._getTopOffset(),s=0,o=this._textLines.length;s0&&(r=this._getLineLeftOffset(i),t.fillRect(this._getLeftOffset()+r,this._getTopOffset()+n,i,e/this.lineHeight)),n+=e;t.fillStyle=s,this._removeShadow(t)}},_getLineLeftOffset:function(t){return"center"===this.textAlign?(this.width-t)/2:"right"===this.textAlign?this.width-t:0},_clearCache:function(){this.__lineWidths=[],this.__lineHeights=[]},_shouldClearDimensionCache:function(){var t=this._forceClearCache;return t||(t=this.hasStateChanged("_dimensionAffectingProps")),t&&(this.saveState({propertySet:"_dimensionAffectingProps"}),this.dirty=!0),t},_getLineWidth:function(t,e){if(this.__lineWidths[e])return this.__lineWidths[e]===-1?this.width:this.__lineWidths[e];var i,r,n=this._textLines[e];return i=""===n?0:this._measureLine(t,e),this.__lineWidths[e]=i,i&&"justify"===this.textAlign&&(r=n.split(/\s+/),r.length>1&&(this.__lineWidths[e]=-1)),i},_getWidthOfCharSpacing:function(){return 0!==this.charSpacing?this.fontSize*this.charSpacing/1e3:0},_measureLine:function(t,e){var i,r,n=this._textLines[e],s=t.measureText(n).width,o=0;return 0!==this.charSpacing&&(i=n.split("").length,o=(i-1)*this._getWidthOfCharSpacing()),r=s+o,r>0?r:0},_renderTextDecoration:function(t){function e(e){var n,s,o,a,h,c,l,u=0;for(n=0,s=r._textLines.length;n-1&&n.push(.85),this.textDecoration.indexOf("line-through")>-1&&n.push(.43),this.textDecoration.indexOf("overline")>-1&&n.push(-.12),n.length>0&&e(n)}},_getFontDeclaration:function(){return[e.isLikelyNode?this.fontWeight:this.fontStyle,e.isLikelyNode?this.fontStyle:this.fontWeight,this.fontSize+"px",e.isLikelyNode?'"'+this.fontFamily+'"':this.fontFamily].join(" ")},render:function(t,e){this.visible&&(this.canvas&&this.canvas.skipOffscreen&&!this.group&&!this.isOnScreen()||(this._shouldClearDimensionCache()&&(this._setTextStyles(t),this._initDimensions(t)),this.callSuper("render",t,e)))},_splitTextIntoLines:function(){return this.text.split(this._reNewline)},toObject:function(t){var e=["text","fontSize","fontWeight","fontFamily","fontStyle","lineHeight","textDecoration","textAlign","textBackgroundColor","charSpacing"].concat(t);return this.callSuper("toObject",e)},toSVG:function(t){this.ctx||(this.ctx=e.util.createCanvasElement().getContext("2d"));var i=this._createBaseSVGMarkup(),r=this._getSVGLeftTopOffsets(this.ctx),n=this._getSVGTextAndBg(r.textTop,r.textLeft);return this._wrapSVGTextAndBg(i,n),t?t(i.join("")):i.join("")},_getSVGLeftTopOffsets:function(t){var e=this._getHeightOfLine(t,0),i=-this.width/2,r=0;return{textLeft:i+(this.group&&"path-group"===this.group.type?this.left:0),textTop:r+(this.group&&"path-group"===this.group.type?-this.top:0),lineTop:e}},_wrapSVGTextAndBg:function(t,e){var i=!0,r=this.getSvgFilter(),n=""===r?"":' style="'+r+'"';t.push("\t\n",e.textBgRects.join(""),"\t\t\n',e.textSpans.join(""),"\t\t\n","\t\n")},_getSVGTextAndBg:function(t,e){var i=[],r=[],n=0;this._setSVGBg(r);for(var s=0,o=this._textLines.length;s",e.util.string.escapeXml(this._textLines[t]),"\n")},_setSVGTextLineJustifed:function(t,n,s,o){var a=e.util.createCanvasElement().getContext("2d");this._setTextStyles(a);var h,c,l=this._textLines[t],u=l.split(/\s+/),f=this._getWidthOfWords(a,u.join("")),d=this.width-f,g=u.length-1,p=g>0?d/g:0,v=this._getFillAttributes(this.fill);for(o+=this._getLineLeftOffset(this._getLineWidth(a,t)),t=0,c=u.length;t",e.util.string.escapeXml(h),"\n"),o+=this._getWidthOfWords(a,h)+p},_setSVGTextLineBg:function(t,e,n,s,o){t.push("\t\t\n')},_setSVGBg:function(t){this.backgroundColor&&t.push("\t\t\n')},_getFillAttributes:function(t){var i=t&&"string"==typeof t?new e.Color(t):"";return i&&i.getSource()&&1!==i.getAlpha()?'opacity="'+i.getAlpha()+'" fill="'+i.setAlpha(1).toRgb()+'"':'fill="'+t+'"'},_set:function(t,e){this.callSuper("_set",t,e),this._dimensionAffectingProps.indexOf(t)>-1&&(this._initDimensions(),this.setCoords())},complexity:function(){return 1}}),e.Text.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("x y dx dy font-family font-style font-weight font-size text-decoration text-anchor".split(" ")),e.Text.DEFAULT_SVG_FONT_SIZE=16,e.Text.fromElement=function(t,i){if(!t)return null;var r=e.parseAttributes(t,e.Text.ATTRIBUTE_NAMES);i=e.util.object.extend(i?e.util.object.clone(i):{},r),i.top=i.top||0,i.left=i.left||0,"dx"in r&&(i.left+=r.dx),"dy"in r&&(i.top+=r.dy),"fontSize"in i||(i.fontSize=e.Text.DEFAULT_SVG_FONT_SIZE),i.originX||(i.originX="left");var n="";"textContent"in t?n=t.textContent:"firstChild"in t&&null!==t.firstChild&&"data"in t.firstChild&&null!==t.firstChild.data&&(n=t.firstChild.data),n=n.replace(/^\s+|\s+$|\n+/g,"").replace(/\s+/g," ");var s=new e.Text(n,i),o=s.getHeight()/s.height,a=(s.height+s.strokeWidth)*s.lineHeight-s.height,h=a*o,c=s.getHeight()+h,l=0;return"left"===s.originX&&(l=s.getWidth()/2),"right"===s.originX&&(l=-s.getWidth()/2),s.set({left:s.getLeft()+l,top:s.getTop()-c/2+s.fontSize*(.18+s._fontSizeFraction)/s.lineHeight}),s},e.Text.fromObject=function(t,i,r){return e.Object._fromObject("Text",t,i,r,"text")},e.util.createAccessors(e.Text)}("undefined"!=typeof exports?exports:this),function(){var t=fabric.util.object.clone;fabric.IText=fabric.util.createClass(fabric.Text,fabric.Observable,{type:"i-text",selectionStart:0,selectionEnd:0,selectionColor:"rgba(17,119,255,0.3)",isEditing:!1,editable:!0,editingBorderColor:"rgba(102,153,255,0.25)",cursorWidth:2,cursorColor:"#333",cursorDelay:1e3,cursorDuration:600,styles:null,caching:!0,_reSpace:/\s|\n/,_currentCursorOpacity:0,_selectionDirection:null,_abortCursorAnimation:!1,__widthOfSpace:[],initialize:function(t,e){this.styles=e?e.styles||{}:{},this.callSuper("initialize",t,e),this.initBehavior()},_clearCache:function(){this.callSuper("_clearCache"),this.__widthOfSpace=[]},isEmptyStyles:function(){if(!this.styles)return!0;var t=this.styles;for(var e in t)for(var i in t[e])for(var r in t[e][i])return!1;return!0},setSelectionStart:function(t){t=Math.max(t,0),this._updateAndFire("selectionStart",t)},setSelectionEnd:function(t){t=Math.min(t,this.text.length),this._updateAndFire("selectionEnd",t)},_updateAndFire:function(t,e){this[t]!==e&&(this._fireSelectionChanged(),this[t]=e),this._updateTextarea()},_fireSelectionChanged:function(){this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},getSelectionStyles:function(t,e){if(2===arguments.length){for(var i=[],r=t;r0?a:0,lineLeft:r},this.cursorOffsetCache=i,this.cursorOffsetCache},renderCursor:function(t,e){var i=this.get2DCursorLocation(),r=i.lineIndex,n=i.charIndex,s=this.getCurrentCharFontSize(r,n),o=t.leftOffset,a=this.scaleX*this.canvas.getZoom(),h=this.cursorWidth/a;e.fillStyle=this.getCurrentCharColor(r,n),e.globalAlpha=this.__isMousedown?1:this._currentCursorOpacity,e.fillRect(t.left+o-h/2,t.top+t.topOffset,h,s)},renderSelection:function(t,e,i){i.fillStyle=this.selectionColor;for(var r=this.get2DCursorLocation(this.selectionStart),n=this.get2DCursorLocation(this.selectionEnd),s=r.lineIndex,o=n.lineIndex,a=s;a<=o;a++){var h=this._getLineLeftOffset(this._getLineWidth(i,a))||0,c=this._getHeightOfLine(this.ctx,a),l=0,u=0,f=this._textLines[a];if(a===s){for(var d=0,g=f.length;d=r.charIndex&&(a!==o||ds&&a1)&&(c/=this.lineHeight),i.fillRect(e.left+h,e.top+e.topOffset,u>0?u:0,c),e.topOffset+=l}},_renderChars:function(t,e,i,r,n,s,o){if(this.isEmptyStyles())return this._renderCharsFast(t,e,i,r,n);o=o||0;var a,h,c=this._getHeightOfLine(e,s),l="";e.save(),n-=c/this.lineHeight*this._fontSizeFraction;for(var u=o,f=i.length+o;u<=f;u++)a=a||this.getCurrentCharStyle(s,u),h=this.getCurrentCharStyle(s,u+1),(this._hasStyleChanged(a,h)||u===f)&&(this._renderChar(t,e,s,u-1,l,r,n,c),l="",a=h),l+=i[u-o];e.restore()},_renderCharsFast:function(t,e,i,r,n){"fillText"===t&&this.fill&&this.callSuper("_renderChars",t,e,i,r,n),"strokeText"===t&&(this.stroke&&this.strokeWidth>0||this.skipFillStrokeCheck)&&this.callSuper("_renderChars",t,e,i,r,n)},_renderChar:function(t,e,i,r,n,s,o,a){var h,c,l,u,f,d,g,p,v,b=this._getStyleDeclaration(i,r);if(b?(c=this._getHeightOfChar(e,n,i,r),u=b.stroke,l=b.fill,d=b.textDecoration):c=this.fontSize,u=(u||this.stroke)&&"strokeText"===t,l=(l||this.fill)&&"fillText"===t,b&&e.save(),h=this._applyCharStylesGetWidth(e,n,i,r,b||null),d=d||this.textDecoration,b&&b.textBackgroundColor&&this._removeShadow(e),0!==this.charSpacing){p=this._getWidthOfCharSpacing(),g=n.split(""),h=0;for(var m,y=0,_=g.length;y<_;y++)m=g[y],l&&e.fillText(m,s+h,o),u&&e.strokeText(m,s+h,o),v=e.measureText(m).width+p,h+=v>0?v:0}else l&&e.fillText(n,s,o),u&&e.strokeText(n,s,o);(d||""!==d)&&(f=this._fontSizeFraction*a/this.lineHeight,this._renderCharDecoration(e,d,s,o,f,h,c)),b&&e.restore(),e.translate(h,0)},_hasStyleChanged:function(t,e){return t.fill!==e.fill||t.fontSize!==e.fontSize||t.textBackgroundColor!==e.textBackgroundColor||t.textDecoration!==e.textDecoration||t.fontFamily!==e.fontFamily||t.fontWeight!==e.fontWeight||t.fontStyle!==e.fontStyle||t.stroke!==e.stroke||t.strokeWidth!==e.strokeWidth},_renderCharDecoration:function(t,e,i,r,n,s,o){if(e){var a,h,c=o/15,l={underline:r+o/10,"line-through":r-o*(this._fontSizeFraction+this._fontSizeMult-1)+c,overline:r-(this._fontSizeMult-this._fontSizeFraction)*o},u=["underline","line-through","overline"];for(a=0;a-1&&t.fillRect(i,l[h],s,c)}},_renderTextLine:function(t,e,i,r,n,s){this.isEmptyStyles()||(n+=this.fontSize*(this._fontSizeFraction+.03)),this.callSuper("_renderTextLine",t,e,i,r,n,s)},_renderTextDecoration:function(t){if(this.isEmptyStyles())return this.callSuper("_renderTextDecoration",t)},_renderTextLinesBackground:function(t){this.callSuper("_renderTextLinesBackground",t);var e,i,r,n,s,o,a,h,c,l,u=0,f=this._getLeftOffset(),d=this._getTopOffset(),g="";t.save();for(var p=0,v=this._textLines.length;p0?n:0},_getHeightOfChar:function(t,e,i){var r=this._getStyleDeclaration(e,i);return r&&r.fontSize?r.fontSize:this.fontSize},_getWidthOfCharsAt:function(t,e,i){var r,n,s=0;for(r=0;r0?i:0},_getWidthOfSpace:function(t,e){if(this.__widthOfSpace[e])return this.__widthOfSpace[e];var i=this._textLines[e],r=this._getWidthOfWords(t,i,e,0),n=this.width-r,s=i.length-i.replace(this._reSpacesAndTabs,"").length,o=Math.max(n/s,t.measureText(" ").width);return this.__widthOfSpace[e]=o,o},_getWidthOfWords:function(t,e,i,r){for(var n=0,s=0;sr&&(r=o); -}return this.__lineHeights[e]=r*this.lineHeight*this._fontSizeMult,this.__lineHeights[e]},_getTextHeight:function(t){for(var e,i=0,r=0,n=this._textLines.length;r-1;)e++,i--;return t-e},findWordBoundaryRight:function(t){var e=0,i=t;if(this._reSpace.test(this.text.charAt(i)))for(;this._reSpace.test(this.text.charAt(i));)e++,i++;for(;/\S/.test(this.text.charAt(i))&&i-1;)e++,i--;return t-e},findLineBoundaryRight:function(t){for(var e=0,i=t;!/\n/.test(this.text.charAt(i))&&i0&&ithis.__selectionStartOnMouseDown?(this.selectionStart=this.__selectionStartOnMouseDown,this.selectionEnd=e):(this.selectionStart=e,this.selectionEnd=this.__selectionStartOnMouseDown),this.selectionStart===i&&this.selectionEnd===r||(this.restartCursorIfNeeded(),this._fireSelectionChanged(),this._updateTextarea(),this.renderCursorOrSelection()))}},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},_updateTextarea:function(){if(this.hiddenTextarea&&!this.inCompositionMode&&(this.cursorOffsetCache={},this.hiddenTextarea.value=this.text,this.hiddenTextarea.selectionStart=this.selectionStart,this.hiddenTextarea.selectionEnd=this.selectionEnd,this.selectionStart===this.selectionEnd)){var t=this._calcTextareaPosition();this.hiddenTextarea.style.left=t.left,this.hiddenTextarea.style.top=t.top,this.hiddenTextarea.style.fontSize=t.fontSize}},_calcTextareaPosition:function(){if(!this.canvas)return{x:1,y:1};var t=this.text.split(""),e=this._getCursorBoundaries(t,"cursor"),i=this.get2DCursorLocation(),r=i.lineIndex,n=i.charIndex,s=this.getCurrentCharFontSize(r,n),o=e.leftOffset,a=this.calcTransformMatrix(),h={x:e.left+o,y:e.top+e.topOffset+s},c=this.canvas.upperCanvasEl,l=c.width-s,u=c.height-s;return h=fabric.util.transformPoint(h,a),h=fabric.util.transformPoint(h,this.canvas.viewportTransform),h.x<0&&(h.x=0),h.x>l&&(h.x=l),h.y<0&&(h.y=0),h.y>u&&(h.y=u),h.x+=this.canvas._offset.left,h.y+=this.canvas._offset.top,{left:h.x+"px",top:h.y+"px",fontSize:s}},_saveEditingProps:function(){this._savedProps={hasControls:this.hasControls,borderColor:this.borderColor,lockMovementX:this.lockMovementX,lockMovementY:this.lockMovementY,hoverCursor:this.hoverCursor,defaultCursor:this.canvas&&this.canvas.defaultCursor,moveCursor:this.canvas&&this.canvas.moveCursor}},_restoreEditingProps:function(){this._savedProps&&(this.hoverCursor=this._savedProps.overCursor,this.hasControls=this._savedProps.hasControls,this.borderColor=this._savedProps.borderColor,this.lockMovementX=this._savedProps.lockMovementX,this.lockMovementY=this._savedProps.lockMovementY,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor))},exitEditing:function(){var t=this._textBeforeEdit!==this.text;return this.selected=!1,this.isEditing=!1,this.selectable=!0,this.selectionEnd=this.selectionStart,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,this.fire("editing:exited"),t&&this.fire("modified"),this.canvas&&(this.canvas.off("mouse:move",this.mouseMoveHandler),this.canvas.fire("text:editing:exited",{target:this}),t&&this.canvas.fire("object:modified",{target:this})),this},_removeExtraneousStyles:function(){for(var t in this.styles)this._textLines[t]||delete this.styles[t]},_removeCharsFromTo:function(t,e){for(;e!==t;)this._removeSingleCharAndStyle(t+1),e--;this.selectionStart=t,this.selectionEnd=t},_removeSingleCharAndStyle:function(t){var e="\n"===this.text[t-1],i=e?t:t-1;this.removeStyleObject(e,i),this.text=this.text.slice(0,t-1)+this.text.slice(t),this._textLines=this._splitTextIntoLines()},insertChars:function(t,e){var i;if(this.selectionEnd-this.selectionStart>1&&this._removeCharsFromTo(this.selectionStart,this.selectionEnd),!e&&this.isEmptyStyles())return void this.insertChar(t,!1);for(var r=0,n=t.length;r=i&&(o=!0,s[h-i]=this.styles[e][a],delete this.styles[e][a])}o&&(this.styles[e+1]=s)}this._forceClearCache=!0},insertCharStyleObject:function(e,i,r){var n=this.styles[e],s=t(n);0!==i||r||(i=1);for(var o in s){var a=parseInt(o,10);a>=i&&(n[a+1]=s[a],s[a-1]||delete n[a])}var h=r||t(n[i-1]);h&&(this.styles[e][i]=h),this._forceClearCache=!0},insertStyleObjects:function(t,e,i){var r=this.get2DCursorLocation(),n=r.lineIndex,s=r.charIndex;this._getLineStyle(n)||this._setLineStyle(n,{}),"\n"===t?this.insertNewlineStyleObject(n,s,e):this.insertCharStyleObject(n,s,i)},shiftLineStyles:function(e,i){var r=t(this.styles);for(var n in r){var s=parseInt(n,10);s<=e&&delete r[s]}for(var n in this.styles){var s=parseInt(n,10);s>e&&(this.styles[s+i]=r[s],r[s-i]||delete this.styles[s])}},removeStyleObject:function(t,e){var i=this.get2DCursorLocation(e),r=i.lineIndex,n=i.charIndex;this._removeStyleObject(t,i,r,n)},_getTextOnPreviousLine:function(t){return this._textLines[t-1]},_removeStyleObject:function(e,i,r,n){if(e){var s=this._getTextOnPreviousLine(i.lineIndex),o=s?s.length:0;this.styles[r-1]||(this.styles[r-1]={});for(n in this.styles[r])this.styles[r-1][parseInt(n,10)+o]=this.styles[r][n];this.shiftLineStyles(i.lineIndex,-1)}else{var a=this.styles[r];a&&delete a[n];var h=t(a);for(var c in h){var l=parseInt(c,10);l>=n&&0!==l&&(a[l-1]=h[l],delete a[l])}}},insertNewline:function(){this.insertChars("\n")},setSelectionStartEndWithShift:function(t,e,i){i<=t?(e===t?this._selectionDirection="left":"right"===this._selectionDirection&&(this._selectionDirection="left",this.selectionEnd=t),this.selectionStart=i):i>t&&it?this.selectionStart=t:this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd>t?this.selectionEnd=t:this.selectionEnd<0&&(this.selectionEnd=0)}})}(),fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+new Date,this.__lastLastClickTime=+new Date,this.__lastPointer={},this.on("mousedown",this.onMouseDown.bind(this))},onMouseDown:function(t){this.__newClickTime=+new Date;var e=this.canvas.getPointer(t.e);this.isTripleClick(e)?(this.fire("tripleclick",t),this._stopEvent(t.e)):this.isDoubleClick(e)&&(this.fire("dblclick",t),this._stopEvent(t.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=e,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected},isDoubleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y&&this.__lastIsEditing},isTripleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y},_stopEvent:function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation()},initCursorSelectionHandlers:function(){this.initMousedownHandler(),this.initMouseupHandler(),this.initClicks()},initClicks:function(){this.on("dblclick",function(t){this.selectWord(this.getSelectionStartFromPointer(t.e))}),this.on("tripleclick",function(t){this.selectLine(this.getSelectionStartFromPointer(t.e))})},initMousedownHandler:function(){this.on("mousedown",function(t){if(this.editable){var e=this.canvas.getPointer(t.e);this.__mousedownX=e.x,this.__mousedownY=e.y,this.__isMousedown=!0,this.selected&&this.setCursorByClick(t.e),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.selectionStart===this.selectionEnd&&this.abortCursorAnimation(),this.renderCursorOrSelection())}})},_isObjectMoved:function(t){var e=this.canvas.getPointer(t);return this.__mousedownX!==e.x||this.__mousedownY!==e.y},initMouseupHandler:function(){this.on("mouseup",function(t){this.__isMousedown=!1,this.editable&&!this._isObjectMoved(t.e)&&(this.__lastSelected&&!this.__corner&&(this.enterEditing(t.e),this.selectionStart===this.selectionEnd?this.initDelayedCursor(!0):this.renderCursorOrSelection()),this.selected=!0)})},setCursorByClick:function(t){var e=this.getSelectionStartFromPointer(t),i=this.selectionStart,r=this.selectionEnd;t.shiftKey?this.setSelectionStartEndWithShift(i,r,e):(this.selectionStart=e,this.selectionEnd=e),this.isEditing&&(this._fireSelectionChanged(),this._updateTextarea())},getSelectionStartFromPointer:function(t){for(var e,i,r=this.getLocalPointer(t),n=0,s=0,o=0,a=0,h=0,c=this._textLines.length;hs?0:1,h=r+a;return this.flipX&&(h=n-h),h>this.text.length&&(h=this.text.length),h}}),fabric.util.object.extend(fabric.IText.prototype,{initHiddenTextarea:function(){this.hiddenTextarea=fabric.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off");var t=this._calcTextareaPosition();this.hiddenTextarea.style.cssText="white-space: nowrap; position: absolute; top: "+t.top+"; left: "+t.left+"; opacity: 0; width: 1px; height: 1px; z-index: -999;",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.cut.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)),!this._clickHandlerInitialized&&this.canvas&&(fabric.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},_keysMap:{8:"removeChars",9:"exitEditing",27:"exitEditing",13:"insertNewline",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown",46:"forwardDelete"},_ctrlKeysMapUp:{67:"copy",88:"cut"},_ctrlKeysMapDown:{65:"selectAll"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(t){if(this.isEditing){if(t.keyCode in this._keysMap)this[this._keysMap[t.keyCode]](t);else{if(!(t.keyCode in this._ctrlKeysMapDown&&(t.ctrlKey||t.metaKey)))return;this[this._ctrlKeysMapDown[t.keyCode]](t)}t.stopImmediatePropagation(),t.preventDefault(),t.keyCode>=33&&t.keyCode<=40?(this.clearContextTop(),this.renderCursorOrSelection()):this.canvas&&this.canvas.renderAll()}},onKeyUp:function(t){return!this.isEditing||this._copyDone?void(this._copyDone=!1):void(t.keyCode in this._ctrlKeysMapUp&&(t.ctrlKey||t.metaKey)&&(this[this._ctrlKeysMapUp[t.keyCode]](t),t.stopImmediatePropagation(),t.preventDefault(),this.canvas&&this.canvas.renderAll()))},onInput:function(t){if(this.isEditing&&!this.inCompositionMode){var e,i,r,n=this.selectionStart||0,s=this.selectionEnd||0,o=this.text.length,a=this.hiddenTextarea.value.length;a>o?(r="left"===this._selectionDirection?s:n,e=a-o,i=this.hiddenTextarea.value.slice(r,r+e)):(e=a-o+s-n,i=this.hiddenTextarea.value.slice(n,n+e)),this.insertChars(i),t.stopPropagation()}},onCompositionStart:function(){this.inCompositionMode=!0,this.prevCompositionLength=0,this.compositionStart=this.selectionStart},onCompositionEnd:function(){this.inCompositionMode=!1},onCompositionUpdate:function(t){var e=t.data;this.selectionStart=this.compositionStart,this.selectionEnd=this.selectionEnd===this.selectionStart?this.compositionStart+this.prevCompositionLength:this.selectionEnd,this.insertChars(e,!1),this.prevCompositionLength=e.length},forwardDelete:function(t){if(this.selectionStart===this.selectionEnd){if(this.selectionStart===this.text.length)return;this.moveCursorRight(t)}this.removeChars(t)},copy:function(t){if(this.selectionStart!==this.selectionEnd){var e=this.getSelectedText(),i=this._getClipboardData(t);i&&i.setData("text",e),fabric.copiedText=e,fabric.copiedTextStyle=this.getSelectionStyles(this.selectionStart,this.selectionEnd),t.stopImmediatePropagation(),t.preventDefault(),this._copyDone=!0}},paste:function(t){var e=null,i=this._getClipboardData(t),r=!0;i?(e=i.getData("text").replace(/\r/g,""),fabric.copiedTextStyle&&fabric.copiedText===e||(r=!1)):e=fabric.copiedText,e&&this.insertChars(e,r),t.stopImmediatePropagation(),t.preventDefault()},cut:function(t){this.selectionStart!==this.selectionEnd&&(this.copy(t),this.removeChars(t))},_getClipboardData:function(t){return t&&t.clipboardData||fabric.window.clipboardData},_getWidthBeforeCursor:function(t,e){for(var i,r=this._textLines[t].slice(0,e),n=this._getLineWidth(this.ctx,t),s=this._getLineLeftOffset(n),o=0,a=r.length;oe){i=!0;var f=o-u,d=o,g=Math.abs(f-e),p=Math.abs(d-e);a=p=this.text.length&&this.selectionEnd>=this.text.length||this._moveCursorUpOrDown("Down",t)},moveCursorUp:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorUpOrDown("Up",t)},_moveCursorUpOrDown:function(t,e){var i="get"+t+"CursorOffset",r=this[i](e,"right"===this._selectionDirection);e.shiftKey?this.moveCursorWithShift(r):this.moveCursorWithoutShift(r),0!==r&&(this.setSelectionInBoundaries(),this.abortCursorAnimation(),this._currentCursorOpacity=1,this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorWithShift:function(t){var e="left"===this._selectionDirection?this.selectionStart+t:this.selectionEnd+t;return this.setSelectionStartEndWithShift(this.selectionStart,this.selectionEnd,e),0!==t},moveCursorWithoutShift:function(t){return t<0?(this.selectionStart+=t,this.selectionEnd=this.selectionStart):(this.selectionEnd+=t,this.selectionStart=this.selectionEnd),0!==t},moveCursorLeft:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorLeftOrRight("Left",t)},_move:function(t,e,i){var r;if(t.altKey)r=this["findWordBoundary"+i](this[e]);else{if(!t.metaKey&&35!==t.keyCode&&36!==t.keyCode)return this[e]+="Left"===i?-1:1,!0;r=this["findLineBoundary"+i](this[e])}if(void 0!==typeof r&&this[e]!==r)return this[e]=r,!0},_moveLeft:function(t,e){return this._move(t,e,"Left")},_moveRight:function(t,e){return this._move(t,e,"Right")},moveCursorLeftWithoutShift:function(t){var e=!0;return this._selectionDirection="left",this.selectionEnd===this.selectionStart&&0!==this.selectionStart&&(e=this._moveLeft(t,"selectionStart")),this.selectionEnd=this.selectionStart,e},moveCursorLeftWithShift:function(t){return"right"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveLeft(t,"selectionEnd"):0!==this.selectionStart?(this._selectionDirection="left",this._moveLeft(t,"selectionStart")):void 0},moveCursorRight:function(t){this.selectionStart>=this.text.length&&this.selectionEnd>=this.text.length||this._moveCursorLeftOrRight("Right",t)},_moveCursorLeftOrRight:function(t,e){var i="moveCursor"+t+"With";this._currentCursorOpacity=1,i+=e.shiftKey?"Shift":"outShift",this[i](e)&&(this.abortCursorAnimation(),this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorRightWithShift:function(t){return"left"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveRight(t,"selectionStart"):this.selectionEnd!==this.text.length?(this._selectionDirection="right",this._moveRight(t,"selectionEnd")):void 0},moveCursorRightWithoutShift:function(t){var e=!0;return this._selectionDirection="right",this.selectionStart===this.selectionEnd?(e=this._moveRight(t,"selectionStart"),this.selectionEnd=this.selectionStart):this.selectionStart=this.selectionEnd,e},removeChars:function(t){this.selectionStart===this.selectionEnd?this._removeCharsNearCursor(t):this._removeCharsFromTo(this.selectionStart,this.selectionEnd),this.set("dirty",!0),this.setSelectionEnd(this.selectionStart),this._removeExtraneousStyles(),this.canvas&&this.canvas.renderAll(),this.setCoords(),this.fire("changed"),this.canvas&&this.canvas.fire("text:changed",{target:this})},_removeCharsNearCursor:function(t){if(0!==this.selectionStart)if(t.metaKey){var e=this.findLineBoundaryLeft(this.selectionStart);this._removeCharsFromTo(e,this.selectionStart),this.setSelectionStart(e)}else if(t.altKey){var i=this.findWordBoundaryLeft(this.selectionStart);this._removeCharsFromTo(i,this.selectionStart),this.setSelectionStart(i)}else this._removeSingleCharAndStyle(this.selectionStart),this.setSelectionStart(this.selectionStart-1)}}),function(){var t=fabric.util.toFixed,e=fabric.Object.NUM_FRACTION_DIGITS;fabric.util.object.extend(fabric.IText.prototype,{_setSVGTextLineText:function(t,e,i,r,n,s){this._getLineStyle(t)?this._setSVGTextLineChars(t,e,i,r,s):fabric.Text.prototype._setSVGTextLineText.call(this,t,e,i,r,n)},_setSVGTextLineChars:function(t,e,i,r,n){for(var s=this._textLines[t],o=0,a=this._getLineLeftOffset(this._getLineWidth(this.ctx,t))-this.width/2,h=this._getSVGLineTopOffset(t),c=this._getHeightOfLine(this.ctx,t),l=0,u=s.length;l\n'].join("")},_createTextCharSpan:function(i,r,n,s,o){var a=this.getSvgStyles.call(fabric.util.object.extend({visible:!0,fill:this.fill,stroke:this.stroke,type:"text",getSvgFilter:fabric.Object.prototype.getSvgFilter},r));return['\t\t\t',fabric.util.string.escapeXml(i),"\n"].join("")}})}(),function(t){"use strict";var e=t.fabric||(t.fabric={});e.Textbox=e.util.createClass(e.IText,e.Observable,{type:"textbox",minWidth:20,dynamicMinWidth:2,__cachedLines:null,lockScalingY:!0,lockScalingFlip:!0,noScaleCache:!1,initialize:function(t,i){this.callSuper("initialize",t,i),this.setControlsVisibility(e.Textbox.getTextboxControlVisibility()),this.ctx=this.objectCaching?this._cacheContext:e.util.createCanvasElement().getContext("2d"),this._dimensionAffectingProps.push("width")},_initDimensions:function(t){this.__skipDimension||(t||(t=e.util.createCanvasElement().getContext("2d"),this._setTextStyles(t),this.clearContextTop()),this.dynamicMinWidth=0,this._textLines=this._splitTextIntoLines(t),this.dynamicMinWidth>this.width&&this._set("width",this.dynamicMinWidth),this._clearCache(),this.height=this._getTextHeight(t))},_generateStyleMap:function(){for(var t=0,e=0,i=0,r={},n=0;n0?(e=0,i++,t++):" "===this.text[i]&&n>0&&(e++,i++),r[n]={line:t,offset:e},i+=this._textLines[n].length,e+=this._textLines[n].length;return r},_getStyleDeclaration:function(t,e,i){if(this._styleMap){var r=this._styleMap[t];if(!r)return i?{}:null;t=r.line,e=r.offset+e}return this.callSuper("_getStyleDeclaration",t,e,i)},_setStyleDeclaration:function(t,e,i){var r=this._styleMap[t];t=r.line,e=r.offset+e,this.styles[t][e]=i},_deleteStyleDeclaration:function(t,e){var i=this._styleMap[t];t=i.line,e=i.offset+e,delete this.styles[t][e]},_getLineStyle:function(t){var e=this._styleMap[t];return this.styles[e.line]},_setLineStyle:function(t,e){var i=this._styleMap[t];this.styles[i.line]=e},_deleteLineStyle:function(t){var e=this._styleMap[t];delete this.styles[e.line]},_wrapText:function(t,e){var i,r=e.split(this._reNewline),n=[];for(i=0;i=this.width&&!d?(n.push(s),s="",r=l,d=!0):r+=g,d||(s+=c),s+=a,u=this._measureText(t,c,i,h),h++,d=!1,l>f&&(f=l);return p&&n.push(s),f>this.dynamicMinWidth&&(this.dynamicMinWidth=f-g),n},_splitTextIntoLines:function(t){t=t||this.ctx;var e=this.textAlign;this._styleMap=null,t.save(),this._setTextStyles(t),this.textAlign="left";var i=this._wrapText(t,this.text);return this.textAlign=e,t.restore(),this._textLines=i,this._styleMap=this._generateStyleMap(),i},setOnGroup:function(t,e){"scaleX"===t&&(this.set("scaleX",Math.abs(1/e)),this.set("width",this.get("width")*e/("undefined"==typeof this.__oldScaleX?1:this.__oldScaleX)),this.__oldScaleX=e)},get2DCursorLocation:function(t){"undefined"==typeof t&&(t=this.selectionStart);for(var e=this._textLines.length,i=0,r=0;r=h.getMinWidth()?(h.set("width",c),!0):void 0},fabric.Group.prototype._refreshControlsVisibility=function(){if("undefined"!=typeof fabric.Textbox)for(var t=this._objects.length;t--;)if(this._objects[t]instanceof fabric.Textbox)return void this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility())},fabric.util.object.extend(fabric.Textbox.prototype,{_removeExtraneousStyles:function(){for(var t in this._styleMap)this._textLines[t]||delete this.styles[this._styleMap[t].line]},insertCharStyleObject:function(t,e,i){var r=this._styleMap[t];t=r.line,e=r.offset+e,fabric.IText.prototype.insertCharStyleObject.apply(this,[t,e,i])},insertNewlineStyleObject:function(t,e,i){var r=this._styleMap[t];t=r.line,e=r.offset+e,fabric.IText.prototype.insertNewlineStyleObject.apply(this,[t,e,i])},shiftLineStyles:function(t,e){var i=this._styleMap[t];t=i.line,fabric.IText.prototype.shiftLineStyles.call(this,t,e)},_getTextOnPreviousLine:function(t){for(var e=this._textLines[t-1];this._styleMap[t-2]&&this._styleMap[t-2].line===this._styleMap[t-1].line;)e=this._textLines[t-2]+e, -t--;return e},removeStyleObject:function(t,e){var i=this.get2DCursorLocation(e),r=this._styleMap[i.lineIndex],n=r.line,s=r.offset+i.charIndex;this._removeStyleObject(t,i,n,s)}})}(),function(){var t=fabric.IText.prototype._getNewSelectionStartFromOffset;fabric.IText.prototype._getNewSelectionStartFromOffset=function(e,i,r,n,s){n=t.call(this,e,i,r,n,s);for(var o=0,a=0,h=0;h=n));h++)"\n"!==this.text[o+a]&&" "!==this.text[o+a]||a++;return n-h+a}}(),function(){function request(t,e,i){var r=URL.parse(t);r.port||(r.port=0===r.protocol.indexOf("https:")?443:80);var n=0===r.protocol.indexOf("https:")?HTTPS:HTTP,s=n.request({hostname:r.hostname,port:r.port,path:r.path,method:"GET"},function(t){var r="";e&&t.setEncoding(e),t.on("end",function(){i(r)}),t.on("data",function(e){200===t.statusCode&&(r+=e)})});s.on("error",function(t){t.errno===process.ECONNREFUSED?fabric.log("ECONNREFUSED: connection refused to "+r.hostname+":"+r.port):fabric.log(t.message),i(null)}),s.end()}function requestFs(t,e){var i=require("fs");i.readFile(t,function(t,i){if(t)throw fabric.log(t),t;e(i)})}if("undefined"==typeof document||"undefined"==typeof window){var DOMParser=require("xmldom").DOMParser,URL=require("url"),HTTP=require("http"),HTTPS=require("https"),Canvas=require("canvas"),Image=require("canvas").Image;fabric.util.loadImage=function(t,e,i){function r(r){r?(n.src=new Buffer(r,"binary"),n._src=t,e&&e.call(i,n)):(n=null,e&&e.call(i,null,!0))}var n=new Image;t&&(t instanceof Buffer||0===t.indexOf("data"))?(n.src=n._src=t,e&&e.call(i,n)):t&&0!==t.indexOf("http")?requestFs(t,r):t?request(t,"binary",r):e&&e.call(i,t)},fabric.loadSVGFromURL=function(t,e,i){t=t.replace(/^\n\s*/,"").replace(/\?.*$/,"").trim(),0!==t.indexOf("http")?requestFs(t,function(t){fabric.loadSVGFromString(t.toString(),e,i)}):request(t,"",function(t){fabric.loadSVGFromString(t,e,i)})},fabric.loadSVGFromString=function(t,e,i){var r=(new DOMParser).parseFromString(t);fabric.parseSVGDocument(r.documentElement,function(t,i){e&&e(t,i)},i)},fabric.util.getScript=function(url,callback){request(url,"",function(body){eval(body),callback&&callback()})},fabric.createCanvasForNode=function(t,e,i,r){r=r||i;var n=fabric.document.createElement("canvas"),s=new Canvas(t||600,e||600,r),o=new Canvas(t||600,e||600,r);n.style={},n.width=s.width,n.height=s.height,i=i||{},i.nodeCanvas=s,i.nodeCacheCanvas=o;var a=fabric.Canvas||fabric.StaticCanvas,h=new a(n,i);return h.nodeCanvas=s,h.nodeCacheCanvas=o,h.contextContainer=s.getContext("2d"),h.contextCache=o.getContext("2d"),h.Font=Canvas.Font,h};var originaInitStatic=fabric.StaticCanvas.prototype._initStatic;fabric.StaticCanvas.prototype._initStatic=function(t,e){t=t||fabric.document.createElement("canvas"),this.nodeCanvas=new Canvas(t.width,t.height),this.nodeCacheCanvas=new Canvas(t.width,t.height),originaInitStatic.call(this,t,e),this.contextContainer=this.nodeCanvas.getContext("2d"),this.contextCache=this.nodeCacheCanvas.getContext("2d"),this.Font=Canvas.Font},fabric.StaticCanvas.prototype.createPNGStream=function(){return this.nodeCanvas.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(t){return this.nodeCanvas.createJPEGStream(t)},fabric.StaticCanvas.prototype._initRetinaScaling=function(){if(this._isRetinaScaling())return this.lowerCanvasEl.setAttribute("width",this.width*fabric.devicePixelRatio),this.lowerCanvasEl.setAttribute("height",this.height*fabric.devicePixelRatio),this.nodeCanvas.width=this.width*fabric.devicePixelRatio,this.nodeCanvas.height=this.height*fabric.devicePixelRatio,this.contextContainer.scale(fabric.devicePixelRatio,fabric.devicePixelRatio),this},fabric.Canvas&&(fabric.Canvas.prototype._initRetinaScaling=fabric.StaticCanvas.prototype._initRetinaScaling);var origSetBackstoreDimension=fabric.StaticCanvas.prototype._setBackstoreDimension;fabric.StaticCanvas.prototype._setBackstoreDimension=function(t,e){return origSetBackstoreDimension.call(this,t,e),this.nodeCanvas[t]=e,this},fabric.Canvas&&(fabric.Canvas.prototype._setBackstoreDimension=fabric.StaticCanvas.prototype._setBackstoreDimension)}}(); \ No newline at end of file +var fabric=fabric||{version:"5.3.0"};if("undefined"!=typeof exports?exports.fabric=fabric:"function"==typeof define&&define.amd&&define([],function(){return fabric}),"undefined"!=typeof document&&"undefined"!=typeof window)document instanceof("undefined"!=typeof HTMLDocument?HTMLDocument:Document)?fabric.document=document:fabric.document=document.implementation.createHTMLDocument(""),fabric.window=window;else{var jsdom=require("jsdom"),virtualWindow=new jsdom.JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]},resources:"usable"}).window;fabric.document=virtualWindow.document,fabric.jsdomImplForWrapper=require("jsdom/lib/jsdom/living/generated/utils").implForWrapper,fabric.nodeCanvas=require("jsdom/lib/jsdom/utils").Canvas,fabric.window=virtualWindow,DOMParser=fabric.window.DOMParser}function resizeCanvasIfNeeded(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,s=t.destinationHeight;i===n&&r===s||(e.width=n,e.height=s)}function copyGLTo2DDrawImage(t,e){var i=t.canvas,r=e.targetCanvas,n=r.getContext("2d");n.translate(0,r.height),n.scale(1,-1);var s=i.height-r.height;n.drawImage(i,0,s,r.width,r.height,0,0,r.width,r.height)}function copyGLTo2DPutImageData(t,e){var i=e.targetCanvas.getContext("2d"),r=e.destinationWidth,n=e.destinationHeight,s=r*n*4,o=new Uint8Array(this.imageBuffer,0,s),a=new Uint8ClampedArray(this.imageBuffer,0,s);t.readPixels(0,0,r,n,t.RGBA,t.UNSIGNED_BYTE,o);var c=new ImageData(a,r,n);i.putImageData(c,0,0)}fabric.isTouchSupported="ontouchstart"in fabric.window||"ontouchstart"in fabric.document||fabric.window&&fabric.window.navigator&&0_)for(var C=1,S=d.length;Ct[i-2].x?1:n.x===t[i-2].x?0:-1,c=n.y>t[i-2].y?1:n.y===t[i-2].y?0:-1),r.push(["L",n.x+a*e,n.y+c*e]),r},fabric.util.getPathSegmentsInfo=l,fabric.util.getBoundsOfCurve=function(t,e,i,r,n,s,o,a){var c;if(fabric.cachesBoundsOfCurve&&(c=A.call(arguments),fabric.boundsOfCurveCache[c]))return fabric.boundsOfCurveCache[c];var h,l,u,f,d,g,p,v,m=Math.sqrt,b=Math.min,y=Math.max,_=Math.abs,x=[],C=[[],[]];l=6*t-12*i+6*n,h=-3*t+9*i-9*n+3*o,u=3*i-3*t;for(var S=0;S<2;++S)if(0/g,">")},graphemeSplit:function(t){var e,i=0,r=[];for(i=0;it.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,e){return void 0===e&&(e=.5),e=Math.max(Math.min(1,e),0),new i(this.x+(t.x-this.x)*e,this.y+(t.y-this.y)*e)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new i(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new i(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new i(this.x,this.y)}}}("undefined"!=typeof exports?exports:this),function(t){"use strict";var f=t.fabric||(t.fabric={});function d(t){this.status=t,this.points=[]}f.Intersection?f.warn("fabric.Intersection is already defined"):(f.Intersection=d,f.Intersection.prototype={constructor:d,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},f.Intersection.intersectLineLine=function(t,e,i,r){var n,s=(r.x-i.x)*(t.y-i.y)-(r.y-i.y)*(t.x-i.x),o=(e.x-t.x)*(t.y-i.y)-(e.y-t.y)*(t.x-i.x),a=(r.y-i.y)*(e.x-t.x)-(r.x-i.x)*(e.y-t.y);if(0!==a){var c=s/a,h=o/a;0<=c&&c<=1&&0<=h&&h<=1?(n=new d("Intersection")).appendPoint(new f.Point(t.x+c*(e.x-t.x),t.y+c*(e.y-t.y))):n=new d}else n=new d(0===s||0===o?"Coincident":"Parallel");return n},f.Intersection.intersectLinePolygon=function(t,e,i){var r,n,s,o,a=new d,c=i.length;for(o=0;o=c&&(h.x-=c),h.x<=-c&&(h.x+=c),h.y>=c&&(h.y-=c),h.y<=c&&(h.y+=c),h.x-=o.offsetX,h.y-=o.offsetY,h}function y(t){return t.flipX!==t.flipY}function _(t,e,i,r,n){if(0!==t[e]){var s=n/t._getTransformedDimensions()[r]*t[i];t.set(i,s)}}function x(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(0,s.skewY),a=P(e,e.originX,e.originY,i,r),c=Math.abs(2*a.x)-o.x,h=s.skewX;c<2?n=0:(n=v(Math.atan2(c/s.scaleX,o.y/s.scaleY)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),y(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().y;s.set("skewX",n),_(s,"skewY","scaleY","y",u)}return l}function C(t,e,i,r){var n,s=e.target,o=s._getTransformedDimensions(s.skewX,0),a=P(e,e.originX,e.originY,i,r),c=Math.abs(2*a.y)-o.y,h=s.skewY;c<2?n=0:(n=v(Math.atan2(c/s.scaleY,o.x/s.scaleX)),e.originX===f&&e.originY===p&&(n=-n),e.originX===g&&e.originY===d&&(n=-n),y(s)&&(n=-n));var l=h!==n;if(l){var u=s._getTransformedDimensions().x;s.set("skewY",n),_(s,"skewX","scaleX","x",u)}return l}function E(t,e,i,r,n){n=n||{};var s,o,a,c,h,l,u=e.target,f=u.lockScalingX,d=u.lockScalingY,g=n.by,p=w(t,u),v=k(u,g,p),m=e.gestureScale;if(v)return!1;if(m)o=e.scaleX*m,a=e.scaleY*m;else{if(s=P(e,e.originX,e.originY,i,r),h="y"!==g?T(s.x):1,l="x"!==g?T(s.y):1,e.signX||(e.signX=h),e.signY||(e.signY=l),u.lockScalingFlip&&(e.signX!==h||e.signY!==l))return!1;if(c=u._getTransformedDimensions(),p&&!g){var b=Math.abs(s.x)+Math.abs(s.y),y=e.original,_=b/(Math.abs(c.x*y.scaleX/u.scaleX)+Math.abs(c.y*y.scaleY/u.scaleY));o=y.scaleX*_,a=y.scaleY*_}else o=Math.abs(s.x*u.scaleX/c.x),a=Math.abs(s.y*u.scaleY/c.y);O(e)&&(o*=2,a*=2),e.signX!==h&&"y"!==g&&(e.originX=S[e.originX],o*=-1,e.signX=h),e.signY!==l&&"x"!==g&&(e.originY=S[e.originY],a*=-1,e.signY=l)}var x=u.scaleX,C=u.scaleY;return g?("x"===g&&u.set("scaleX",o),"y"===g&&u.set("scaleY",a)):(!f&&u.set("scaleX",o),!d&&u.set("scaleY",a)),x!==u.scaleX||C!==u.scaleY}n.scaleCursorStyleHandler=function(t,e,i){var r=w(t,i),n="";if(0!==e.x&&0===e.y?n="x":0===e.x&&0!==e.y&&(n="y"),k(i,n,r))return"not-allowed";var s=a(i,e);return o[s]+"-resize"},n.skewCursorStyleHandler=function(t,e,i){var r="not-allowed";if(0!==e.x&&i.lockSkewingY)return r;if(0!==e.y&&i.lockSkewingX)return r;var n=a(i,e)%4;return s[n]+"-resize"},n.scaleSkewCursorStyleHandler=function(t,e,i){return t[i.canvas.altActionKey]?n.skewCursorStyleHandler(t,e,i):n.scaleCursorStyleHandler(t,e,i)},n.rotationWithSnapping=b("rotating",m(function(t,e,i,r){var n=e,s=n.target,o=s.translateToOriginPoint(s.getCenterPoint(),n.originX,n.originY);if(s.lockRotation)return!1;var a,c=Math.atan2(n.ey-o.y,n.ex-o.x),h=Math.atan2(r-o.y,i-o.x),l=v(h-c+n.theta);if(0o.r2,h=this.gradientTransform?this.gradientTransform.concat():fabric.iMatrix.concat(),l=-this.offsetX,u=-this.offsetY,f=!!e.additionalTransform,d="pixels"===this.gradientUnits?"userSpaceOnUse":"objectBoundingBox";if(a.sort(function(t,e){return t.offset-e.offset}),"objectBoundingBox"===d?(l/=t.width,u/=t.height):(l+=t.width/2,u+=t.height/2),"path"===t.type&&"percentage"!==this.gradientUnits&&(l-=t.pathOffset.x,u-=t.pathOffset.y),h[4]-=l,h[5]-=u,s='id="SVGID_'+this.id+'" gradientUnits="'+d+'"',s+=' gradientTransform="'+(f?e.additionalTransform+" ":"")+fabric.util.matrixToSVG(h)+'" ',"linear"===this.type?n=["\n']:"radial"===this.type&&(n=["\n']),"radial"===this.type){if(c)for((a=a.concat()).reverse(),i=0,r=a.length;i\n')}return n.push("linear"===this.type?"\n":"\n"),n.join("")},toLive:function(t){var e,i,r,n=fabric.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;i\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e=this.source;if(!e)return"";if(void 0!==e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}})}(),function(t){"use strict";var o=t.fabric||(t.fabric={}),a=o.util.toFixed;o.Shadow?o.warn("fabric.Shadow is already defined."):(o.Shadow=o.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,nonScaling:!1,initialize:function(t){for(var e in"string"==typeof t&&(t=this._parseShadow(t)),t)this[e]=t[e];this.id=o.Object.__uid++},_parseShadow:function(t){var e=t.trim(),i=o.Shadow.reOffsetsAndBlur.exec(e)||[];return{color:(e.replace(o.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)").trim(),offsetX:parseFloat(i[1],10)||0,offsetY:parseFloat(i[2],10)||0,blur:parseFloat(i[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var e=40,i=40,r=o.Object.NUM_FRACTION_DIGITS,n=o.util.rotateVector({x:this.offsetX,y:this.offsetY},o.util.degreesToRadians(-t.angle)),s=new o.Color(this.color);return t.width&&t.height&&(e=100*a((Math.abs(n.x)+this.blur)/t.width,r)+20,i=100*a((Math.abs(n.y)+this.blur)/t.height,r)+20),t.flipX&&(n.x*=-1),t.flipY&&(n.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},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 e={},i=o.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke","nonScaling"].forEach(function(t){this[t]!==i[t]&&(e[t]=this[t])},this),e}}),o.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/)}("undefined"!=typeof exports?exports:this),function(){"use strict";if(fabric.StaticCanvas)fabric.warn("fabric.StaticCanvas is already defined.");else{var n=fabric.util.object.extend,t=fabric.util.getElementOffset,h=fabric.util.removeFromArray,a=fabric.util.toFixed,s=fabric.util.transformPoint,o=fabric.util.invertTransform,i=fabric.util.getNodeCanvas,r=fabric.util.createCanvasElement,e=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass(fabric.CommonMethods,{initialize:function(t,e){e||(e={}),this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:fabric.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,clipPath:void 0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1\n'),this._setSVGBgOverlayColor(i,"background"),this._setSVGBgOverlayImage(i,"backgroundImage",e),this._setSVGObjects(i,e),this.clipPath&&i.push("\n"),this._setSVGBgOverlayColor(i,"overlay"),this._setSVGBgOverlayImage(i,"overlayImage",e),i.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,r=e.width||this.width,n=e.height||this.height,s='viewBox="0 0 '+this.width+" "+this.height+'" ',o=fabric.Object.NUM_FRACTION_DIGITS;e.viewBox?s='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,s='viewBox="'+a(-i[4]/i[0],o)+" "+a(-i[5]/i[3],o)+" "+a(this.width/i[0],o)+" "+a(this.height/i[3],o)+'" '),t.push("\n',"Created with Fabric.js ",fabric.version,"\n","\n",this.createSVGFontFacesMarkup(),this.createSVGRefElementsMarkup(),this.createSVGClipPathMarkup(e),"\n")},createSVGClipPathMarkup:function(t){var e=this.clipPath;return e?(e.clipPathId="CLIPPATH_"+fabric.Object.__uid++,'\n'+this.clipPath.toClipPathSVG(t.reviver)+"\n"):""},createSVGRefElementsMarkup:function(){var s=this;return["background","overlay"].map(function(t){var e=s[t+"Color"];if(e&&e.toLive){var i=s[t+"Vpt"],r=s.viewportTransform,n={width:s.width/(i?r[0]:1),height:s.height/(i?r[3]:1)};return e.toSVG(n,{additionalTransform:i?fabric.util.matrixToSVG(r):""})}}).join("")},createSVGFontFacesMarkup:function(){var t,e,i,r,n,s,o,a,c="",h={},l=fabric.fontPaths,u=[];for(this._objects.forEach(function t(e){u.push(e),e._objects&&e._objects.forEach(t)}),o=0,a=u.length;o',"\n",c,"","\n"].join("")),c},_setSVGObjects:function(t,e){var i,r,n,s=this._objects;for(r=0,n=s.length;r\n")}else t.push('\n")},sendToBack:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(e=(r=n._objects).length;e--;)i=r[e],h(this._objects,i),this._objects.unshift(i);else h(this._objects,t),this._objects.unshift(t);return this.renderOnAddRemove&&this.requestRenderAll(),this},bringToFront:function(t){if(!t)return this;var e,i,r,n=this._activeObject;if(t===n&&"activeSelection"===t.type)for(r=n._objects,e=0;e"}}),n(fabric.StaticCanvas.prototype,fabric.Observable),n(fabric.StaticCanvas.prototype,fabric.Collection),n(fabric.StaticCanvas.prototype,fabric.DataURLExporter),n(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(t){var e=r();if(!e||!e.getContext)return null;var i=e.getContext("2d");if(!i)return null;switch(t){case"setLineDash":return void 0!==i.setLineDash;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject,fabric.isLikelyNode&&(fabric.StaticCanvas.prototype.createPNGStream=function(){var t=i(this.lowerCanvasEl);return t&&t.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(t){var e=i(this.lowerCanvasEl);return e&&e.createJPEGStream(t)})}}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",strokeMiterLimit:10,strokeDashArray:null,limitedToCanvasSize:!1,_setBrushStyles:function(t){t.strokeStyle=this.color,t.lineWidth=this.width,t.lineCap=this.strokeLineCap,t.miterLimit=this.strokeMiterLimit,t.lineJoin=this.strokeLineJoin,t.setLineDash(this.strokeDashArray||[])},_saveAndTransform:function(t){var e=this.canvas.viewportTransform;t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5])},_setShadow:function(){if(this.shadow){var t=this.canvas,e=this.shadow,i=t.contextTop,r=t.getZoom();t&&t._isRetinaScaling()&&(r*=fabric.devicePixelRatio),i.shadowColor=e.color,i.shadowBlur=e.blur*r,i.shadowOffsetX=e.offsetX*r,i.shadowOffsetY=e.offsetY*r}},needsFullRender:function(){return new fabric.Color(this.color).getAlpha()<1||!!this.shadow},_resetShadow:function(){var t=this.canvas.contextTop;t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0},_isOutSideCanvas:function(t){return t.x<0||t.x>this.canvas.getWidth()||t.y<0||t.y>this.canvas.getHeight()}}),fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{decimate:.4,drawStraightLine:!1,straightLineKey:"shiftKey",initialize:function(t){this.canvas=t,this._points=[]},needsFullRender:function(){return this.callSuper("needsFullRender")||this._hasStraightLine},_drawSegment:function(t,e,i){var r=e.midPointFrom(i);return t.quadraticCurveTo(e.x,e.y,r.x,r.y),r},onMouseDown:function(t,e){this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],this._prepareForDrawing(t),this._captureDrawingPath(t),this._render())},onMouseMove:function(t,e){if(this.canvas._isMainEvent(e.e)&&(this.drawStraightLine=e.e[this.straightLineKey],(!0!==this.limitedToCanvasSize||!this._isOutSideCanvas(t))&&this._captureDrawingPath(t)&&1"},getObjectScaling:function(){if(!this.group)return{scaleX:this.scaleX,scaleY:this.scaleY};var t=x.util.qrDecompose(this.calcTransformMatrix());return{scaleX:Math.abs(t.scaleX),scaleY:Math.abs(t.scaleY)}},getTotalObjectScaling:function(){var t=this.getObjectScaling(),e=t.scaleX,i=t.scaleY;if(this.canvas){var r=this.canvas.getZoom(),n=this.canvas.getRetinaScaling();e*=r*n,i*=r*n}return{scaleX:e,scaleY:i}},getObjectOpacity:function(){var t=this.opacity;return this.group&&(t*=this.group.getObjectOpacity()),t},_set:function(t,e){var i="scaleX"===t||"scaleY"===t,r=this[t]!==e,n=!1;return i&&(e=this._constrainScale(e)),"scaleX"===t&&e<0?(this.flipX=!this.flipX,e*=-1):"scaleY"===t&&e<0?(this.flipY=!this.flipY,e*=-1):"shadow"!==t||!e||e instanceof x.Shadow?"dirty"===t&&this.group&&this.group.set("dirty",e):e=new x.Shadow(e),this[t]=e,r&&(n=this.group&&this.group.isOnACache(),-1=t.x&&n.left+n.width<=e.x&&n.top>=t.y&&n.top+n.height<=e.y},containsPoint:function(t,e,i,r){var n=this._getCoords(i,r),s=(e=e||this._getImageLines(n),this._findCrossPoints(t,e));return 0!==s&&s%2==1},isOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.getCoords(!0,t).some(function(t){return t.x<=i.x&&t.x>=e.x&&t.y<=i.y&&t.y>=e.y})||(!!this.intersectsWithRect(e,i,!0,t)||this._containsCenterOfCanvas(e,i,t))},_containsCenterOfCanvas:function(t,e,i){var r={x:(t.x+e.x)/2,y:(t.y+e.y)/2};return!!this.containsPoint(r,null,!0,i)},isPartiallyOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.intersectsWithRect(e,i,!0,t)||this.getCoords(!0,t).every(function(t){return(t.x>=i.x||t.x<=e.x)&&(t.y>=i.y||t.y<=e.y)})&&this._containsCenterOfCanvas(e,i,t)},_getImageLines:function(t){return{topline:{o:t.tl,d:t.tr},rightline:{o:t.tr,d:t.br},bottomline:{o:t.br,d:t.bl},leftline:{o:t.bl,d:t.tl}}},_findCrossPoints:function(t,e){var i,r,n,s=0;for(var o in e)if(!((n=e[o]).o.y=t.y&&n.d.y>=t.y||(n.o.x===n.d.x&&n.o.x>=t.x?r=n.o.x:(0,i=(n.d.y-n.o.y)/(n.d.x-n.o.x),r=-(t.y-0*t.x-(n.o.y-i*n.o.x))/(0-i)),r>=t.x&&(s+=1),2!==s)))break;return s},getBoundingRect:function(t,e){var i=this.getCoords(t,e);return h.makeBoundingBoxFromPoints(i)},getScaledWidth:function(){return this._getTransformedDimensions().x},getScaledHeight:function(){return this._getTransformedDimensions().y},_constrainScale:function(t){return Math.abs(t)\n')}},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(t),{reviver:t})},toClipPathSVG:function(t){return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(t),{reviver:t})},_createBaseClipPathSVGMarkup:function(t,e){var i=(e=e||{}).reviver,r=e.additionalTransform||"",n=[this.getSvgTransform(!0,r),this.getSvgCommons()].join(""),s=t.indexOf("COMMON_PARTS");return t[s]=n,i?i(t.join("")):t.join("")},_createBaseSVGMarkup:function(t,e){var i,r,n=(e=e||{}).noStyle,s=e.reviver,o=n?"":'style="'+this.getSvgStyles()+'" ',a=e.withShadow?'style="'+this.getSvgFilter()+'" ':"",c=this.clipPath,h=this.strokeUniform?'vector-effect="non-scaling-stroke" ':"",l=c&&c.absolutePositioned,u=this.stroke,f=this.fill,d=this.shadow,g=[],p=t.indexOf("COMMON_PARTS"),v=e.additionalTransform;return c&&(c.clipPathId="CLIPPATH_"+fabric.Object.__uid++,r='\n'+c.toClipPathSVG(s)+"\n"),l&&g.push("\n"),g.push("\n"),i=[o,h,n?"":this.addPaintOrder()," ",v?'transform="'+v+'" ':""].join(""),t[p]=i,f&&f.toLive&&g.push(f.toSVG(this)),u&&u.toLive&&g.push(u.toSVG(this)),d&&g.push(d.toSVG(this)),c&&g.push(r),g.push(t.join("")),g.push("\n"),l&&g.push("\n"),s?s(g.join("")):g.join("")},addPaintOrder:function(){return"fill"!==this.paintFirst?' paint-order="'+this.paintFirst+'" ':""}})}(),function(){var n=fabric.util.object.extend,r="stateProperties";function s(e,t,i){var r={};i.forEach(function(t){r[t]=e[t]}),n(e[t],r,!0)}fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(t){var e="_"+(t=t||r);return Object.keys(this[e]).length\n']}}),s.Line.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),s.Line.fromElement=function(t,e,i){i=i||{};var r=s.parseAttributes(t,s.Line.ATTRIBUTE_NAMES),n=[r.x1||0,r.y1||0,r.x2||0,r.y2||0];e(new s.Line(n,o(r,i)))},s.Line.fromObject=function(t,e){var i=r(t,!0);i.points=[t.x1,t.y1,t.x2,t.y2],s.Object._fromObject("Line",i,function(t){delete t.points,e&&e(t)},"points")})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var s=t.fabric||(t.fabric={}),o=s.util.degreesToRadians;s.Circle?s.warn("fabric.Circle is already defined."):(s.Circle=s.util.createClass(s.Object,{type:"circle",radius:0,startAngle:0,endAngle:360,cacheProperties:s.Object.prototype.cacheProperties.concat("radius","startAngle","endAngle"),_set:function(t,e){return this.callSuper("_set",t,e),"radius"===t&&this.setRadius(e),this},toObject:function(t){return this.callSuper("toObject",["radius","startAngle","endAngle"].concat(t))},_toSVG:function(){var t,e=(this.endAngle-this.startAngle)%360;if(0===e)t=["\n'];else{var i=o(this.startAngle),r=o(this.endAngle),n=this.radius;t=['\n"]}return t},_render:function(t){t.beginPath(),t.arc(0,0,this.radius,o(this.startAngle),o(this.endAngle),!1),this._renderPaintInOrder(t)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(t){return this.radius=t,this.set("width",2*t).set("height",2*t)}}),s.Circle.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),s.Circle.fromElement=function(t,e){var i,r=s.parseAttributes(t,s.Circle.ATTRIBUTE_NAMES);if(!("radius"in(i=r)&&0<=i.radius))throw new Error("value of `r` attribute is required and can not be negative");r.left=(r.left||0)-r.radius,r.top=(r.top||0)-r.radius,e(new s.Circle(r))},s.Circle.fromObject=function(t,e){s.Object._fromObject("Circle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var i=t.fabric||(t.fabric={});i.Triangle?i.warn("fabric.Triangle is already defined"):(i.Triangle=i.util.createClass(i.Object,{type:"triangle",width:100,height:100,_render:function(t){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,i),t.lineTo(0,-i),t.lineTo(e,i),t.closePath(),this._renderPaintInOrder(t)},_toSVG:function(){var t=this.width/2,e=this.height/2;return["']}}),i.Triangle.fromObject=function(t,e){return i.Object._fromObject("Triangle",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var r=t.fabric||(t.fabric={}),e=2*Math.PI;r.Ellipse?r.warn("fabric.Ellipse is already defined."):(r.Ellipse=r.util.createClass(r.Object,{type:"ellipse",rx:0,ry:0,cacheProperties:r.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this.set("rx",t&&t.rx||0),this.set("ry",t&&t.ry||0)},_set:function(t,e){switch(this.callSuper("_set",t,e),t){case"rx":this.rx=e,this.set("width",2*e);break;case"ry":this.ry=e,this.set("height",2*e)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']},_render:function(t){t.beginPath(),t.save(),t.transform(1,0,0,this.ry/this.rx,0,0),t.arc(0,0,this.rx,0,e,!1),t.restore(),this._renderPaintInOrder(t)}}),r.Ellipse.ATTRIBUTE_NAMES=r.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),r.Ellipse.fromElement=function(t,e){var i=r.parseAttributes(t,r.Ellipse.ATTRIBUTE_NAMES);i.left=(i.left||0)-i.rx,i.top=(i.top||0)-i.ry,e(new r.Ellipse(i))},r.Ellipse.fromObject=function(t,e){r.Object._fromObject("Ellipse",t,e)})}("undefined"!=typeof exports?exports:this),function(t){"use strict";var s=t.fabric||(t.fabric={}),o=s.util.object.extend;s.Rect?s.warn("fabric.Rect is already defined"):(s.Rect=s.util.createClass(s.Object,{stateProperties:s.Object.prototype.stateProperties.concat("rx","ry"),type:"rect",rx:0,ry:0,cacheProperties:s.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(t){var e=this.rx?Math.min(this.rx,this.width/2):0,i=this.ry?Math.min(this.ry,this.height/2):0,r=this.width,n=this.height,s=-this.width/2,o=-this.height/2,a=0!==e||0!==i,c=.4477152502;t.beginPath(),t.moveTo(s+e,o),t.lineTo(s+r-e,o),a&&t.bezierCurveTo(s+r-c*e,o,s+r,o+c*i,s+r,o+i),t.lineTo(s+r,o+n-i),a&&t.bezierCurveTo(s+r,o+n-c*i,s+r-c*e,o+n,s+r-e,o+n),t.lineTo(s+e,o+n),a&&t.bezierCurveTo(s+c*e,o+n,s,o+n-c*i,s,o+n-i),t.lineTo(s,o+i),a&&t.bezierCurveTo(s,o+c*i,s+c*e,o,s+e,o),t.closePath(),this._renderPaintInOrder(t)},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']}}),s.Rect.ATTRIBUTE_NAMES=s.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),s.Rect.fromElement=function(t,e,i){if(!t)return e(null);i=i||{};var r=s.parseAttributes(t,s.Rect.ATTRIBUTE_NAMES);r.left=r.left||0,r.top=r.top||0,r.height=r.height||0,r.width=r.width||0;var n=new s.Rect(o(i?s.util.object.clone(i):{},r));n.visible=n.visible&&0\n']},commonRender:function(t){var e,i=this.points.length,r=this.pathOffset.x,n=this.pathOffset.y;if(!i||isNaN(this.points[i-1].y))return!1;t.beginPath(),t.moveTo(this.points[0].x-r,this.points[0].y-n);for(var s=0;s"},toObject:function(t){return n(this.callSuper("toObject",t),{path:this.path.map(function(t){return t.slice()})})},toDatalessObject:function(t){var e=this.toObject(["sourcePath"].concat(t));return e.sourcePath&&delete e.path,e},_toSVG:function(){return["\n"]},_getOffsetTransform:function(){var t=f.Object.NUM_FRACTION_DIGITS;return" translate("+e(-this.pathOffset.x,t)+", "+e(-this.pathOffset.y,t)+")"},toClipPathSVG:function(t){var e=this._getOffsetTransform();return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},toSVG:function(t){var e=this._getOffsetTransform();return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},complexity:function(){return this.path.length},_calcDimensions:function(){for(var t,e,i=[],r=[],n=0,s=0,o=0,a=0,c=0,h=this.path.length;c"},addWithUpdate:function(t){var e=!!this.group;return this._restoreObjectsState(),h.util.resetObjectTransform(this),t&&(e&&h.util.removeTransformFromObject(t,this.group.calcTransformMatrix()),this._objects.push(t),t.group=this,t._set("canvas",this.canvas)),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,e?this.group.addWithUpdate():this.setCoords(),this},removeWithUpdate:function(t){return this._restoreObjectsState(),h.util.resetObjectTransform(this),this.remove(t),this._calcBounds(),this._updateObjectsCoords(),this.setCoords(),this.dirty=!0,this},_onObjectAdded:function(t){this.dirty=!0,t.group=this,t._set("canvas",this.canvas)},_onObjectRemoved:function(t){this.dirty=!0,delete t.group},_set:function(t,e){var i=this._objects.length;if(this.useSetOnGroup)for(;i--;)this._objects[i].setOnGroup(t,e);if("canvas"===t)for(;i--;)this._objects[i]._set(t,e);h.Object.prototype._set.call(this,t,e)},toObject:function(r){var n=this.includeDefaultValues,t=this._objects.filter(function(t){return!t.excludeFromExport}).map(function(t){var e=t.includeDefaultValues;t.includeDefaultValues=n;var i=t.toObject(r);return t.includeDefaultValues=e,i}),e=h.Object.prototype.toObject.call(this,r);return e.objects=t,e},toDatalessObject:function(r){var t,e=this.sourcePath;if(e)t=e;else{var n=this.includeDefaultValues;t=this._objects.map(function(t){var e=t.includeDefaultValues;t.includeDefaultValues=n;var i=t.toDatalessObject(r);return t.includeDefaultValues=e,i})}var i=h.Object.prototype.toDatalessObject.call(this,r);return i.objects=t,i},render:function(t){this._transformDone=!0,this.callSuper("render",t),this._transformDone=!1},shouldCache:function(){var t=h.Object.prototype.shouldCache.call(this);if(t)for(var e=0,i=this._objects.length;e\n"],i=0,r=this._objects.length;i\n"),e},getSvgStyles:function(){var t=void 0!==this.opacity&&1!==this.opacity?"opacity: "+this.opacity+";":"",e=this.visible?"":" visibility: hidden;";return[t,this.getSvgFilter(),e].join("")},toClipPathSVG:function(t){for(var e=[],i=0,r=this._objects.length;i"},shouldCache:function(){return!1},isOnACache:function(){return!1},_renderControls:function(t,e,i){t.save(),t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,this.callSuper("_renderControls",t,e),void 0===(i=i||{}).hasControls&&(i.hasControls=!1),i.forActiveSelection=!0;for(var r=0,n=this._objects.length;r\n','\t\n',"\n"),o=' clip-path="url(#imageCrop_'+c+')" '}if(this.imageSmoothing||(a='" image-rendering="optimizeSpeed'),i.push("\t\n"),this.stroke||this.strokeDashArray){var h=this.fill;this.fill=null,t=["\t\n'],this.fill=h}return e="fill"!==this.paintFirst?e.concat(t,i):e.concat(i,t)},getSrc:function(t){var e=t?this._element:this._originalElement;return e?e.toDataURL?e.toDataURL():this.srcFromAttribute?e.getAttribute("src"):e.src:this.src||""},setSrc:function(t,i,r){return fabric.util.loadImage(t,function(t,e){this.setElement(t,r),this._setWidthHeight(),i&&i(this,e)},this,r&&r.crossOrigin),this},toString:function(){return'#'},applyResizeFilters:function(){var t=this.resizeFilter,e=this.minimumScaleTrigger,i=this.getTotalObjectScaling(),r=i.scaleX,n=i.scaleY,s=this._filteredEl||this._originalElement;if(this.group&&this.set("dirty",!0),!t||e=t;for(var a=["highp","mediump","lowp"],c=0;c<3;c++)if(void 0,i="precision "+a[c]+" float;\nvoid main(){}",r=(e=s).createShader(e.FRAGMENT_SHADER),e.shaderSource(r,i),e.compileShader(r),e.getShaderParameter(r,e.COMPILE_STATUS)){fabric.webGlPrecision=a[c];break}}return this.isSupported=o},(fabric.WebglFilterBackend=t).prototype={tileSize:2048,resources:{},setupGLContext:function(t,e){this.dispose(),this.createWebGLCanvas(t,e),this.aPosition=new Float32Array([0,0,0,1,1,0,1,1]),this.chooseFastestCopyGLTo2DMethod(t,e)},chooseFastestCopyGLTo2DMethod:function(t,e){var i,r=void 0!==window.performance;try{new ImageData(1,1),i=!0}catch(t){i=!1}var n="undefined"!=typeof ArrayBuffer,s="undefined"!=typeof Uint8ClampedArray;if(r&&i&&n&&s){var o=fabric.util.createCanvasElement(),a=new ArrayBuffer(t*e*4);if(fabric.forceGLPutImageData)return this.imageBuffer=a,void(this.copyGLTo2D=copyGLTo2DPutImageData);var c,h,l={imageBuffer:a,destinationWidth:t,destinationHeight:e,targetCanvas:o};o.width=t,o.height=e,c=window.performance.now(),copyGLTo2DDrawImage.call(l,this.gl,l),h=window.performance.now()-c,c=window.performance.now(),copyGLTo2DPutImageData.call(l,this.gl,l),window.performance.now()-c 0.0) {\n"+this.fragmentSource[t]+"}\n}"},retrieveShader:function(t){var e,i=this.type+"_"+this.mode;return t.programCache.hasOwnProperty(i)||(e=this.buildSource(this.mode),t.programCache[i]=this.createProgram(t.context,e)),t.programCache[i]},applyTo2d:function(t){var e,i,r,n,s,o,a,c=t.imageData.data,h=c.length,l=1-this.alpha;e=(a=new f.Color(this.color).getSource())[0]*this.alpha,i=a[1]*this.alpha,r=a[2]*this.alpha;for(var u=0;u'},_getCacheCanvasDimensions:function(){var t=this.callSuper("_getCacheCanvasDimensions"),e=this.fontSize;return t.width+=e*t.zoomX,t.height+=e*t.zoomY,t},_render:function(t){var e=this.path;e&&!e.isNotVisible()&&e._render(t),this._setTextStyles(t),this._renderTextLinesBackground(t),this._renderTextDecoration(t,"underline"),this._renderText(t),this._renderTextDecoration(t,"overline"),this._renderTextDecoration(t,"linethrough")},_renderText:function(t){"stroke"===this.paintFirst?(this._renderTextStroke(t),this._renderTextFill(t)):(this._renderTextFill(t),this._renderTextStroke(t))},_setTextStyles:function(t,e,i){if(t.textBaseline="alphabetical",this.path)switch(this.pathAlign){case"center":t.textBaseline="middle";break;case"ascender":t.textBaseline="top";break;case"descender":t.textBaseline="bottom"}t.font=this._getFontDeclaration(e,i)},calcTextWidth:function(){for(var t=this.getLineWidth(0),e=1,i=this._textLines.length;ethis.__selectionStartOnMouseDown?(this.selectionStart=this.__selectionStartOnMouseDown,this.selectionEnd=e):(this.selectionStart=e,this.selectionEnd=this.__selectionStartOnMouseDown),this.selectionStart===i&&this.selectionEnd===r||(this.restartCursorIfNeeded(),this._fireSelectionChanged(),this._updateTextarea(),this.renderCursorOrSelection()))}},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},fromStringToGraphemeSelection:function(t,e,i){var r=i.slice(0,t),n=fabric.util.string.graphemeSplit(r).length;if(t===e)return{selectionStart:n,selectionEnd:n};var s=i.slice(t,e);return{selectionStart:n,selectionEnd:n+fabric.util.string.graphemeSplit(s).length}},fromGraphemeToStringSelection:function(t,e,i){var r=i.slice(0,t).join("").length;return t===e?{selectionStart:r,selectionEnd:r}:{selectionStart:r,selectionEnd:r+i.slice(t,e).join("").length}},_updateTextarea:function(){if(this.cursorOffsetCache={},this.hiddenTextarea){if(!this.inCompositionMode){var t=this.fromGraphemeToStringSelection(this.selectionStart,this.selectionEnd,this._text);this.hiddenTextarea.selectionStart=t.selectionStart,this.hiddenTextarea.selectionEnd=t.selectionEnd}this.updateTextareaPosition()}},updateFromTextArea:function(){if(this.hiddenTextarea){this.cursorOffsetCache={},this.text=this.hiddenTextarea.value,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords());var t=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value);this.selectionEnd=this.selectionStart=t.selectionEnd,this.inCompositionMode||(this.selectionStart=t.selectionStart),this.updateTextareaPosition()}},updateTextareaPosition:function(){if(this.selectionStart===this.selectionEnd){var t=this._calcTextareaPosition();this.hiddenTextarea.style.left=t.left,this.hiddenTextarea.style.top=t.top}},_calcTextareaPosition:function(){if(!this.canvas)return{x:1,y:1};var t=this.inCompositionMode?this.compositionStart:this.selectionStart,e=this._getCursorBoundaries(t),i=this.get2DCursorLocation(t),r=i.lineIndex,n=i.charIndex,s=this.getValueOfPropertyAt(r,n,"fontSize")*this.lineHeight,o=e.leftOffset,a=this.calcTransformMatrix(),c={x:e.left+o,y:e.top+e.topOffset+s},h=this.canvas.getRetinaScaling(),l=this.canvas.upperCanvasEl,u=l.width/h,f=l.height/h,d=u-s,g=f-s,p=l.clientWidth/u,v=l.clientHeight/f;return c=fabric.util.transformPoint(c,a),(c=fabric.util.transformPoint(c,this.canvas.viewportTransform)).x*=p,c.y*=v,c.x<0&&(c.x=0),c.x>d&&(c.x=d),c.y<0&&(c.y=0),c.y>g&&(c.y=g),c.x+=this.canvas._offset.left,c.y+=this.canvas._offset.top,{left:c.x+"px",top:c.y+"px",fontSize:s+"px",charHeight:s}},_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}},_restoreEditingProps:function(){this._savedProps&&(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,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor))},exitEditing:function(){var t=this._textBeforeEdit!==this.text,e=this.hiddenTextarea;return this.selected=!1,this.isEditing=!1,this.selectionEnd=this.selectionStart,e&&(e.blur&&e.blur(),e.parentNode&&e.parentNode.removeChild(e)),this.hiddenTextarea=null,this.abortCursorAnimation(),this._restoreEditingProps(),this._currentCursorOpacity=0,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this.fire("editing:exited"),t&&this.fire("modified"),this.canvas&&(this.canvas.off("mouse:move",this.mouseMoveHandler),this.canvas.fire("text:editing:exited",{target:this}),t&&this.canvas.fire("object:modified",{target:this})),this},_removeExtraneousStyles:function(){for(var t in this.styles)this._textLines[t]||delete this.styles[t]},removeStyleFromTo:function(t,e){var i,r,n=this.get2DCursorLocation(t,!0),s=this.get2DCursorLocation(e,!0),o=n.lineIndex,a=n.charIndex,c=s.lineIndex,h=s.charIndex;if(o!==c){if(this.styles[o])for(i=a;it?this.selectionStart=t:this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd>t?this.selectionEnd=t:this.selectionEnd<0&&(this.selectionEnd=0)}})}(),fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+new Date,this.__lastLastClickTime=+new Date,this.__lastPointer={},this.on("mousedown",this.onMouseDown)},onMouseDown:function(t){if(this.canvas){this.__newClickTime=+new Date;var e=t.pointer;this.isTripleClick(e)&&(this.fire("tripleclick",t),this._stopEvent(t.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=e,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected}},isTripleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y},_stopEvent:function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation()},initCursorSelectionHandlers:function(){this.initMousedownHandler(),this.initMouseupHandler(),this.initClicks()},doubleClickHandler:function(t){this.isEditing&&this.selectWord(this.getSelectionStartFromPointer(t.e))},tripleClickHandler:function(t){this.isEditing&&this.selectLine(this.getSelectionStartFromPointer(t.e))},initClicks:function(){this.on("mousedblclick",this.doubleClickHandler),this.on("tripleclick",this.tripleClickHandler)},_mouseDownHandler:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.__isMousedown=!0,this.selected&&(this.inCompositionMode=!1,this.setCursorByClick(t.e)),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.selectionStart===this.selectionEnd&&this.abortCursorAnimation(),this.renderCursorOrSelection()))},_mouseDownHandlerBefore:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.selected=this===this.canvas._activeObject)},initMousedownHandler:function(){this.on("mousedown",this._mouseDownHandler),this.on("mousedown:before",this._mouseDownHandlerBefore)},initMouseupHandler:function(){this.on("mouseup",this.mouseUpHandler)},mouseUpHandler:function(t){if(this.__isMousedown=!1,!(!this.editable||this.group||t.transform&&t.transform.actionPerformed||t.e.button&&1!==t.e.button)){if(this.canvas){var e=this.canvas._activeObject;if(e&&e!==this)return}this.__lastSelected&&!this.__corner?(this.selected=!1,this.__lastSelected=!1,this.enterEditing(t.e),this.selectionStart===this.selectionEnd?this.initDelayedCursor(!0):this.renderCursorOrSelection()):this.selected=!0}},setCursorByClick:function(t){var e=this.getSelectionStartFromPointer(t),i=this.selectionStart,r=this.selectionEnd;t.shiftKey?this.setSelectionStartEndWithShift(i,r,e):(this.selectionStart=e,this.selectionEnd=e),this.isEditing&&(this._fireSelectionChanged(),this._updateTextarea())},getSelectionStartFromPointer:function(t){for(var e,i=this.getLocalPointer(t),r=0,n=0,s=0,o=0,a=0,c=0,h=this._textLines.length;cthis._text.length&&(a=this._text.length),a}}),fabric.util.object.extend(fabric.IText.prototype,{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 t=this._calcTextareaPosition();this.hiddenTextarea.style.cssText="position: absolute; top: "+t.top+"; left: "+t.left+"; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; padding-top: "+t.fontSize+";",this.hiddenTextareaContainer?this.hiddenTextareaContainer.appendChild(this.hiddenTextarea):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)),!this._clickHandlerInitialized&&this.canvas&&(fabric.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},keysMap:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown"},keysMapRtl:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorLeft",36:"moveCursorRight",37:"moveCursorRight",38:"moveCursorUp",39:"moveCursorLeft",40:"moveCursorDown"},ctrlKeysMapUp:{67:"copy",88:"cut"},ctrlKeysMapDown:{65:"selectAll"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(t){if(this.isEditing){var e="rtl"===this.direction?this.keysMapRtl:this.keysMap;if(t.keyCode in e)this[e[t.keyCode]](t);else{if(!(t.keyCode in this.ctrlKeysMapDown&&(t.ctrlKey||t.metaKey)))return;this[this.ctrlKeysMapDown[t.keyCode]](t)}t.stopImmediatePropagation(),t.preventDefault(),33<=t.keyCode&&t.keyCode<=40?(this.inCompositionMode=!1,this.clearContextTop(),this.renderCursorOrSelection()):this.canvas&&this.canvas.requestRenderAll()}},onKeyUp:function(t){!this.isEditing||this._copyDone||this.inCompositionMode?this._copyDone=!1:t.keyCode in this.ctrlKeysMapUp&&(t.ctrlKey||t.metaKey)&&(this[this.ctrlKeysMapUp[t.keyCode]](t),t.stopImmediatePropagation(),t.preventDefault(),this.canvas&&this.canvas.requestRenderAll())},onInput:function(t){var e=this.fromPaste;if(this.fromPaste=!1,t&&t.stopPropagation(),this.isEditing){var i,r,n,s,o,a=this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,c=this._text.length,h=a.length,l=h-c,u=this.selectionStart,f=this.selectionEnd,d=u!==f;if(""===this.hiddenTextarea.value)return this.styles={},this.updateFromTextArea(),this.fire("changed"),void(this.canvas&&(this.canvas.fire("text:changed",{target:this}),this.canvas.requestRenderAll()));var g=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value),p=u>g.selectionStart;d?(i=this._text.slice(u,f),l+=f-u):h=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorUpOrDown("Down",t)},moveCursorUp:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorUpOrDown("Up",t)},_moveCursorUpOrDown:function(t,e){var i=this["get"+t+"CursorOffset"](e,"right"===this._selectionDirection);e.shiftKey?this.moveCursorWithShift(i):this.moveCursorWithoutShift(i),0!==i&&(this.setSelectionInBoundaries(),this.abortCursorAnimation(),this._currentCursorOpacity=1,this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorWithShift:function(t){var e="left"===this._selectionDirection?this.selectionStart+t:this.selectionEnd+t;return this.setSelectionStartEndWithShift(this.selectionStart,this.selectionEnd,e),0!==t},moveCursorWithoutShift:function(t){return t<0?(this.selectionStart+=t,this.selectionEnd=this.selectionStart):(this.selectionEnd+=t,this.selectionStart=this.selectionEnd),0!==t},moveCursorLeft:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorLeftOrRight("Left",t)},_move:function(t,e,i){var r;if(t.altKey)r=this["findWordBoundary"+i](this[e]);else{if(!t.metaKey&&35!==t.keyCode&&36!==t.keyCode)return this[e]+="Left"===i?-1:1,!0;r=this["findLineBoundary"+i](this[e])}if(void 0!==r&&this[e]!==r)return this[e]=r,!0},_moveLeft:function(t,e){return this._move(t,e,"Left")},_moveRight:function(t,e){return this._move(t,e,"Right")},moveCursorLeftWithoutShift:function(t){var e=!0;return this._selectionDirection="left",this.selectionEnd===this.selectionStart&&0!==this.selectionStart&&(e=this._moveLeft(t,"selectionStart")),this.selectionEnd=this.selectionStart,e},moveCursorLeftWithShift:function(t){return"right"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveLeft(t,"selectionEnd"):0!==this.selectionStart?(this._selectionDirection="left",this._moveLeft(t,"selectionStart")):void 0},moveCursorRight:function(t){this.selectionStart>=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorLeftOrRight("Right",t)},_moveCursorLeftOrRight:function(t,e){var i="moveCursor"+t+"With";this._currentCursorOpacity=1,e.shiftKey?i+="Shift":i+="outShift",this[i](e)&&(this.abortCursorAnimation(),this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorRightWithShift:function(t){return"left"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveRight(t,"selectionStart"):this.selectionEnd!==this._text.length?(this._selectionDirection="right",this._moveRight(t,"selectionEnd")):void 0},moveCursorRightWithoutShift:function(t){var e=!0;return this._selectionDirection="right",this.selectionStart===this.selectionEnd?(e=this._moveRight(t,"selectionStart"),this.selectionEnd=this.selectionStart):this.selectionStart=this.selectionEnd,e},removeChars:function(t,e){void 0===e&&(e=t+1),this.removeStyleFromTo(t,e),this._text.splice(t,e-t),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()},insertChars:function(t,e,i,r){void 0===r&&(r=i),i",t.textSpans.join(""),"\n"]},_getSVGTextAndBg:function(t,e){var i,r=[],n=[],s=t;this._setSVGBg(n);for(var o=0,a=this._textLines.length;o",fabric.util.string.escapeXml(t),""].join("")},_setSVGTextLineText:function(t,e,i,r){var n,s,o,a,c,h=this.getHeightOfLine(e),l=-1!==this.textAlign.indexOf("justify"),u="",f=0,d=this._textLines[e];r+=h*(1-this._fontSizeFraction)/this.lineHeight;for(var g=0,p=d.length-1;g<=p;g++)c=g===p||this.charSpacing,u+=d[g],o=this.__charBounds[e][g],0===f?(i+=o.kernedWidth-o.width,f+=o.width):f+=o.kernedWidth,l&&!c&&this._reSpaceAndTab.test(d[g])&&(c=!0),c||(n=n||this.getCompleteStyleDeclaration(e,g),s=this.getCompleteStyleDeclaration(e,g+1),c=fabric.util.hasStyleChanged(n,s,!0)),c&&(a=this._getStyleDeclaration(e,g)||{},t.push(this._createTextCharSpan(u,a,i,r)),u="",n=s,i+=f,f=0)},_pushTextBgRect:function(t,e,i,r,n,s){var o=fabric.Object.NUM_FRACTION_DIGITS;t.push("\t\t\n')},_setSVGTextLineBg:function(t,e,i,r){for(var n,s,o=this._textLines[e],a=this.getHeightOfLine(e)/this.lineHeight,c=0,h=0,l=this.getValueOfPropertyAt(e,0,"textBackgroundColor"),u=0,f=o.length;uthis.width&&this._set("width",this.dynamicMinWidth),-1!==this.textAlign.indexOf("justify")&&this.enlargeSpaces(),this.height=this.calcTextHeight(),this.saveState({propertySet:"_dimensionAffectingProps"}))},_generateStyleMap:function(t){for(var e=0,i=0,r=0,n={},s=0;sthis.dynamicMinWidth&&(this.dynamicMinWidth=g-v+r),o},isEndOfWrapping:function(t){return!this._styleMap[t+1]||this._styleMap[t+1].line!==this._styleMap[t].line},missingNewlineOffset:function(t){return this.splitByGrapheme?this.isEndOfWrapping(t)?1:0:1},_splitTextIntoLines:function(t){for(var e=b.Text.prototype._splitTextIntoLines.call(this,t),i=this._wrapText(e.lines,this.width),r=new Array(i.length),n=0;n 1) { $("#toolbox").attr("data-type", "group"); $("#toolbox-heading").text(gettext("Group of objects")); - var g = editor.fabric.getActiveGroup(); - } else if (editor.fabric.getActiveObject()) { - var o = editor.fabric.getActiveObject(); + } else if (selected.length == 1) { + var o = selected[0]; $("#toolbox").attr("data-type", o.type); if (o.type === "textarea" || o.type === "text") { $("#toolbox-heading").text(gettext("Text object")); @@ -716,13 +707,13 @@ var editor = { var rect = new fabric.Poweredby({ left: 100, top: 100, - width: 205, - height: 126, + height: 629, + width: 1024, lockRotation: true, - lockUniScaling: true, content: content }); - rect.setControlsVisibility({'mtr': false}); + rect.scaleToHeight(126); + rect.setControlsVisibility({'mtr': false, 'mb': false, 'mt': false, 'mr': false, 'ml': false}); editor.fabric.add(rect); editor._create_savepoint(); return rect; @@ -751,13 +742,12 @@ var editor = { width: 100, height: 100, lockRotation: true, - lockUniScaling: true, fill: '#666', content: $(this).attr("data-content"), text: '', nowhitespace: true, }); - rect.setControlsVisibility({'mtr': false}); + rect.setControlsVisibility({'mtr': false, 'mb': false, 'mt': false, 'mr': false, 'ml': false}); editor.fabric.add(rect); editor._create_savepoint(); return rect; @@ -765,18 +755,17 @@ var editor = { _cut: function () { editor._history_modification_in_progress = true; - var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup(); - if (thing.type === "group") { + var thing = editor.fabric.getActiveObject(); + if (thing.type === "activeSelection") { editor.clipboard = editor.dump(thing._objects); thing.forEachObject(function (o) { - o.remove(); + editor.fabric.remove(o); }); - thing.remove(); + editor.fabric.remove(thing); } else { editor.clipboard = editor.dump([thing]); - thing.remove(); + editor.fabric.remove(thing); } - editor.fabric.discardActiveGroup(); editor.fabric.discardActiveObject(); editor._history_modification_in_progress = false; editor._create_savepoint(); @@ -784,8 +773,8 @@ var editor = { _copy: function () { editor._history_modification_in_progress = true; - var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup(); - if (thing.type === "group") { + var thing = editor.fabric.getActiveObject(); + if (thing.type === "activeSelection") { editor.clipboard = editor.dump(thing._objects); } else { editor.clipboard = editor.dump([thing]); @@ -805,16 +794,9 @@ var editor = { objs.push(editor._add_from_data(editor.clipboard[i])); } editor.fabric.discardActiveObject(); - editor.fabric.discardActiveGroup(); if (editor.clipboard.length > 1) { - var group = new fabric.Group(objs, { - originX: 'left', - originY: 'top', - left: 100, - top: 100, - }); - group.setCoords(); - editor.fabric.setActiveGroup(group); + var selection = new fabric.ActiveSelection(objs, {canvas: editor.fabric}); + editor.fabric.setActiveObject(selection); } else { editor.fabric.setActiveObject(objs[0]); } @@ -823,15 +805,15 @@ var editor = { }, _delete: function () { - var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup(); - if (thing.type === "group") { + var thing = editor.fabric.getActiveObject(); + if (thing.type === "activeSelection") { thing.forEachObject(function (o) { - o.remove(); + editor.fabric.remove(o); }); - thing.remove(); - editor.fabric.discardActiveGroup(); + editor.fabric.remove(thing); + editor.fabric.discardActiveObject(); } else { - thing.remove(); + editor.fabric.remove(thing); editor.fabric.discardActiveObject(); } editor._create_savepoint(); @@ -839,7 +821,7 @@ var editor = { _on_keydown: function (e) { var step = e.shiftKey ? editor._mm2px(10) : editor._mm2px(1); - var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup(); + var thing = editor.fabric.getActiveObject(); if ($("#source-container").is(':visible')) { return true; } @@ -921,12 +903,8 @@ var editor = { }, _selectAll: function () { - var group = new fabric.Group(editor.fabric.getObjects(), { - originX: 'center', - originY: 'center', - }); - group.setCoords(); - editor.fabric.setActiveGroup(group); + var selection = new fabric.ActiveSelection(editor.fabric._objects, {canvas: editor.fabric}); + editor.fabric.setActiveObject(selection); }, _undo: function undo() {