diff --git a/src/pretix/static/fabric/fabric.js b/src/pretix/static/fabric/fabric.js
index 0e5f8a7354..faee7fc675 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;
}
//