diff --git a/.gitignore b/.gitignore
index b4fee0d2bce..a69b8890a22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ change-output.md
before_commit
/coverage/
.idea/
-/dist/fabric.require.js
-/dist/fabric.min.js.gz
+/dist
+!/dist/fabric.js
+!/dist/fabric.min.js
/scripts/cli_cache.json
diff --git a/dist/fabric.js b/dist/fabric.js
index 9b7bf655d2a..846e4b54cdb 100644
--- a/dist/fabric.js
+++ b/dist/fabric.js
@@ -1,4 +1,4 @@
-/* build: `node build.js modules=ALL exclude=gestures,accessors,erasing requirejs minifier=uglifyjs` */
+/* build: `node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs` */
/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */
var fabric = fabric || { version: '5.1.0' };
@@ -236,8 +236,7 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
* @alias on
* @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler})
* @param {Function} handler Function that receives a notification when an event of the specified type occurs
- * @return {Self} thisArg
- * @chainable
+ * @return {Function} disposer
*/
function on(eventName, handler) {
if (!this.__eventListeners) {
@@ -255,7 +254,7 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
}
this.__eventListeners[eventName].push(handler);
}
- return this;
+ return off.bind(this, eventName, handler);
}
function _once(eventName, handler) {
@@ -264,19 +263,30 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
this.off(eventName, _handler);
}.bind(this);
this.on(eventName, _handler);
+ return _handler;
}
+ /**
+ * Observes specified event **once**
+ * @memberOf fabric.Observable
+ * @alias once
+ * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler})
+ * @param {Function} handler Function that receives a notification when an event of the specified type occurs
+ * @return {Function} disposer
+ */
function once(eventName, handler) {
// one object with key/value pairs was passed
if (arguments.length === 1) {
+ var handlers = {};
for (var prop in eventName) {
- _once.call(this, prop, eventName[prop]);
+ handlers[prop] = _once.call(this, prop, eventName[prop]);
}
+ return off.bind(this, handlers);
}
else {
- _once.call(this, eventName, handler);
+ var _handler = _once.call(this, eventName, handler);
+ return off.bind(this, eventName, _handler);
}
- return this;
}
/**
@@ -286,12 +296,10 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
* @alias off
* @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler})
* @param {Function} handler Function to be deleted from EventListeners
- * @return {Self} thisArg
- * @chainable
*/
function off(eventName, handler) {
if (!this.__eventListeners) {
- return this;
+ return;
}
// remove all key/value pairs (event name -> event handler)
@@ -301,7 +309,7 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
}
}
// one object with key/value pairs was passed
- else if (arguments.length === 1 && typeof arguments[0] === 'object') {
+ else if (typeof eventName === 'object' && typeof handler === 'undefined') {
for (var prop in eventName) {
_removeEventListener.call(this, prop, eventName[prop]);
}
@@ -309,7 +317,6 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
else {
_removeEventListener.call(this, eventName, handler);
}
- return this;
}
/**
@@ -317,17 +324,15 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
* @memberOf fabric.Observable
* @param {String} eventName Event name to fire
* @param {Object} [options] Options object
- * @return {Self} thisArg
- * @chainable
*/
function fire(eventName, options) {
if (!this.__eventListeners) {
- return this;
+ return;
}
var listenersForEvent = this.__eventListeners[eventName];
if (!listenersForEvent) {
- return this;
+ return;
}
for (var i = 0, len = listenersForEvent.length; i < len; i++) {
@@ -336,7 +341,6 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
this.__eventListeners[eventName] = listenersForEvent.filter(function(value) {
return value !== false;
});
- return this;
}
/**
@@ -358,79 +362,70 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
*/
fabric.Collection = {
+ /**
+ * @type {fabric.Object[]}
+ */
_objects: [],
/**
* Adds objects to collection, Canvas or Group, then renders canvas
* (if `renderOnAddRemove` is not `false`).
- * in case of Group no changes to bounding box are made.
* Objects should be instances of (or inherit from) fabric.Object
- * Use of this function is highly discouraged for groups.
- * you can add a bunch of objects with the add method but then you NEED
- * to run a addWithUpdate call for the Group class or position/bbox will be wrong.
- * @param {...fabric.Object} object Zero or more fabric instances
- * @return {Self} thisArg
- * @chainable
+ * @private
+ * @param {fabric.Object[]} objects to add
+ * @param {(object:fabric.Object) => any} [callback]
+ * @returns {number} new array length
*/
- add: function () {
- this._objects.push.apply(this._objects, arguments);
- if (this._onObjectAdded) {
- for (var i = 0, length = arguments.length; i < length; i++) {
- this._onObjectAdded(arguments[i]);
+ add: function (objects, callback) {
+ var size = this._objects.push.apply(this._objects, objects);
+ if (callback) {
+ for (var i = 0; i < objects.length; i++) {
+ callback.call(this, objects[i]);
}
}
- this.renderOnAddRemove && this.requestRenderAll();
- return this;
+ return size;
},
/**
* 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
+ * @private
+ * @param {fabric.Object|fabric.Object[]} objects Object(s) to insert
* @param {Number} index Index to insert object at
- * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs
- * @return {Self} thisArg
- * @chainable
+ * @param {(object:fabric.Object) => any} [callback]
+ * @returns {number} new array length
*/
- insertAt: function (object, index, nonSplicing) {
- var objects = this._objects;
- if (nonSplicing) {
- objects[index] = object;
- }
- else {
- objects.splice(index, 0, object);
+ insertAt: function (objects, index, callback) {
+ var args = [index, 0].concat(objects);
+ this._objects.splice.apply(this._objects, args);
+ if (callback) {
+ for (var i = 2; i < args.length; i++) {
+ callback.call(this, args[i]);
+ }
}
- this._onObjectAdded && this._onObjectAdded(object);
- this.renderOnAddRemove && this.requestRenderAll();
- return this;
+ return this._objects.length;
},
/**
* Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`)
- * @param {...fabric.Object} object Zero or more fabric instances
- * @return {Self} thisArg
- * @chainable
- */
- remove: function() {
- var objects = this._objects,
- index, somethingRemoved = false;
-
- for (var i = 0, length = arguments.length; i < length; i++) {
- index = objects.indexOf(arguments[i]);
-
+ * @private
+ * @param {fabric.Object[]} objectsToRemove objects to remove
+ * @param {(object:fabric.Object) => any} [callback] function to call for each object removed
+ * @returns {fabric.Object[]} removed objects
+ */
+ remove: function(objectsToRemove, callback) {
+ var objects = this._objects, removed = [];
+ for (var i = 0, object, index; i < objectsToRemove.length; i++) {
+ object = objectsToRemove[i];
+ index = objects.indexOf(object);
// only call onObjectRemoved if an object was actually removed
if (index !== -1) {
- somethingRemoved = true;
objects.splice(index, 1);
- this._onObjectRemoved && this._onObjectRemoved(arguments[i]);
+ removed.push(object);
+ callback && callback.call(this, object);
}
}
-
- this.renderOnAddRemove && somethingRemoved && this.requestRenderAll();
- return this;
+ return removed;
},
/**
@@ -447,7 +442,7 @@ fabric.Collection = {
*/
forEachObject: function(callback, context) {
var objects = this.getObjects();
- for (var i = 0, len = objects.length; i < len; i++) {
+ for (var i = 0; i < objects.length; i++) {
callback.call(context, objects[i], i, objects);
}
return this;
@@ -455,17 +450,16 @@ 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
+ * @param {...String} [types] When specified, only objects of these types are returned
* @return {Array}
*/
- getObjects: function(type) {
- if (typeof type === 'undefined') {
+ getObjects: function() {
+ if (arguments.length === 0) {
return this._objects.concat();
}
- return this._objects.filter(function(o) {
- return o.type === type;
+ var types = Array.from(arguments);
+ return this._objects.filter(function (o) {
+ return types.indexOf(o.type) > -1;
});
},
@@ -495,7 +489,9 @@ fabric.Collection = {
},
/**
- * Returns true if collection contains an object
+ * Returns true if collection contains an object.\
+ * **Prefer using {@link `fabric.Object#isDescendantOf`} for performance reasons**
+ * instead of a.contains(b) use b.isDescendantOf(a)
* @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
@@ -540,32 +536,6 @@ fabric.CommonMethods = {
}
},
- /**
- * @private
- * @param {Object} [filler] Options object
- * @param {String} [property] property to set the Gradient to
- */
- _initGradient: function(filler, property) {
- if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) {
- this.set(property, new fabric.Gradient(filler));
- }
- },
-
- /**
- * @private
- * @param {Object} [filler] Options object
- * @param {String} [property] property to set the Pattern to
- * @param {Function} [callback] callback to invoke after pattern load
- */
- _initPattern: function(filler, property, callback) {
- if (filler && filler.source && !(filler instanceof fabric.Pattern)) {
- this.set(property, new fabric.Pattern(filler, callback));
- }
- else {
- callback && callback();
- }
- },
-
/**
* @private
*/
@@ -629,6 +599,10 @@ fabric.CommonMethods = {
PiBy180 = Math.PI / 180,
PiBy2 = Math.PI / 2;
+ /**
+ * @typedef {[number,number,number,number,number,number]} Matrix
+ */
+
/**
* @namespace fabric.util
*/
@@ -740,7 +714,7 @@ fabric.CommonMethods = {
rotatePoint: function(point, origin, 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);
+ return v.addEquals(origin);
},
/**
@@ -749,17 +723,14 @@ fabric.CommonMethods = {
* @memberOf fabric.util
* @param {Object} vector The vector to rotate (x and y)
* @param {Number} radians The radians of the angle for the rotation
- * @return {Object} The new rotated point
+ * @return {fabric.Point} The new rotated point
*/
rotateVector: function(vector, radians) {
var sin = fabric.util.sin(radians),
cos = fabric.util.cos(radians),
rx = vector.x * cos - vector.y * sin,
ry = vector.x * sin + vector.y * cos;
- return {
- x: rx,
- y: ry
- };
+ return new fabric.Point(rx, ry);
},
/**
@@ -798,7 +769,7 @@ fabric.CommonMethods = {
* @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));
+ return new fabric.Point(v.x, v.y).scalarMultiply(1 / Math.hypot(v.x, v.y));
},
/**
@@ -913,8 +884,70 @@ fabric.CommonMethods = {
);
},
+ /**
+ * Sends a point from the source coordinate plane to the destination coordinate plane.\
+ * From the canvas/viewer's perspective the point remains unchanged.
+ *
+ * @example
Send point from canvas plane to group plane
+ * var obj = new fabric.Rect({ left: 20, top: 20, width: 60, height: 60, strokeWidth: 0 });
+ * var group = new fabric.Group([obj], { strokeWidth: 0 });
+ * var sentPoint1 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), null, group.calcTransformMatrix());
+ * var sentPoint2 = fabric.util.sendPointToPlane(new fabric.Point(50, 50), fabric.iMatrix, group.calcTransformMatrix());
+ * console.log(sentPoint1, sentPoint2) // both points print (0,0) which is the center of group
+ *
+ * @static
+ * @memberOf fabric.util
+ * @see {fabric.util.transformPointRelativeToCanvas} for transforming relative to canvas
+ * @param {fabric.Point} point
+ * @param {Matrix} [from] plane matrix containing object. Passing `null` is equivalent to passing the identity matrix, which means `point` exists in the canvas coordinate plane.
+ * @param {Matrix} [to] destination plane matrix to contain object. Passing `null` means `point` should be sent to the canvas coordinate plane.
+ * @returns {fabric.Point} transformed point
+ */
+ sendPointToPlane: function (point, from, to) {
+ // we are actually looking for the transformation from the destination plane to the source plane (which is a linear mapping)
+ // the object will exist on the destination plane and we want it to seem unchanged by it so we reverse the destination matrix (to) and then apply the source matrix (from)
+ var inv = fabric.util.invertTransform(to || fabric.iMatrix);
+ var t = fabric.util.multiplyTransformMatrices(inv, from || fabric.iMatrix);
+ return fabric.util.transformPoint(point, t);
+ },
+
+ /**
+ * Transform point relative to canvas.
+ * From the viewport/viewer's perspective the point remains unchanged.
+ *
+ * `child` relation means `point` exists in the coordinate plane created by `canvas`.
+ * In other words point is measured acoording to canvas' top left corner
+ * meaning that if `point` is equal to (0,0) it is positioned at canvas' top left corner.
+ *
+ * `sibling` relation means `point` exists in the same coordinate plane as canvas.
+ * In other words they both relate to the same (0,0) and agree on every point, which is how an event relates to canvas.
+ *
+ * @static
+ * @memberOf fabric.util
+ * @param {fabric.Point} point
+ * @param {fabric.StaticCanvas} canvas
+ * @param {'sibling'|'child'} relationBefore current relation of point to canvas
+ * @param {'sibling'|'child'} relationAfter desired relation of point to canvas
+ * @returns {fabric.Point} transformed point
+ */
+ transformPointRelativeToCanvas: function (point, canvas, relationBefore, relationAfter) {
+ if (relationBefore !== 'child' && relationBefore !== 'sibling') {
+ throw new Error('fabric.js: received bad argument ' + relationBefore);
+ }
+ if (relationAfter !== 'child' && relationAfter !== 'sibling') {
+ throw new Error('fabric.js: received bad argument ' + relationAfter);
+ }
+ if (relationBefore === relationAfter) {
+ return point;
+ }
+ var t = canvas.viewportTransform;
+ return fabric.util.transformPoint(point, relationAfter === 'child' ? fabric.util.invertTransform(t) : t);
+ },
+
/**
* Returns coordinates of points's bounding rectangle (left, top, width, height)
+ * @static
+ * @memberOf fabric.util
* @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
@@ -1080,185 +1113,84 @@ fabric.CommonMethods = {
},
/**
- * Loads image element from given url and passes it to a callback
+ * Loads image element from given url and resolve it, or catch.
* @memberOf fabric.util
* @param {String} url URL representing an image
- * @param {Function} callback Callback; invoked with loaded image
- * @param {*} [context] Context to invoke callback in
- * @param {Object} [crossOrigin] crossOrigin value to set image element to
- */
- loadImage: function(url, callback, context, crossOrigin) {
- if (!url) {
- callback && callback.call(context, url);
- return;
- }
-
- var img = fabric.util.createImage();
-
- /** @ignore */
- var onLoadCallback = function () {
- callback && callback.call(context, img, false);
- img = img.onload = img.onerror = null;
- };
-
- img.onload = onLoadCallback;
- /** @ignore */
- img.onerror = function() {
- fabric.log('Error loading ' + img.src);
- callback && callback.call(context, null, true);
- img = img.onload = img.onerror = null;
- };
-
- // data-urls appear to be buggy with crossOrigin
- // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767
- // see https://code.google.com/p/chromium/issues/detail?id=315152
- // https://bugzilla.mozilla.org/show_bug.cgi?id=935069
- // 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;
- };
+ * @param {Object} [options] image loading options
+ * @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous
+ * @param {Promise} img the loaded image.
+ */
+ loadImage: function(url, options) {
+ return new Promise(function(resolve, reject) {
+ var img = fabric.util.createImage();
+ var done = function() {
+ img.onload = img.onerror = null;
+ resolve(img);
+ };
+ if (!url) {
+ done();
+ }
+ else {
+ img.onload = done;
+ img.onerror = function () {
+ reject(new Error('Error loading ' + img.src));
+ };
+ options && options.crossOrigin && (img.crossOrigin = options.crossOrigin);
+ img.src = url;
+ }
+ });
},
/**
* Creates corresponding fabric instances from their object representations
* @static
* @memberOf fabric.util
- * @param {Array} objects Objects to enliven
- * @param {Function} callback Callback to invoke when all objects are created
+ * @param {Object[]} objects Objects to enliven
* @param {String} namespace Namespace to get klass "Class" object from
* @param {Function} reviver Method for further parsing of object elements,
* called after each fabric object created.
*/
- enlivenObjects: function(objects, callback, namespace, reviver) {
- objects = objects || [];
-
- var enlivenedObjects = [],
- numLoadedObjects = 0,
- numTotalObjects = objects.length;
-
- function onLoaded() {
- if (++numLoadedObjects === numTotalObjects) {
- callback && callback(enlivenedObjects.filter(function(obj) {
- // filter out undefined objects (objects that gave error)
- return obj;
- }));
- }
- }
-
- if (!numTotalObjects) {
- callback && callback(enlivenedObjects);
- return;
- }
-
- objects.forEach(function (o, index) {
- // if sparse array
- if (!o || !o.type) {
- onLoaded();
- return;
- }
- var klass = fabric.util.getKlass(o.type, namespace);
- klass.fromObject(o, function (obj, error) {
- error || (enlivenedObjects[index] = obj);
- reviver && reviver(o, obj, error);
- onLoaded();
+ enlivenObjects: function(objects, namespace, reviver) {
+ return Promise.all(objects.map(function(obj) {
+ var klass = fabric.util.getKlass(obj.type, namespace);
+ return klass.fromObject(obj).then(function(fabricInstance) {
+ reviver && reviver(obj, fabricInstance);
+ return fabricInstance;
});
- });
+ }));
},
/**
* 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);
- });
- },
-
- /**
- * Create and wait for loading of patterns
- * @static
- * @memberOf fabric.util
- * @param {Array} patterns Objects to enliven
- * @param {Function} callback Callback to invoke when all objects are created
- * called after each fabric object created.
+ * @param {Object} object with properties to enlive ( fill, stroke, clipPath, path )
+ * @returns {Promise