fabric.js/dist/all.require.js

15857 lines
438 KiB
JavaScript

/* build: `node build.js modules=ALL exclude=node,json,gestures,cufon no-strict no-svg-export` */
/*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */
var fabric = fabric || { version: "1.3.0" };
if (typeof document !== 'undefined' && typeof window !== 'undefined') {
fabric.document = document;
fabric.window = window;
}
else {
// assume we're running under node.js when document/window are not present
fabric.document = require("jsdom").jsdom("<!DOCTYPE html><html><head></head><body></body></html>");
fabric.window = fabric.document.createWindow();
}
/**
* True when in environment that supports touch events
* @type boolean
*/
fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement;
/**
* True when in environment that's probably Node.js
* @type boolean
*/
fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined';
/**
* Wrapper around `console.log` (when available)
* @param {Any} values Values to log
*/
fabric.log = function() { };
/**
* Wrapper around `console.warn` (when available)
* @param {Any} Values to log as a warning
*/
fabric.warn = function() { };
if (typeof console !== 'undefined') {
if (typeof console.log !== 'undefined' && console.log.apply) {
fabric.log = function() {
return console.log.apply(console, arguments);
};
}
if (typeof console.warn !== 'undefined' && console.warn.apply) {
fabric.warn = function() {
return console.warn.apply(console, arguments);
};
}
}
(function(){
/**
* @private
* @param {String} eventName
* @param {Function} handler
*/
function _removeEventListener(eventName, handler) {
if (!this.__eventListeners[eventName]) return;
if (handler) {
fabric.util.removeFromArray(this.__eventListeners[eventName], handler);
}
else {
this.__eventListeners[eventName].length = 0;
}
}
/**
* Observes specified event
* @deprecated `observe` deprecated since 0.8.34 (use `on` instead)
* @memberOf fabric.Observable
* @alias on
* @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler})
* @param {Function} handler Function that receives a notification when an event of the specified type occurs
* @return {Self} thisArg
* @chainable
*/
function observe(eventName, handler) {
if (!this.__eventListeners) {
this.__eventListeners = { };
}
// one object with key/value pairs was passed
if (arguments.length === 1) {
for (var prop in eventName) {
this.on(prop, eventName[prop]);
}
}
else {
if (!this.__eventListeners[eventName]) {
this.__eventListeners[eventName] = [ ];
}
this.__eventListeners[eventName].push(handler);
}
return this;
}
/**
* Stops event observing for a particular event handler
* @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead)
* @memberOf fabric.Observable
* @alias off
* @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler})
* @param {Function} handler Function to be deleted from EventListeners
* @return {Self} thisArg
* @chainable
*/
function stopObserving(eventName, handler) {
if (!this.__eventListeners) return;
// one object with key/value pairs was passed
if (arguments.length === 1 && typeof arguments[0] === 'object') {
for (var prop in eventName) {
_removeEventListener.call(this, prop, eventName[prop]);
}
}
else {
_removeEventListener.call(this, eventName, handler);
}
return this;
}
/**
* Fires event with an optional options object
* @deprecated `fire` deprecated since 1.0.7 (use `trigger` instead)
* @memberOf fabric.Observable
* @alias trigger
* @param {String} eventName Event name to fire
* @param {Object} [options] Options object
* @return {Self} thisArg
* @chainable
*/
function fire(eventName, options) {
if (!this.__eventListeners) return;
var listenersForEvent = this.__eventListeners[eventName];
if (!listenersForEvent) return;
for (var i = 0, len = listenersForEvent.length; i < len; i++) {
// avoiding try/catch for perf. reasons
listenersForEvent[i].call(this, options || { });
}
return this;
}
/**
* @namespace fabric.Observable
*/
fabric.Observable = {
observe: observe,
stopObserving: stopObserving,
fire: fire,
on: observe,
off: stopObserving,
trigger: fire
};
})();
/**
* @namespace fabric.Collection
*/
fabric.Collection = {
/**
* Adds objects to collection, then renders canvas (if `renderOnAddRemove` is not `false`)
* Objects should be instances of (or inherit from) fabric.Object
* @param [...] Zero or more fabric instances
* @return {Self} thisArg
*/
add: function () {
this._objects.push.apply(this._objects, arguments);
for (var i = arguments.length; i--; ) {
this._onObjectAdded(arguments[i]);
}
this.renderOnAddRemove && this.renderAll();
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
* @param {Object} object Object to insert
* @param {Number} index Index to insert object at
* @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs
* @return {Self} thisArg
*/
insertAt: function (object, index, nonSplicing) {
var objects = this.getObjects();
if (nonSplicing) {
objects[index] = object;
}
else {
objects.splice(index, 0, object);
}
this._onObjectAdded(object);
this.renderOnAddRemove && this.renderAll();
return this;
},
/**
* Removes an object from a collection, then renders canvas (if `renderOnAddRemove` is not `false`)
* @param {Object} object Object to remove
* @return {Self} thisArg
*/
remove: function(object) {
var objects = this.getObjects(),
index = objects.indexOf(object);
// only call onObjectRemoved if an object was actually removed
if (index !== -1) {
objects.splice(index, 1);
this._onObjectRemoved(object);
}
this.renderOnAddRemove && this.renderAll();
return object;
},
/**
* Executes given function for each object in this group
* @param {Function} callback
* Callback invoked with current object as first argument,
* index - as second and an array of all objects - as third.
* Iteration happens in reverse order (for performance reasons).
* Callback is invoked in a context of Global Object (e.g. `window`)
* when no `context` argument is given
*
* @param {Object} context Context (aka thisObject)
* @return {Self} thisArg
*/
forEachObject: function(callback, context) {
var objects = this.getObjects(),
i = objects.length;
while (i--) {
callback.call(context, objects[i], i, objects);
}
return this;
},
/**
* Returns object at specified index
* @param {Number} index
* @return {Self} thisArg
*/
item: function (index) {
return this.getObjects()[index];
},
/**
* Returns true if collection contains no objects
* @return {Boolean} true if collection is empty
*/
isEmpty: function () {
return this.getObjects().length === 0;
},
/**
* Returns a size of a collection (i.e: length of an array containing its objects)
* @return {Number} Collection size
*/
size: function() {
return this.getObjects().length;
},
/**
* Returns true if collection contains an object
* @param {Object} object Object to check against
* @return {Boolean} `true` if collection contains an object
*/
contains: function(object) {
return this.getObjects().indexOf(object) > -1;
},
/**
* Returns number representation of a collection complexity
* @return {Number} complexity
*/
complexity: function () {
return this.getObjects().reduce(function (memo, current) {
memo += current.complexity ? current.complexity() : 0;
return memo;
}, 0);
},
/**
* Makes all of the collection objects grayscale (i.e. calling `toGrayscale` on them)
* @return {Self} thisArg
*/
toGrayscale: function() {
return this.forEachObject(function(obj) {
obj.toGrayscale();
});
}
};
(function() {
var sqrt = Math.sqrt,
atan2 = Math.atan2;
/**
* @namespace fabric.util
*/
fabric.util = { };
/**
* Removes value from an array.
* Presence of value (and its position in an array) is determined via `Array.prototype.indexOf`
* @static
* @memberOf fabric.util
* @param {Array} array
* @param {Any} value
* @return {Array} original array
*/
function removeFromArray(array, value) {
var idx = array.indexOf(value);
if (idx !== -1) {
array.splice(idx, 1);
}
return array;
}
/**
* Returns random number between 2 specified ones.
* @static
* @memberOf fabric.util
* @param {Number} min lower limit
* @param {Number} max upper limit
* @return {Number} random value (between min and max)
*/
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var PiBy180 = Math.PI / 180;
/**
* Transforms degrees to radians.
* @static
* @memberOf fabric.util
* @param {Number} degrees value in degrees
* @return {Number} value in radians
*/
function degreesToRadians(degrees) {
return degrees * PiBy180;
}
/**
* Transforms radians to degrees.
* @static
* @memberOf fabric.util
* @param {Number} radians value in radians
* @return {Number} value in degrees
*/
function radiansToDegrees(radians) {
return radians / PiBy180;
}
/**
* Rotates `point` around `origin` with `radians`
* @static
* @memberOf fabric.util
* @param {fabric.Point} The point to rotate
* @param {fabric.Point} The origin of the rotation
* @param {Number} The radians of the angle for the rotation
* @return {fabric.Point} The new rotated point
*/
function rotatePoint(point, origin, radians) {
var sin = Math.sin(radians),
cos = Math.cos(radians);
point.subtractEquals(origin);
var rx = point.x * cos - point.y * sin;
var ry = point.x * sin + point.y * cos;
return new fabric.Point(rx, ry).addEquals(origin);
}
/**
* A wrapper around Number#toFixed, which contrary to native method returns number, not string.
* @static
* @memberOf fabric.util
* @param {Number | String} number number to operate on
* @param {Number} fractionDigits number of fraction digits to "leave"
* @return {Number}
*/
function toFixed(number, fractionDigits) {
return parseFloat(Number(number).toFixed(fractionDigits));
}
/**
* Function which always returns `false`.
* @static
* @memberOf fabric.util
* @return {Boolean}
*/
function falseFunction() {
return false;
}
/**
* Returns klass "Class" object of given namespace
* @memberOf fabric.util
* @param {String} type Type of object (eg. 'circle')
* @param {String} namespace Namespace to get klass "Class" object from
* @return {Object} klass "Class"
*/
function getKlass(type, namespace) {
return resolveNamespace(namespace)[fabric.util.string.camelize(fabric.util.string.capitalize(type))];
}
/**
* Returns object of given namespace
* @memberOf fabric.util
* @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric'
* @return {Object} Object for given namespace (default fabric)
*/
function resolveNamespace(namespace) {
if (!namespace) return fabric;
var parts = namespace.split('.'),
len = parts.length,
obj = fabric.window;
for (var i = 0; i < len; ++i) {
obj = obj[parts[i]];
}
return obj;
}
/**
* Loads image element from given url and passes it to a callback
* @memberOf fabric.util
* @param {String} url URL representing an image
* @param {Function} callback Callback; invoked with loaded image
* @param {Any} context optional Context to invoke callback in
*/
function loadImage(url, callback, context) {
if (url) {
var img = fabric.util.createImage();
/** @ignore */
img.onload = function () {
callback && callback.call(context, img);
img = img.onload = null;
};
img.src = url;
}
else {
callback && callback.call(context, 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 {Function} [reviver] Method for further parsing of object elements, called after each fabric object created.
*/
function enlivenObjects(objects, callback, namespace, reviver) {
function onLoaded() {
if (++numLoadedObjects === numTotalObjects) {
if (callback) {
callback(enlivenedObjects);
}
}
}
var enlivenedObjects = [ ],
numLoadedObjects = 0,
numTotalObjects = objects.length;
objects.forEach(function (o, index) {
if (!o.type) {
return;
}
var klass = fabric.util.getKlass(o.type, namespace);
if (klass.async) {
klass.fromObject(o, function (obj, error) {
if (!error) {
enlivenedObjects[index] = obj;
reviver && reviver(o, enlivenedObjects[index]);
}
onLoaded();
});
}
else {
enlivenedObjects[index] = klass.fromObject(o);
reviver && reviver(o, enlivenedObjects[index]);
onLoaded();
}
});
}
/**
* Groups SVG elements (usually those retrieved from SVG document)
* @static
* @memberOf fabric.util
* @param {Array} elements SVG elements to group
* @param {Object} [options] Options object
* @return {fabric.Object|fabric.PathGroup}
*/
function groupSVGElements(elements, options, path) {
var object;
if (elements.length > 1) {
object = new fabric.PathGroup(elements, options);
}
else {
object = elements[0];
}
if (typeof path !== 'undefined') {
object.setSourcePath(path);
}
return object;
}
/**
* Populates an object with properties of another object
* @static
* @memberOf fabric.util
* @param {Object} source Source object
* @param {Object} destination Destination object
* @return {Array} properties Propertie names to include
*/
function populateWithProperties(source, destination, properties) {
if (properties && Object.prototype.toString.call(properties) === '[object Array]') {
for (var i = 0, len = properties.length; i < len; i++) {
if (properties[i] in source) {
destination[properties[i]] = source[properties[i]];
}
}
}
}
/**
* Draws a dashed line between two points
*
* This method is used to draw dashed line around selection area.
* See <a href="http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas">dotted stroke in canvas</a>
*
* @param ctx {Canvas} context
* @param x {Number} start x coordinate
* @param y {Number} start y coordinate
* @param x2 {Number} end x coordinate
* @param y2 {Number} end y coordinate
* @param da {Array} dash array pattern
*/
function drawDashedLine(ctx, x, y, x2, y2, da) {
var dx = x2 - x,
dy = y2 - y,
len = sqrt(dx*dx + dy*dy),
rot = atan2(dy, dx),
dc = da.length,
di = 0,
draw = true;
ctx.save();
ctx.translate(x, y);
ctx.moveTo(0, 0);
ctx.rotate(rot);
x = 0;
while (len > x) {
x += da[di++ % dc];
if (x > len) {
x = len;
}
ctx[draw ? 'lineTo' : 'moveTo'](x, 0);
draw = !draw;
}
ctx.restore();
}
/**
* Creates canvas element and initializes it via excanvas if necessary
* @static
* @memberOf fabric.util
* @param {CanvasElement} [canvasEl] optional canvas element to initialize; when not given, element is created implicitly
* @return {CanvasElement} initialized canvas element
*/
function createCanvasElement(canvasEl) {
canvasEl || (canvasEl = fabric.document.createElement('canvas'));
if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(canvasEl);
}
return canvasEl;
}
/**
* Creates image element (works on client and node)
* @static
* @memberOf fabric.util
* @return {HTMLImageElement} HTML image element
*/
function createImage() {
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
*/
function createAccessors(klass) {
var proto = klass.prototype;
for (var i = proto.stateProperties.length; i--; ) {
var 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
*/
function clipContext(receiver, ctx) {
ctx.save();
ctx.beginPath();
receiver.clipTo(ctx);
ctx.clip();
}
/**
* Multiply matrix A by matrix B to nest transformations
* @static
* @memberOf fabric.util
* @param {Array} matrixA First transformMatrix
* @param {Array} matrixB Second transformMatrix
* @return {Array} The product of the two transform matrices
*/
function multiplyTransformMatrices(matrixA, matrixB) {
// Matrix multiply matrixA * matrixB
var a = [
[matrixA[0], matrixA[2], matrixA[4]],
[matrixA[1], matrixA[3], matrixA[5]],
[0 , 0 , 1 ]
];
var b = [
[matrixB[0], matrixB[2], matrixB[4]],
[matrixB[1], matrixB[3], matrixB[5]],
[0 , 0 , 1 ]
];
var result = [];
for (var r=0; r<3; r++) {
result[r] = [];
for (var c=0; c<3; c++) {
var sum = 0;
for (var k=0; k<3; k++) {
sum += a[r][k]*b[k][c];
}
result[r][c] = sum;
}
}
return [
result[0][0],
result[1][0],
result[0][1],
result[1][1],
result[0][2],
result[1][2]
];
}
function getFunctionBody(fn) {
return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1];
}
function drawArc(ctx, x, y, coords) {
var rx = coords[0];
var ry = coords[1];
var rot = coords[2];
var large = coords[3];
var sweep = coords[4];
var ex = coords[5];
var ey = coords[6];
var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y);
for (var i=0; i<segs.length; i++) {
var bez = segmentToBezier.apply(this, segs[i]);
ctx.bezierCurveTo.apply(ctx, bez);
}
}
var arcToSegmentsCache = { },
segmentToBezierCache = { },
_join = Array.prototype.join,
argsString;
// Generous contribution by Raph Levien, from libsvg-0.1.0.tar.gz
function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
argsString = _join.call(arguments);
if (arcToSegmentsCache[argsString]) {
return arcToSegmentsCache[argsString];
}
var th = rotateX * (Math.PI/180);
var sin_th = Math.sin(th);
var cos_th = Math.cos(th);
rx = Math.abs(rx);
ry = Math.abs(ry);
var px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5;
var py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5;
var pl = (px*px) / (rx*rx) + (py*py) / (ry*ry);
if (pl > 1) {
pl = Math.sqrt(pl);
rx *= pl;
ry *= pl;
}
var a00 = cos_th / rx;
var a01 = sin_th / rx;
var a10 = (-sin_th) / ry;
var a11 = (cos_th) / ry;
var x0 = a00 * ox + a01 * oy;
var y0 = a10 * ox + a11 * oy;
var x1 = a00 * x + a01 * y;
var y1 = a10 * x + a11 * y;
var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0);
var sfactor_sq = 1 / d - 0.25;
if (sfactor_sq < 0) sfactor_sq = 0;
var sfactor = Math.sqrt(sfactor_sq);
if (sweep === large) sfactor = -sfactor;
var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0);
var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0);
var th0 = Math.atan2(y0-yc, x0-xc);
var th1 = Math.atan2(y1-yc, x1-xc);
var th_arc = th1-th0;
if (th_arc < 0 && sweep === 1){
th_arc += 2*Math.PI;
} else if (th_arc > 0 && sweep === 0) {
th_arc -= 2 * Math.PI;
}
var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001)));
var result = [];
for (var i=0; i<segments; i++) {
var th2 = th0 + i * th_arc / segments;
var th3 = th0 + (i+1) * th_arc / segments;
result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th];
}
arcToSegmentsCache[argsString] = result;
return result;
}
function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) {
argsString = _join.call(arguments);
if (segmentToBezierCache[argsString]) {
return segmentToBezierCache[argsString];
}
var a00 = cos_th * rx;
var a01 = -sin_th * ry;
var a10 = sin_th * rx;
var a11 = cos_th * ry;
var th_half = 0.5 * (th1 - th0);
var t = (8/3) * Math.sin(th_half * 0.5) * Math.sin(th_half * 0.5) / Math.sin(th_half);
var x1 = cx + Math.cos(th0) - t * Math.sin(th0);
var y1 = cy + Math.sin(th0) + t * Math.cos(th0);
var x3 = cx + Math.cos(th1);
var y3 = cy + Math.sin(th1);
var x2 = x3 + t * Math.sin(th1);
var y2 = y3 - t * Math.cos(th1);
segmentToBezierCache[argsString] = [
a00 * x1 + a01 * y1, a10 * x1 + a11 * y1,
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2,
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3
];
return segmentToBezierCache[argsString];
}
fabric.util.removeFromArray = removeFromArray;
fabric.util.degreesToRadians = degreesToRadians;
fabric.util.radiansToDegrees = radiansToDegrees;
fabric.util.rotatePoint = rotatePoint;
fabric.util.toFixed = toFixed;
fabric.util.getRandomInt = getRandomInt;
fabric.util.falseFunction = falseFunction;
fabric.util.getKlass = getKlass;
fabric.util.resolveNamespace = resolveNamespace;
fabric.util.loadImage = loadImage;
fabric.util.enlivenObjects = enlivenObjects;
fabric.util.groupSVGElements = groupSVGElements;
fabric.util.populateWithProperties = populateWithProperties;
fabric.util.drawDashedLine = drawDashedLine;
fabric.util.createCanvasElement = createCanvasElement;
fabric.util.createImage = createImage;
fabric.util.createAccessors = createAccessors;
fabric.util.clipContext = clipContext;
fabric.util.multiplyTransformMatrices = multiplyTransformMatrices;
fabric.util.getFunctionBody = getFunctionBody;
fabric.util.drawArc = drawArc;
})();
(function() {
var slice = Array.prototype.slice;
/* _ES5_COMPAT_START_ */
if (!Array.prototype.indexOf) {
/**
* Finds index of an element in an array
* @param {Any} searchElement
* @param {Number} [fromIndex]
* @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
* @param {Object} [context] Context to invoke callback in
* @return {Any}
*/
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
* @param {Array} array Array to iterate over
* @param {String} method Name of a method to invoke
* @return {Array}
*/
function invoke(array, method) {
var args = slice.call(arguments, 2), result = [ ];
for (var i = 0, len = array.length; i < len; i++) {
result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]);
}
return result;
}
/**
* Finds maximum value in array (not necessarily "first" one)
* @memberOf fabric.util.array
* @param {Array} array Array to iterate over
* @param {String} byProperty
* @return {Any}
*/
function max(array, byProperty) {
if (!array || array.length === 0) return undefined;
var i = array.length - 1,
result = byProperty ? array[i][byProperty] : array[i];
if (byProperty) {
while (i--) {
if (array[i][byProperty] >= result) {
result = array[i][byProperty];
}
}
}
else {
while (i--) {
if (array[i] >= result) {
result = array[i];
}
}
}
return result;
}
/**
* Finds minimum value in array (not necessarily "first" one)
* @memberOf fabric.util.array
* @param {Array} array Array to iterate over
* @param {String} byProperty
* @return {Any}
*/
function min(array, byProperty) {
if (!array || array.length === 0) return undefined;
var i = array.length - 1,
result = byProperty ? array[i][byProperty] : array[i];
if (byProperty) {
while (i--) {
if (array[i][byProperty] < result) {
result = array[i][byProperty];
}
}
}
else {
while (i--) {
if (array[i] < result) {
result = array[i];
}
}
}
return result;
}
/**
* @namespace fabric.util.array
*/
fabric.util.array = {
invoke: invoke,
min: min,
max: max
};
})();
(function(){
/**
* Copies all enumerable properties of one object to another
* @memberOf fabric.util.object
* @param {Object} destination Where to copy to
* @param {Object} source Where to copy from
* @return {Object}
*/
function extend(destination, source) {
// JScript DontEnum bug is not taken care of
for (var property in source) {
destination[property] = source[property];
}
return destination;
}
/**
* Creates an empty object and copies all enumerable properties of another object to it
* @memberOf fabric.util.object
* @param {Object} object Object to clone
* @return {Object}
*/
function clone(object) {
return extend({ }, object);
}
/** @namespace fabric.util.object */
fabric.util.object = {
extend: extend,
clone: clone
};
})();
(function() {
/* _ES5_COMPAT_START_ */
if (!String.prototype.trim) {
/**
* Trims a string (removing whitespace from the beginning and the end)
* @function external:String#trim
* @see <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/Trim">String#trim on MDN</a>
*/
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
* @param {String} string String to camelize
* @return {String} Camelized version of a string
*/
function camelize(string) {
return string.replace(/-+(.)?/g, function(match, character) {
return character ? character.toUpperCase() : '';
});
}
/**
* Capitalizes a string
* @memberOf fabric.util.string
* @param {String} string String to capitalize
* @return {String} Capitalized version of a string
*/
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
}
/**
* Escapes XML in a string
* @memberOf fabric.util.string
* @param {String} string String to escape
* @return {String} Escaped version of a string
*/
function escapeXml(string) {
return string.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
/**
* String utilities
* @namespace fabric.util.string
*/
fabric.util.string = {
camelize: camelize,
capitalize: capitalize,
escapeXml: escapeXml
};
}());
/* _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 <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind">Function#bind on MDN</a>
* @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 fn = this, args = slice.call(arguments, 1), bound;
if (args.length) {
bound = function() {
return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments)));
};
}
else {
/** @ignore */
bound = function() {
return apply.call(fn, 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() { };
var IS_DONTENUM_BUGGY = (function(){
for (var p in { toString: 1 }) {
if (p === 'toString') return false;
}
return true;
})();
/** @ignore */
var addMethods = function(klass, source, parent) {
for (var property in source) {
if (property in klass.prototype &&
typeof klass.prototype[property] === 'function' &&
(source[property] + '').indexOf('callSuper') > -1) {
klass.prototype[property] = (function(property) {
return function() {
var superclass = this.constructor.superclass;
this.constructor.superclass = parent;
var returnValue = source[property].apply(this, arguments);
this.constructor.superclass = superclass;
if (property !== 'initialize') {
return returnValue;
}
};
})(property);
}
else {
klass.prototype[property] = source[property];
}
if (IS_DONTENUM_BUGGY) {
if (source.toString !== Object.prototype.toString) {
klass.prototype.toString = source.toString;
}
if (source.valueOf !== Object.prototype.valueOf) {
klass.prototype.valueOf = source.valueOf;
}
}
}
};
function Subclass() { }
function callSuper(methodName) {
var fn = this.constructor.superclass.prototype[methodName];
return (arguments.length > 1)
? fn.apply(this, slice.call(arguments, 1))
: fn.call(this);
}
/**
* Helper for creation of "classes".
* @memberOf fabric.util
* @param parent optional "Class" to inherit from
* @param properties Properties shared by all instances of this class
* (be careful modifying objects defined here as this would affect all instances)
*/
function createClass() {
var parent = null,
properties = slice.call(arguments, 0);
if (typeof properties[0] === 'function') {
parent = properties.shift();
}
function klass() {
this.initialize.apply(this, arguments);
}
klass.superclass = parent;
klass.subclasses = [ ];
if (parent) {
Subclass.prototype = parent.prototype;
klass.prototype = new Subclass();
parent.subclasses.push(klass);
}
for (var i = 0, length = properties.length; i < length; i++) {
addMethods(klass, properties[i], parent);
}
if (!klass.prototype.initialize) {
klass.prototype.initialize = emptyFunction;
}
klass.prototype.constructor = klass;
klass.prototype.callSuper = callSuper;
return klass;
}
fabric.util.createClass = createClass;
})();
(function () {
/* 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;
}
var getUniqueId = (function () {
var uid = 0;
return function (element) {
return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
};
})();
/** @ignore */
var getElement, setElement;
(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) {
element.addEventListener(eventName, handler, false);
};
/** @ignore */
removeListener = function (element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
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);
}
}
}
};
}
/**
* Adds an event listener to an element
* @function
* @memberOf fabric.util
* @param {HTMLElement} element
* @param {String} eventName
* @param {Function} handler
*/
fabric.util.addListener = addListener;
/**
* Removes an event listener from an element
* @function
* @memberOf fabric.util
* @param {HTMLElement} element
* @param {String} eventName
* @param {Function} handler
*/
fabric.util.removeListener = removeListener;
/**
* Cross-browser wrapper for getting event's coordinates
* @memberOf fabric.util
* @param {Event} event
* @param {HTMLCanvasElement} upperCanvasEl &lt;canvas> element on which object selection is drawn
*/
function getPointer(event, upperCanvasEl) {
event || (event = fabric.window.event);
var element = event.target || (typeof event.srcElement !== 'unknown' ? event.srcElement : null),
body = fabric.document.body || {scrollLeft: 0, scrollTop: 0},
docElement = fabric.document.documentElement,
orgElement = element,
scrollLeft = 0,
scrollTop = 0,
firstFixedAncestor;
while (element && element.parentNode && !firstFixedAncestor) {
element = element.parentNode;
if (element !== fabric.document &&
fabric.util.getElementStyle(element, 'position') === 'fixed') {
firstFixedAncestor = element;
}
if (element !== fabric.document &&
orgElement !== upperCanvasEl &&
fabric.util.getElementStyle(element, 'position') === 'absolute') {
scrollLeft = 0;
scrollTop = 0;
}
else if (element === fabric.document) {
scrollLeft = body.scrollLeft || docElement.scrollLeft || 0;
scrollTop = body.scrollTop || docElement.scrollTop || 0;
}
else {
scrollLeft += element.scrollLeft || 0;
scrollTop += element.scrollTop || 0;
}
}
return {
x: pointerX(event) + scrollLeft,
y: pointerY(event) + scrollTop
};
}
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);
};
var pointerY = function(event) {
return (typeof event.clientY !== 'unknown' ? event.clientY : 0);
};
if (fabric.isTouchSupported) {
pointerX = function(event) {
if (event.type !== 'touchend') {
return (event.touches && event.touches[0] ?
(event.touches[0].pageX - (event.touches[0].pageX - event.touches[0].clientX)) || event.clientX : event.clientX);
}
return (event.changedTouches && event.changedTouches[0]
? (event.changedTouches[0].pageX - (event.changedTouches[0].pageX - event.changedTouches[0].clientX)) || event.clientX : event.clientX);
};
pointerY = function(event) {
if (event.type !== 'touchend') {
return (event.touches && event.touches[0]
? (event.touches[0].pageY - (event.touches[0].pageY - event.touches[0].clientY)) || event.clientY : event.clientY);
}
return (event.changedTouches && event.changedTouches[0]
? (event.changedTouches[0].pageY - (event.changedTouches[0].pageY - event.changedTouches[0].clientY)) || event.clientY : event.clientY);
};
}
fabric.util.getPointer = getPointer;
fabric.util.object.extend(fabric.util, fabric.Observable);
})();
(function () {
/**
* Cross-browser wrapper for setting element's style
* @memberOf fabric.util
* @param {HTMLElement} element
* @param {Object} styles
* @return {HTMLElement} Element that was passed as a first argument
*/
function setStyle(element, styles) {
var elementStyle = element.style;
if (!elementStyle) {
return element;
}
if (typeof styles === 'string') {
element.style.cssText += ';' + styles;
return styles.indexOf('opacity') > -1
? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1])
: element;
}
for (var property in styles) {
if (property === 'opacity') {
setOpacity(element, styles[property]);
}
else {
var normalizedProperty = (property === 'float' || property === 'cssFloat')
? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat')
: property;
elementStyle[normalizedProperty] = styles[property];
}
}
return element;
}
var parseEl = fabric.document.createElement('div'),
supportsOpacity = typeof parseEl.style.opacity === 'string',
supportsFilters = typeof parseEl.style.filter === 'string',
reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,
/** @ignore */
setOpacity = function (element) { return element; };
if (supportsOpacity) {
/** @ignore */
setOpacity = function(element, value) {
element.style.opacity = value;
return element;
};
}
else if (supportsFilters) {
/** @ignore */
setOpacity = function(element, value) {
var es = element.style;
if (element.currentStyle && !element.currentStyle.hasLayout) {
es.zoom = 1;
}
if (reOpacity.test(es.filter)) {
value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')');
es.filter = es.filter.replace(reOpacity, value);
}
else {
es.filter += ' alpha(opacity=' + (value * 100) + ')';
}
return element;
};
}
fabric.util.setStyle = setStyle;
})();
(function() {
var _slice = Array.prototype.slice;
/**
* Takes id and returns an element with that id (if one exists in a document)
* @memberOf fabric.util
* @param {String|HTMLElement} id
* @return {HTMLElement|null}
*/
function getById(id) {
return typeof id === 'string' ? fabric.document.getElementById(id) : id;
}
/**
* Converts an array-like object (e.g. arguments or NodeList) to an array
* @memberOf fabric.util
* @param {Object} arrayLike
* @return {Array}
*/
var toArray = function(arrayLike) {
return _slice.call(arrayLike, 0);
};
var sliceCanConvertNodelists;
try {
sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array;
}
catch(err) { }
if (!sliceCanConvertNodelists) {
toArray = function(arrayLike) {
var arr = new Array(arrayLike.length), i = arrayLike.length;
while (i--) {
arr[i] = arrayLike[i];
}
return arr;
};
}
/**
* Creates specified element with specified attributes
* @memberOf fabric.util
* @param {String} tagName Type of an element to create
* @param {Object} [attributes] Attributes to set on an element
* @return {HTMLElement} Newly created element
*/
function makeElement(tagName, attributes) {
var el = fabric.document.createElement(tagName);
for (var prop in attributes) {
if (prop === 'class') {
el.className = attributes[prop];
}
else if (prop === 'for') {
el.htmlFor = attributes[prop];
}
else {
el.setAttribute(prop, attributes[prop]);
}
}
return el;
}
/**
* Adds class to an element
* @memberOf fabric.util
* @param {HTMLElement} element Element to add class to
* @param {String} className Class to add to an element
*/
function addClass(element, className) {
if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) {
element.className += (element.className ? ' ' : '') + className;
}
}
/**
* Wraps element with another element
* @memberOf fabric.util
* @param {HTMLElement} element Element to wrap
* @param {HTMLElement|String} wrapper Element to wrap with
* @param {Object} [attributes] Attributes to set on a wrapper
* @return {HTMLElement} wrapper
*/
function wrapElement(element, wrapper, attributes) {
if (typeof wrapper === 'string') {
wrapper = makeElement(wrapper, attributes);
}
if (element.parentNode) {
element.parentNode.replaceChild(wrapper, element);
}
wrapper.appendChild(element);
return wrapper;
}
/**
* Returns offset for a given element
* @function
* @memberOf fabric.util
* @param {HTMLElement} element Element to get offset for
* @return {Object} Object with "left" and "top" properties
*/
function getElementOffset(element) {
var docElem, win,
box = {left: 0, top: 0},
doc = element && element.ownerDocument,
offset = {left: 0, top: 0},
offsetAttributes = {
'borderLeftWidth': 'left',
'borderTopWidth': 'top',
'paddingLeft': 'left',
'paddingTop': 'top'
};
if (!doc){
return {left: 0, top: 0};
}
for (var attr in offsetAttributes) {
offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0;
}
docElem = doc.documentElement;
if ( typeof element.getBoundingClientRect !== "undefined" ) {
box = element.getBoundingClientRect();
}
if(doc != null && doc === doc.window){
win = doc;
} else {
win = doc.nodeType === 9 && (doc.defaultView || doc.parentWindow);
}
return {
left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + offset.left,
top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0) + offset.top
};
}
/**
* Returns style attribute value of a given element
* @memberOf fabric.util
* @param {HTMLElement} element Element to get style attribute for
* @param {String} attr Style attribute to get for element
* @return {String} Style attribute value of the given element.
*/
function getElementStyle(element, attr) {
if (!element.style) {
element.style = { };
}
if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) {
return fabric.document.defaultView.getComputedStyle(element, null)[attr];
}
else {
var value = element.style[attr];
if (!value && element.currentStyle) value = element.currentStyle[attr];
return value;
}
}
(function () {
var style = fabric.document.documentElement.style;
var selectProp = 'userSelect' in style
? 'userSelect'
: 'MozUserSelect' in style
? 'MozUserSelect'
: 'WebkitUserSelect' in style
? 'WebkitUserSelect'
: 'KhtmlUserSelect' in style
? 'KhtmlUserSelect'
: '';
/**
* Makes element unselectable
* @memberOf fabric.util
* @param {HTMLElement} element Element to make unselectable
* @return {HTMLElement} Element that was passed in
*/
function makeElementUnselectable(element) {
if (typeof element.onselectstart !== 'undefined') {
element.onselectstart = fabric.util.falseFunction;
}
if (selectProp) {
element.style[selectProp] = 'none';
}
else if (typeof element.unselectable === 'string') {
element.unselectable = 'on';
}
return element;
}
/**
* Makes element selectable
* @memberOf fabric.util
* @param {HTMLElement} element Element to make selectable
* @return {HTMLElement} Element that was passed in
*/
function makeElementSelectable(element) {
if (typeof element.onselectstart !== 'undefined') {
element.onselectstart = null;
}
if (selectProp) {
element.style[selectProp] = '';
}
else if (typeof element.unselectable === 'string') {
element.unselectable = '';
}
return element;
}
fabric.util.makeElementUnselectable = makeElementUnselectable;
fabric.util.makeElementSelectable = makeElementSelectable;
})();
(function() {
/**
* Inserts a script element with a given url into a document; invokes callback, when that script is finished loading
* @memberOf fabric.util
* @param {String} url URL of a script to load
* @param {Function} callback Callback to execute when script is finished loading
*/
function getScript(url, callback) {
var headEl = fabric.document.getElementsByTagName("head")[0],
scriptEl = fabric.document.createElement('script'),
loading = true;
/** @ignore */
scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) {
if (loading) {
if (typeof this.readyState === 'string' &&
this.readyState !== 'loaded' &&
this.readyState !== 'complete') return;
loading = false;
callback(e || fabric.window.event);
scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null;
}
};
scriptEl.src = url;
headEl.appendChild(scriptEl);
// causes issue in Opera
// headEl.removeChild(scriptEl);
}
fabric.util.getScript = getScript;
})();
fabric.util.getById = getById;
fabric.util.toArray = toArray;
fabric.util.makeElement = makeElement;
fabric.util.addClass = addClass;
fabric.util.wrapElement = wrapElement;
fabric.util.getElementOffset = getElementOffset;
fabric.util.getElementStyle = getElementStyle;
})();
(function(){
function addParamToUrl(url, param) {
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() { }
/**
* Cross-browser abstraction for sending XMLHttpRequest
* @memberOf fabric.util
* @param {String} url URL to send XMLHttpRequest to
* @param {Object} [options] Options object
* @param {String} [options.method="GET"]
* @param {Function} options.onComplete Callback to invoke when request is completed
* @return {XMLHttpRequest} request
*/
function request(url, options) {
options || (options = { });
var method = options.method ? options.method.toUpperCase() : 'GET',
onComplete = options.onComplete || function() { },
xhr = makeXHR(),
body;
/** @ignore */
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
onComplete(xhr);
xhr.onreadystatechange = emptyFn;
}
};
if (method === 'GET') {
body = null;
if (typeof options.parameters === 'string') {
url = addParamToUrl(url, options.parameters);
}
}
xhr.open(method, url, true);
if (method === 'POST' || method === 'PUT') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhr.send(body);
return xhr;
}
fabric.util.request = request;
})();
(function() {
/**
* 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
*/
function animate(options) {
options || (options = { });
var start = +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;},
startValue = 'startValue' in options ? options.startValue : 0,
endValue = 'endValue' in options ? options.endValue : 100,
byValue = options.byValue || endValue - startValue;
options.onStart && options.onStart();
(function tick() {
time = +new Date();
var currentTime = time > finish ? duration : (time - start);
if (abort()) {
options.onComplete && options.onComplete();
return;
}
onChange(easing(currentTime, startValue, byValue, duration));
if (time > finish) {
options.onComplete && options.onComplete();
return;
}
requestAnimFrame(tick);
})();
}
var _requestAnimFrame = fabric.window.requestAnimationFrame ||
fabric.window.webkitRequestAnimationFrame ||
fabric.window.mozRequestAnimationFrame ||
fabric.window.oRequestAnimationFrame ||
fabric.window.msRequestAnimationFrame ||
function(callback) {
fabric.window.setTimeout(callback, 1000 / 60);
};
/**
* requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/
* @memberOf fabric.util
* @param {Function} callback Callback to invoke
* @param {DOMElement} element optional Element to associate with animation
*/
var requestAnimFrame = function() {
return _requestAnimFrame.apply(fabric.window, arguments);
};
fabric.util.animate = animate;
fabric.util.requestAnimFrame = requestAnimFrame;
})();
(function() {
/**
* Quadratic easing in
* @memberOf fabric.util.ease
*/
function easeInQuad(t, b, c, d) {
return c*(t/=d)*t + b;
}
/**
* Quadratic easing out
* @memberOf fabric.util.ease
*/
function easeOutQuad(t, b, c, d) {
return -c *(t/=d)*(t-2) + b;
}
/**
* Quadratic easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutQuad(t, b, c, d) {
t /= (d/2);
if (t < 1) return c/2*t*t + b;
return -c/2 * ((--t)*(t-2) - 1) + b;
}
/**
* Cubic easing in
* @memberOf fabric.util.ease
*/
function easeInCubic(t, b, c, d) {
return c*(t/=d)*t*t + b;
}
/**
* Cubic easing out
* @memberOf fabric.util.ease
*/
function easeOutCubic(t, b, c, d) {
return c*((t=t/d-1)*t*t + 1) + b;
}
/**
* Cubic easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutCubic(t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t*t + b;
return c/2*((t-=2)*t*t + 2) + b;
}
/**
* Quartic easing in
* @memberOf fabric.util.ease
*/
function easeInQuart(t, b, c, d) {
return c*(t/=d)*t*t*t + b;
}
/**
* Quartic easing out
* @memberOf fabric.util.ease
*/
function easeOutQuart(t, b, c, d) {
return -c * ((t=t/d-1)*t*t*t - 1) + b;
}
/**
* Quartic easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutQuart(t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t*t*t + b;
return -c/2 * ((t-=2)*t*t*t - 2) + b;
}
/**
* Quintic easing in
* @memberOf fabric.util.ease
*/
function easeInQuint(t, b, c, d) {
return c*(t/=d)*t*t*t*t + b;
}
/**
* Quintic easing out
* @memberOf fabric.util.ease
*/
function easeOutQuint(t, b, c, d) {
return c*((t=t/d-1)*t*t*t*t + 1) + b;
}
/**
* Quintic easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutQuint(t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t*t*t*t + b;
return c/2*((t-=2)*t*t*t*t + 2) + b;
}
/**
* Sinusoidal easing in
* @memberOf fabric.util.ease
*/
function easeInSine(t, b, c, d) {
return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
}
/**
* Sinusoidal easing out
* @memberOf fabric.util.ease
*/
function easeOutSine(t, b, c, d) {
return c * Math.sin(t/d * (Math.PI/2)) + b;
}
/**
* Sinusoidal easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutSine(t, b, c, d) {
return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
}
/**
* Exponential easing in
* @memberOf fabric.util.ease
*/
function easeInExpo(t, b, c, d) {
return (t===0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
}
/**
* Exponential easing out
* @memberOf fabric.util.ease
*/
function easeOutExpo(t, b, c, d) {
return (t===d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
}
/**
* Exponential easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutExpo(t, b, c, d) {
if (t===0) return b;
if (t===d) return b+c;
t /= d/2;
if (t < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
/**
* Circular easing in
* @memberOf fabric.util.ease
*/
function easeInCirc(t, b, c, d) {
return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
}
/**
* Circular easing out
* @memberOf fabric.util.ease
*/
function easeOutCirc(t, b, c, d) {
return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
}
/**
* Circular easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutCirc(t, b, c, d) {
t /= d/2;
if (t < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
}
/**
* Elastic easing in
* @memberOf fabric.util.ease
*/
function easeInElastic(t, b, c, d) {
var s=1.70158;var p=0;var a=c;
if (t===0) return b;
t /= d;
if (t===1) return b+c;
if (!p) p=d*0.3;
if (a < Math.abs(c)) { a=c; s=p/4; }
else s = p/(2*Math.PI) * Math.asin (c/a);
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
}
/**
* Elastic easing out
* @memberOf fabric.util.ease
*/
function easeOutElastic(t, b, c, d) {
var s=1.70158;var p=0;var a=c;
if (t===0) return b;
t /= d;
if (t===1) return b+c;
if (!p) p=d*0.3;
if (a < Math.abs(c)) { a=c; s=p/4; }
else s = p/(2*Math.PI) * Math.asin (c/a);
return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
}
/**
* Elastic easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutElastic(t, b, c, d) {
var s=1.70158;var p=0;var a=c;
if (t===0) return b;
t /= d/2;
if (t===2) return b+c;
if (!p) p=d*(0.3*1.5);
if (a < Math.abs(c)) { a=c; s=p/4; }
else s = p/(2*Math.PI) * Math.asin (c/a);
if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b;
}
/**
* Backwards easing in
* @memberOf fabric.util.ease
*/
function easeInBack(t, b, c, d, s) {
if (s === undefined) s = 1.70158;
return c*(t/=d)*t*((s+1)*t - s) + b;
}
/**
* Backwards easing out
* @memberOf fabric.util.ease
*/
function easeOutBack(t, b, c, d, s) {
if (s === undefined) s = 1.70158;
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
}
/**
* Backwards easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutBack(t, b, c, d, s) {
if (s === undefined) s = 1.70158;
t /= d/2;
if (t < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
}
/**
* Bouncing easing in
* @memberOf fabric.util.ease
*/
function easeInBounce(t, b, c, d) {
return c - easeOutBounce (d-t, 0, c, d) + b;
}
/**
* Bouncing easing out
* @memberOf fabric.util.ease
*/
function easeOutBounce(t, b, c, d) {
if ((t/=d) < (1/2.75)) {
return c*(7.5625*t*t) + b;
} else if (t < (2/2.75)) {
return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b;
} else if (t < (2.5/2.75)) {
return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b;
} else {
return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b;
}
}
/**
* Bouncing easing in and out
* @memberOf fabric.util.ease
*/
function easeInOutBounce(t, b, c, d) {
if (t < d/2) return easeInBounce (t*2, 0, c, d) * 0.5 + b;
return easeOutBounce (t*2-d, 0, c, d) * 0.5 + c*0.5 + b;
}
/**
* Easing functions
* See <a href="http://gizma.com/easing/">Easing Equations by Robert Penner</a>
* @namespace fabric.util.ease
*/
fabric.util.ease = {
easeInQuad: easeInQuad,
easeOutQuad: easeOutQuad,
easeInOutQuad: easeInOutQuad,
easeInCubic: easeInCubic,
easeOutCubic: easeOutCubic,
easeInOutCubic: easeInOutCubic,
easeInQuart: easeInQuart,
easeOutQuart: easeOutQuart,
easeInOutQuart: easeInOutQuart,
easeInQuint: easeInQuint,
easeOutQuint: easeOutQuint,
easeInOutQuint: easeInOutQuint,
easeInSine: easeInSine,
easeOutSine: easeOutSine,
easeInOutSine: easeInOutSine,
easeInExpo: easeInExpo,
easeOutExpo: easeOutExpo,
easeInOutExpo: easeInOutExpo,
easeInCirc: easeInCirc,
easeOutCirc: easeOutCirc,
easeInOutCirc: easeInOutCirc,
easeInElastic: easeInElastic,
easeOutElastic: easeOutElastic,
easeInOutElastic: easeInOutElastic,
easeInBack: easeInBack,
easeOutBack: easeOutBack,
easeInOutBack: easeInOutBack,
easeInBounce: easeInBounce,
easeOutBounce: easeOutBounce,
easeInOutBounce: easeInOutBounce
};
}());
(function(global) {
/**
* @name fabric
* @namespace
*/
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
capitalize = fabric.util.string.capitalize,
clone = fabric.util.object.clone,
toFixed = fabric.util.toFixed,
multiplyTransformMatrices = fabric.util.multiplyTransformMatrices;
fabric.SHARED_ATTRIBUTES = [
"transform",
"fill", "fill-opacity", "fill-rule",
"opacity",
"stroke", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width"
];
var attributesMap = {
'fill-opacity': 'fillOpacity',
'fill-rule': 'fillRule',
'font-family': 'fontFamily',
'font-size': 'fontSize',
'font-style': 'fontStyle',
'font-weight': 'fontWeight',
'cx': 'left',
'x': 'left',
'r': 'radius',
'stroke-dasharray': 'strokeDashArray',
'stroke-linecap': 'strokeLineCap',
'stroke-linejoin': 'strokeLineJoin',
'stroke-miterlimit':'strokeMiterLimit',
'stroke-opacity': 'strokeOpacity',
'stroke-width': 'strokeWidth',
'text-decoration': 'textDecoration',
'cy': 'top',
'y': 'top',
'transform': 'transformMatrix'
};
var colorAttributes = {
'stroke': 'strokeOpacity',
'fill': 'fillOpacity'
};
function normalizeAttr(attr) {
// transform attribute names
if (attr in attributesMap) {
return attributesMap[attr];
}
return attr;
}
function normalizeValue(attr, value, parentAttributes) {
var isArray;
if ((attr === 'fill' || attr === 'stroke') && value === 'none') {
value = '';
}
else if (attr === 'fillRule') {
value = (value === 'evenodd') ? 'destination-over' : value;
}
else if (attr === 'strokeDashArray') {
value = value.replace(/,/g, ' ').split(/\s+/);
}
else if (attr === 'transformMatrix') {
if (parentAttributes && parentAttributes.transformMatrix) {
value = multiplyTransformMatrices(
parentAttributes.transformMatrix, fabric.parseTransformAttribute(value));
}
else {
value = fabric.parseTransformAttribute(value);
}
}
isArray = Object.prototype.toString.call(value) === '[object Array]';
// TODO: need to normalize em, %, pt, etc. to px (!)
var parsed = isArray ? value.map(parseFloat) : parseFloat(value);
return (!isArray && isNaN(parsed) ? value : parsed);
}
/**
* @private
* @param {Object} attributes Array of attributes to parse
*/
function _setStrokeFillOpacity(attributes) {
for (var attr in colorAttributes) {
if (!attributes[attr] || typeof attributes[colorAttributes[attr]] === 'undefined') continue;
if (attributes[attr].indexOf('url(') === 0) continue;
var color = new fabric.Color(attributes[attr]);
attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba();
delete attributes[colorAttributes[attr]];
}
return attributes;
}
/**
* Returns an object of attributes' name/value, given element and an array of attribute names;
* Parses parent "g" nodes recursively upwards.
* @static
* @memberOf fabric
* @param {DOMElement} element Element to parse
* @param {Array} attributes Array of attributes to parse
* @return {Object} object containing parsed attributes' names/values
*/
function parseAttributes(element, attributes) {
if (!element) {
return;
}
var value,
parentAttributes = { };
// if there's a parent container (`g` node), parse its attributes recursively upwards
if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) {
parentAttributes = fabric.parseAttributes(element.parentNode, attributes);
}
var ownAttributes = attributes.reduce(function(memo, attr) {
value = element.getAttribute(attr);
if (value) {
attr = normalizeAttr(attr);
value = normalizeValue(attr, value, parentAttributes);
memo[attr] = value;
}
return memo;
}, { });
// add values parsed from style, which take precedence over attributes
// (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes)
ownAttributes = extend(ownAttributes,
extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element)));
return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes));
}
/**
* Parses "transform" attribute, returning an array of values
* @static
* @function
* @memberOf fabric
* @param attributeValue {String} string containing attribute value
* @return {Array} array of 6 elements representing transformation matrix
*/
fabric.parseTransformAttribute = (function() {
function rotateMatrix(matrix, args) {
var angle = args[0];
matrix[0] = Math.cos(angle);
matrix[1] = Math.sin(angle);
matrix[2] = -Math.sin(angle);
matrix[3] = Math.cos(angle);
}
function scaleMatrix(matrix, args) {
var multiplierX = args[0],
multiplierY = (args.length === 2) ? args[1] : args[0];
matrix[0] = multiplierX;
matrix[3] = multiplierY;
}
function skewXMatrix(matrix, args) {
matrix[2] = args[0];
}
function skewYMatrix(matrix, args) {
matrix[1] = args[0];
}
function translateMatrix(matrix, args) {
matrix[4] = args[0];
if (args.length === 2) {
matrix[5] = args[1];
}
}
// identity matrix
var iMatrix = [
1, // a
0, // b
0, // c
1, // d
0, // e
0 // f
],
// == begin transform regexp
number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)',
comma_wsp = '(?:\\s+,?\\s*|,\\s*)',
skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))',
skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))',
rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))',
scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
matrix = '(?:(matrix)\\s*\\(\\s*' +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' +
'\\s*\\))',
transform = '(?:' +
matrix + '|' +
translate + '|' +
scale + '|' +
rotate + '|' +
skewX + '|' +
skewY +
')',
transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')',
transform_list = '^\\s*(?:' + transforms + '?)\\s*$',
// http://www.w3.org/TR/SVG/coords.html#TransformAttribute
reTransformList = new RegExp(transform_list),
// == end transform regexp
reTransform = new RegExp(transform, 'g');
return function(attributeValue) {
// start with identity matrix
var matrix = iMatrix.concat();
var matrices = [ ];
// return if no argument was given or
// an argument does not match transform attribute regexp
if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) {
return matrix;
}
attributeValue.replace(reTransform, function(match) {
var m = new RegExp(transform).exec(match).filter(function (match) {
return (match !== '' && match != null);
}),
operation = m[1],
args = m.slice(2).map(parseFloat);
switch(operation) {
case 'translate':
translateMatrix(matrix, args);
break;
case 'rotate':
rotateMatrix(matrix, args);
break;
case 'scale':
scaleMatrix(matrix, args);
break;
case 'skewX':
skewXMatrix(matrix, args);
break;
case 'skewY':
skewYMatrix(matrix, args);
break;
case 'matrix':
matrix = args;
break;
}
// snapshot current matrix into matrices array
matrices.push(matrix.concat());
// reset
matrix = iMatrix.concat();
});
var combinedMatrix = matrices[0];
while (matrices.length > 1) {
matrices.shift();
combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]);
}
return combinedMatrix;
};
})();
/**
* Parses "points" attribute, returning an array of values
* @static
* @memberOf fabric
* @param points {String} points attribute string
* @return {Array} array of points
*/
function parsePointsAttribute(points) {
// points attribute is required and must not be empty
if (!points) return null;
points = points.trim();
var asPairs = points.indexOf(',') > -1;
points = points.split(/\s+/);
var parsedPoints = [ ], i, len;
// points could look like "10,20 30,40" or "10 20 30 40"
if (asPairs) {
i = 0;
len = points.length;
for (; i < len; i++) {
var pair = points[i].split(',');
parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) });
}
}
else {
i = 0;
len = points.length;
for (; i < len; i+=2) {
parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) });
}
}
// odd number of points is an error
if (parsedPoints.length % 2 !== 0) {
// return null;
}
return parsedPoints;
}
function parseFontDeclaration(value, oStyle) {
// TODO: support non-px font size
var match = value.match(/(normal|italic)?\s*(normal|small-caps)?\s*(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\s*(\d+)px(?:\/(normal|[\d\.]+))?\s+(.*)/);
if (!match) return;
var fontStyle = match[1];
// Font variant is not used
// var fontVariant = match[2];
var fontWeight = match[3];
var fontSize = match[4];
var lineHeight = match[5];
var fontFamily = match[6];
if (fontStyle) {
oStyle.fontStyle = fontStyle;
}
if (fontWeight) {
oStyle.fontSize = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight);
}
if (fontSize) {
oStyle.fontSize = parseFloat(fontSize);
}
if (fontFamily) {
oStyle.fontFamily = fontFamily;
}
if (lineHeight) {
oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight;
}
}
/**
* Parses "style" attribute, retuning an object with values
* @static
* @memberOf fabric
* @param {SVGElement} element Element to parse
* @return {Object} Objects with values parsed from style attribute of an element
*/
function parseStyleAttribute(element) {
var oStyle = { },
style = element.getAttribute('style'),
attr, value;
if (!style) return oStyle;
if (typeof style === 'string') {
style.replace(/;$/, '').split(';').forEach(function (chunk) {
var pair = chunk.split(':');
attr = normalizeAttr(pair[0].trim().toLowerCase());
value = normalizeValue(attr, pair[1].trim());
if (attr === 'font') {
parseFontDeclaration(value, oStyle);
}
else {
oStyle[attr] = value;
}
});
}
else {
for (var prop in style) {
if (typeof style[prop] === 'undefined') continue;
attr = normalizeAttr(prop.toLowerCase());
value = normalizeValue(attr, style[prop]);
if (attr === 'font') {
parseFontDeclaration(value, oStyle);
}
else {
oStyle[attr] = value;
}
}
}
return oStyle;
}
function resolveGradients(instances) {
for (var i = instances.length; i--; ) {
var instanceFillValue = instances[i].get('fill');
if (/^url\(/.test(instanceFillValue)) {
var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1);
if (fabric.gradientDefs[gradientId]) {
instances[i].set('fill',
fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i]));
}
}
}
}
/**
* Transforms an array of svg elements to corresponding fabric.* instances
* @static
* @memberOf fabric
* @param {Array} elements Array of elements to parse
* @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements)
* @param {Object} [options] Options object
* @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created.
*/
function parseElements(elements, callback, options, reviver) {
var instances = new Array(elements.length), i = elements.length;
function checkIfDone() {
if (--i === 0) {
instances = instances.filter(function(el) {
return el != null;
});
resolveGradients(instances);
callback(instances);
}
}
for (var index = 0, el, len = elements.length; index < len; index++) {
el = elements[index];
var klass = fabric[capitalize(el.tagName)];
if (klass && klass.fromElement) {
try {
if (klass.async) {
klass.fromElement(el, (function(index, el) {
return function(obj) {
reviver && reviver(el, obj);
instances.splice(index, 0, obj);
checkIfDone();
};
})(index, el), options);
}
else {
var obj = klass.fromElement(el, options);
reviver && reviver(el, obj);
instances.splice(index, 0, obj);
checkIfDone();
}
}
catch(err) {
fabric.log(err);
}
}
else {
checkIfDone();
}
}
}
/**
* Returns CSS rules for a given SVG document
* @static
* @function
* @memberOf fabric
* @param {SVGDocument} doc SVG document to parse
* @return {Object} CSS rules of this document
*/
function getCSSRules(doc) {
var styles = doc.getElementsByTagName('style'),
allRules = { },
rules;
// very crude parsing of style contents
for (var i = 0, len = styles.length; i < len; i++) {
var styleContents = styles[0].textContent;
// remove comments
styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, '');
rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g);
rules = rules.map(function(rule) { return rule.trim(); });
rules.forEach(function(rule) {
var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/);
rule = match[1];
var declaration = match[2].trim(),
propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/);
if (!allRules[rule]) {
allRules[rule] = { };
}
for (var i = 0, len = propertyValuePairs.length; i < len; i++) {
var pair = propertyValuePairs[i].split(/\s*:\s*/),
property = pair[0],
value = pair[1];
allRules[rule][property] = value;
}
});
}
return allRules;
}
/**
* @private
*/
function getGlobalStylesForElement(element) {
var nodeName = element.nodeName,
className = element.getAttribute('class'),
id = element.getAttribute('id'),
styles = { };
for (var rule in fabric.cssRules) {
var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) ||
(id && new RegExp('^#' + id).test(rule)) ||
(new RegExp('^' + nodeName).test(rule));
if (ruleMatchesElement) {
for (var property in fabric.cssRules[rule]) {
styles[property] = fabric.cssRules[rule][property];
}
}
}
return styles;
}
/**
* Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback
* @static
* @function
* @memberOf fabric
* @param {SVGDocument} doc SVG document to parse
* @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document).
* @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created.
*/
fabric.parseSVGDocument = (function() {
var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/;
// http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute
// \d doesn't quite cut it (as we need to match an actual float number)
// matches, e.g.: +14.56e-12, etc.
var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)';
var reViewBoxAttrValue = new RegExp(
'^' +
'\\s*(' + reNum + '+)\\s*,?' +
'\\s*(' + reNum + '+)\\s*,?' +
'\\s*(' + reNum + '+)\\s*,?' +
'\\s*(' + reNum + '+)\\s*' +
'$'
);
function hasAncestorWithNodeName(element, nodeName) {
while (element && (element = element.parentNode)) {
if (nodeName.test(element.nodeName)) {
return true;
}
}
return false;
}
return function(doc, callback, reviver) {
if (!doc) return;
var startTime = new Date(),
descendants = fabric.util.toArray(doc.getElementsByTagName('*'));
if (descendants.length === 0) {
// we're likely in node, where "o3-xml" library fails to gEBTN("*")
// https://github.com/ajaxorg/node-o3-xml/issues/21
descendants = doc.selectNodes("//*[name(.)!='svg']");
var arr = [ ];
for (var i = 0, len = descendants.length; i < len; i++) {
arr[i] = descendants[i];
}
descendants = arr;
}
var elements = descendants.filter(function(el) {
return reAllowedSVGTagNames.test(el.tagName) &&
!hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement
});
if (!elements || (elements && !elements.length)) return;
var viewBoxAttr = doc.getAttribute('viewBox'),
widthAttr = doc.getAttribute('width'),
heightAttr = doc.getAttribute('height'),
width = null,
height = null,
minX,
minY;
if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) {
minX = parseInt(viewBoxAttr[1], 10);
minY = parseInt(viewBoxAttr[2], 10);
width = parseInt(viewBoxAttr[3], 10);
height = parseInt(viewBoxAttr[4], 10);
}
// values of width/height attributes overwrite those extracted from viewbox attribute
width = widthAttr ? parseFloat(widthAttr) : width;
height = heightAttr ? parseFloat(heightAttr) : height;
var options = {
width: width,
height: height
};
fabric.gradientDefs = fabric.getGradientDefs(doc);
fabric.cssRules = getCSSRules(doc);
// Precedence of rules: style > class > attribute
fabric.parseElements(elements, function(instances) {
fabric.documentParsingTime = new Date() - startTime;
if (callback) {
callback(instances, options);
}
}, clone(options), reviver);
};
})();
/**
* Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`)
* @namespace
*/
var svgCache = {
/**
* @param {String} name
* @param {Function} callback
*/
has: function (name, callback) {
callback(false);
},
/**
* @param {String} url
* @param {Function} callback
*/
get: function () {
/* NOOP */
},
/**
* @param {String} url
* @param {Object} object
*/
set: function () {
/* NOOP */
}
};
/**
* Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy)
* @memberof fabric
* @param {String} url
* @param {Function} callback
* @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created.
*/
function loadSVGFromURL(url, callback, reviver) {
url = url.replace(/^\n\s*/, '').trim();
svgCache.has(url, function (hasUrl) {
if (hasUrl) {
svgCache.get(url, function (value) {
var enlivedRecord = _enlivenCachedObject(value);
callback(enlivedRecord.objects, enlivedRecord.options);
});
}
else {
new fabric.util.request(url, {
method: 'get',
onComplete: onComplete
});
}
});
function onComplete(r) {
var xml = r.responseXML;
if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) {
xml = new ActiveXObject('Microsoft.XMLDOM');
xml.async = 'false';
//IE chokes on DOCTYPE
xml.loadXML(r.responseText.replace(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i,''));
}
if (!xml.documentElement) return;
fabric.parseSVGDocument(xml.documentElement, function (results, options) {
svgCache.set(url, {
objects: fabric.util.array.invoke(results, 'toObject'),
options: options
});
callback(results, options);
}, reviver);
}
}
/**
* @private
*/
function _enlivenCachedObject(cachedObject) {
var objects = cachedObject.objects,
options = cachedObject.options;
objects = objects.map(function (o) {
return fabric[capitalize(o.type)].fromObject(o);
});
return ({ objects: objects, options: options });
}
/**
* Takes string corresponding to an SVG document, and parses it into a set of fabric objects
* @memberof fabric
* @param {String} string
* @param {Function} callback
* @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created.
*/
function loadSVGFromString(string, callback, reviver) {
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(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i,''));
}
fabric.parseSVGDocument(doc.documentElement, function (results, options) {
callback(results, options);
}, reviver);
}
/**
* Creates markup containing SVG font faces
* @param {Array} objects Array of fabric objects
* @return {String}
*/
function createSVGFontFacesMarkup(objects) {
var markup = '';
for (var i = 0, len = objects.length; i < len; i++) {
if (objects[i].type !== 'text' || !objects[i].path) continue;
markup += [
'@font-face {',
'font-family: ', objects[i].fontFamily, '; ',
'src: url(\'', objects[i].path, '\')',
'}'
].join('');
}
if (markup) {
markup = [
'<style type="text/css">',
'<![CDATA[',
markup,
']]>',
'</style>'
].join('');
}
return markup;
}
/**
* Creates markup containing SVG referenced elements like patterns, gradients etc.
* @param {fabric.Canvas} canvas instance of fabric.Canvas
* @return {String}
*/
function createSVGRefElementsMarkup(canvas) {
var markup = '';
if (canvas.backgroundColor && canvas.backgroundColor.source) {
markup = [
'<pattern x="0" y="0" id="backgroundColorPattern" ',
'width="', canvas.backgroundColor.source.width,
'" height="', canvas.backgroundColor.source.height,
'" patternUnits="userSpaceOnUse">',
'<image x="0" y="0" ',
'width="', canvas.backgroundColor.source.width,
'" height="', canvas.backgroundColor.source.height,
'" xlink:href="', canvas.backgroundColor.source.src,
'"></image></pattern>'
].join('');
}
return markup;
}
/**
* Parses an SVG document, returning all of the gradient declarations found in it
* @static
* @function
* @memberOf fabric
* @param {SVGDocument} doc SVG document to parse
* @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element
*/
function getGradientDefs(doc) {
var linearGradientEls = doc.getElementsByTagName('linearGradient'),
radialGradientEls = doc.getElementsByTagName('radialGradient'),
el, i,
gradientDefs = { };
i = linearGradientEls.length;
for (; i--; ) {
el = linearGradientEls[i];
gradientDefs[el.getAttribute('id')] = el;
}
i = radialGradientEls.length;
for (; i--; ) {
el = radialGradientEls[i];
gradientDefs[el.getAttribute('id')] = el;
}
return gradientDefs;
}
extend(fabric, {
parseAttributes: parseAttributes,
parseElements: parseElements,
parseStyleAttribute: parseStyleAttribute,
parsePointsAttribute: parsePointsAttribute,
getCSSRules: getCSSRules,
loadSVGFromURL: loadSVGFromURL,
loadSVGFromString: loadSVGFromString,
createSVGFontFacesMarkup: createSVGFontFacesMarkup,
createSVGRefElementsMarkup: createSVGRefElementsMarkup,
getGradientDefs: getGradientDefs
});
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
/* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */
var fabric = global.fabric || (global.fabric = { });
if (fabric.Point) {
fabric.warn('fabric.Point is already defined');
return;
}
fabric.Point = Point;
/**
* Point class
* @class fabric.Point
* @memberOf fabric
* @constructor
* @param {Number} x
* @param {Number} y
* @return {fabric.Point} thisArg
*/
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype = /** @lends fabric.Point.prototype */ {
constructor: Point,
/**
* Adds another point to this one and returns another one
* @param {fabric.Point} that
* @return {fabric.Point} new Point instance with added values
*/
add: function (that) {
return new Point(this.x + that.x, this.y + that.y);
},
/**
* Adds another point to this one
* @param {fabric.Point} that
* @return {fabric.Point} thisArg
*/
addEquals: function (that) {
this.x += that.x;
this.y += that.y;
return this;
},
/**
* Adds value to this point and returns a new one
* @param {Number} scalar
* @return {fabric.Point} new Point with added value
*/
scalarAdd: function (scalar) {
return new Point(this.x + scalar, this.y + scalar);
},
/**
* Adds value to this point
* @param {Number} scalar
* @param {fabric.Point} thisArg
*/
scalarAddEquals: function (scalar) {
this.x += scalar;
this.y += scalar;
return this;
},
/**
* Subtracts another point from this point and returns a new one
* @param {fabric.Point} that
* @return {fabric.Point} new Point object with subtracted values
*/
subtract: function (that) {
return new Point(this.x - that.x, this.y - that.y);
},
/**
* Subtracts another point from this point
* @param {fabric.Point} that
* @return {fabric.Point} thisArg
*/
subtractEquals: function (that) {
this.x -= that.x;
this.y -= that.y;
return this;
},
/**
* Subtracts value from this point and returns a new one
* @param {Number} scalar
* @return {fabric.Point}
*/
scalarSubtract: function (scalar) {
return new Point(this.x - scalar, this.y - scalar);
},
/**
* Subtracts value from this point
* @param {Number} scalar
* @return {fabric.Point} thisArg
*/
scalarSubtractEquals: function (scalar) {
this.x -= scalar;
this.y -= scalar;
return this;
},
/**
* Miltiplies this point by a value and returns a new one
* @param {Number} scalar
* @return {fabric.Point}
*/
multiply: function (scalar) {
return new Point(this.x * scalar, this.y * scalar);
},
/**
* Miltiplies this point by a value
* @param {Number} scalar
* @return {fabric.Point} thisArg
*/
multiplyEquals: function (scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
},
/**
* Divides this point by a value and returns a new one
* @param {Number} scalar
* @return {fabric.Point}
*/
divide: function (scalar) {
return new Point(this.x / scalar, this.y / scalar);
},
/**
* Divides this point by a value
* @param {Number} scalar
* @return {fabric.Point} thisArg
*/
divideEquals: function (scalar) {
this.x /= scalar;
this.y /= scalar;
return this;
},
/**
* Returns true if this point is equal to another one
* @param {fabric.Point} that
* @return {Boolean}
*/
eq: function (that) {
return (this.x === that.x && this.y === that.y);
},
/**
* Returns true if this point is less than another one
* @param {fabric.Point} that
* @return {Boolean}
*/
lt: function (that) {
return (this.x < that.x && this.y < that.y);
},
/**
* Returns true if this point is less than or equal to another one
* @param {fabric.Point} that
* @return {Boolean}
*/
lte: function (that) {
return (this.x <= that.x && this.y <= that.y);
},
/**
* Returns true if this point is greater another one
* @param {fabric.Point} that
* @return {Boolean}
*/
gt: function (that) {
return (this.x > that.x && this.y > that.y);
},
/**
* Returns true if this point is greater than or equal to another one
* @param {fabric.Point} that
* @return {Boolean}
*/
gte: function (that) {
return (this.x >= that.x && this.y >= that.y);
},
/**
* Returns new point which is the result of linear interpolation with this one and another one
* @param {fabric.Point} that
* @param {Number} t
* @return {fabric.Point}
*/
lerp: function (that, t) {
return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t);
},
/**
* Returns distance from this point and another one
* @param {fabric.Point} that
* @return {Number}
*/
distanceFrom: function (that) {
var dx = this.x - that.x,
dy = this.y - that.y;
return Math.sqrt(dx * dx + dy * dy);
},
/**
* Returns the point between this point and another one
* @param {fabric.Point} that
* @return {fabric.Point}
*/
midPointFrom: function (that) {
return new Point(this.x + (that.x - this.x)/2, this.y + (that.y - this.y)/2);
},
/**
* Returns a new point which is the min of this and another one
* @param {fabric.Point} that
* @return {fabric.Point}
*/
min: function (that) {
return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y));
},
/**
* Returns a new point which is the max of this and another one
* @param {fabric.Point} that
* @return {fabric.Point}
*/
max: function (that) {
return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y));
},
/**
* Returns string representation of this point
* @return {String}
*/
toString: function () {
return this.x + "," + this.y;
},
/**
* Sets x/y of this point
* @param {Number} x
* @return {Number} y
*/
setXY: function (x, y) {
this.x = x;
this.y = y;
},
/**
* Sets x/y of this point from another point
* @param {fabric.Point} that
*/
setFromPoint: function (that) {
this.x = that.x;
this.y = that.y;
},
/**
* Swaps x/y of this point and another point
* @param {fabric.Point} that
*/
swap: function (that) {
var x = this.x,
y = this.y;
this.x = that.x;
this.y = that.y;
that.x = x;
that.y = y;
}
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
/* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */
var fabric = global.fabric || (global.fabric = { });
if (fabric.Intersection) {
fabric.warn('fabric.Intersection is already defined');
return;
}
/**
* Intersection class
* @class fabric.Intersection
* @memberOf fabric
* @constructor
*/
function Intersection(status) {
this.status = status;
this.points = [];
}
fabric.Intersection = Intersection;
fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ {
/**
* Appends a point to intersection
* @param {fabric.Point} point
*/
appendPoint: function (point) {
this.points.push(point);
},
/**
* Appends points to intersection
* @param {Array} points
*/
appendPoints: function (points) {
this.points = this.points.concat(points);
}
};
/**
* Checks if one line intersects another
* @static
* @param {fabric.Point} a1
* @param {fabric.Point} a2
* @param {fabric.Point} b1
* @param {fabric.Point} b2
* @return {fabric.Intersection}
*/
fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) {
var result,
ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
if (u_b !== 0) {
var ua = ua_t / u_b,
ub = ub_t / u_b;
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
result = new Intersection("Intersection");
result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y)));
}
else {
result = new Intersection();
}
}
else {
if (ua_t === 0 || ub_t === 0) {
result = new Intersection("Coincident");
}
else {
result = new Intersection("Parallel");
}
}
return result;
};
/**
* Checks if line intersects polygon
* @static
* @param {fabric.Point} a1
* @param {fabric.Point} a2
* @param {Array} points
* @return {fabric.Intersection}
*/
fabric.Intersection.intersectLinePolygon = function(a1,a2,points){
var result = new Intersection(),
length = points.length;
for (var i = 0; i < length; i++) {
var b1 = points[i],
b2 = points[(i+1) % length],
inter = Intersection.intersectLineLine(a1, a2, b1, b2);
result.appendPoints(inter.points);
}
if (result.points.length > 0) {
result.status = "Intersection";
}
return result;
};
/**
* Checks if polygon intersects another polygon
* @static
* @param {Array} points1
* @param {Array} points2
* @return {fabric.Intersection}
*/
fabric.Intersection.intersectPolygonPolygon = function (points1, points2) {
var result = new Intersection(),
length = points1.length;
for (var i = 0; i < length; i++) {
var a1 = points1[i],
a2 = points1[(i+1) % length],
inter = Intersection.intersectLinePolygon(a1, a2, points2);
result.appendPoints(inter.points);
}
if (result.points.length > 0) {
result.status = "Intersection";
}
return result;
};
/**
* Checks if polygon intersects rectangle
* @static
* @param {Array} points
* @param {Number} r1
* @param {Number} r2
* @return {fabric.Intersection}
*/
fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) {
var min = r1.min(r2),
max = r1.max(r2),
topRight = new fabric.Point(max.x, min.y),
bottomLeft = new fabric.Point(min.x, max.y),
inter1 = Intersection.intersectLinePolygon(min, topRight, points),
inter2 = Intersection.intersectLinePolygon(topRight, max, points),
inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points),
inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points),
result = new Intersection();
result.appendPoints(inter1.points);
result.appendPoints(inter2.points);
result.appendPoints(inter3.points);
result.appendPoints(inter4.points);
if (result.points.length > 0) {
result.status = "Intersection";
}
return result;
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { });
if (fabric.Color) {
fabric.warn('fabric.Color is already defined.');
return;
}
/**
* Color class
* The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations;
* {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects.
*
* @class fabric.Color
* @param {String} color optional in hex or rgb(a) format
* @return {fabric.Color} thisArg
*/
function Color(color) {
if (!color) {
this.setSource([0, 0, 0, 1]);
}
else {
this._tryParsingColor(color);
}
}
fabric.Color = Color;
fabric.Color.prototype = /** @lends fabric.Color.prototype */ {
/**
* @private
* @param {String|Array} color Color value to parse
*/
_tryParsingColor: function(color) {
var source;
if (color in Color.colorNameMap) {
color = Color.colorNameMap[color];
}
source = Color.sourceFromHex(color);
if (!source) {
source = Color.sourceFromRgb(color);
}
if (!source) {
source = Color.sourceFromHsl(color);
}
if (source) {
this.setSource(source);
}
},
/**
* Adapted from <a href="https://rawgithub.com/mjijackson/mjijackson.github.com/master/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript.html">https://github.com/mjijackson</a>
* @private
* @param {Number} r Red color value
* @param {Number} g Green color value
* @param {Number} b Blue color value
* @return {Array} Hsl color
*/
_rgbToHsl: function(r, g, b) {
r /= 255, g /= 255, b /= 255;
var h, s, l,
max = fabric.util.array.max([r, g, b]),
min = fabric.util.array.min([r, g, b]);
l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
}
else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [
Math.round(h * 360),
Math.round(s * 100),
Math.round(l * 100)
];
},
/**
* Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1])
* @return {Array}
*/
getSource: function() {
return this._source;
},
/**
* Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1])
* @param {Array} source
*/
setSource: function(source) {
this._source = source;
},
/**
* Returns color represenation in RGB format
* @return {String} ex: rgb(0-255,0-255,0-255)
*/
toRgb: function() {
var source = this.getSource();
return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')';
},
/**
* Returns color represenation in RGBA format
* @return {String} ex: rgba(0-255,0-255,0-255,0-1)
*/
toRgba: function() {
var source = this.getSource();
return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')';
},
/**
* Returns color represenation in HSL format
* @return {String} ex: hsl(0-360,0%-100%,0%-100%)
*/
toHsl: function() {
var source = this.getSource(),
hsl = this._rgbToHsl(source[0], source[1], source[2]);
return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)';
},
/**
* Returns color represenation in HSLA format
* @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1)
*/
toHsla: function() {
var source = this.getSource(),
hsl = this._rgbToHsl(source[0], source[1], source[2]);
return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')';
},
/**
* Returns color represenation in HEX format
* @return {String} ex: FF5555
*/
toHex: function() {
var source = this.getSource();
var r = source[0].toString(16);
r = (r.length === 1) ? ('0' + r) : r;
var g = source[1].toString(16);
g = (g.length === 1) ? ('0' + g) : g;
var b = source[2].toString(16);
b = (b.length === 1) ? ('0' + b) : b;
return r.toUpperCase() + g.toUpperCase() + b.toUpperCase();
},
/**
* Gets value of alpha channel for this color
* @return {Number} 0-1
*/
getAlpha: function() {
return this.getSource()[3];
},
/**
* Sets value of alpha channel for this color
* @param {Number} alpha Alpha value 0-1
* @return {fabric.Color} thisArg
*/
setAlpha: function(alpha) {
var source = this.getSource();
source[3] = alpha;
this.setSource(source);
return this;
},
/**
* Transforms color to its grayscale representation
* @return {fabric.Color} thisArg
*/
toGrayscale: function() {
var source = this.getSource(),
average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10),
currentAlpha = source[3];
this.setSource([average, average, average, currentAlpha]);
return this;
},
/**
* Transforms color to its black and white representation
* @param {Number} threshold
* @return {fabric.Color} thisArg
*/
toBlackWhite: function(threshold) {
var source = this.getSource(),
average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0),
currentAlpha = source[3];
threshold = threshold || 127;
average = (Number(average) < Number(threshold)) ? 0 : 255;
this.setSource([average, average, average, currentAlpha]);
return this;
},
/**
* Overlays color with another color
* @param {String|fabric.Color} otherColor
* @return {fabric.Color} thisArg
*/
overlayWith: function(otherColor) {
if (!(otherColor instanceof Color)) {
otherColor = new Color(otherColor);
}
var result = [],
alpha = this.getAlpha(),
otherAlpha = 0.5,
source = this.getSource(),
otherSource = otherColor.getSource();
for (var i = 0; i < 3; i++) {
result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha)));
}
result[3] = alpha;
this.setSource(result);
return this;
}
};
/**
* Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5))
* @static
* @field
* @memberOf fabric.Color
*/
fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}\%?)\s*,\s*(\d{1,3}\%?)\s*,\s*(\d{1,3}\%?)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/;
/**
* Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 ))
* @static
* @field
* @memberOf fabric.Color
*/
fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/;
/**
* Regex matching color in HEX format (ex: #FF5555, 010155, aff)
* @static
* @field
* @memberOf fabric.Color
*/
fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i;
/**
* Map of the 17 basic color names with HEX code
* @static
* @field
* @memberOf fabric.Color
* @see: http://www.w3.org/TR/CSS2/syndata.html#color-units
*/
fabric.Color.colorNameMap = {
'aqua': '#00FFFF',
'black': '#000000',
'blue': '#0000FF',
'fuchsia': '#FF00FF',
'gray': '#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'
};
/**
* @private
* @param {Number} p
* @param {Number} q
* @param {Number} t
* @return {Number}
*/
function hue2rgb(p, q, t){
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
/**
* Returns new color object, when given a color in RGB format
* @memberOf fabric.Color
* @param {String} color Color value ex: rgb(0-255,0-255,0-255)
* @return {fabric.Color}
*/
fabric.Color.fromRgb = function(color) {
return Color.fromSource(Color.sourceFromRgb(color));
};
/**
* Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format
* @memberOf fabric.Color
* @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%)
* @return {Array} source
*/
fabric.Color.sourceFromRgb = function(color) {
var match = color.match(Color.reRGBa);
if (match) {
var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1),
g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1),
b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1);
return [
parseInt(r, 10),
parseInt(g, 10),
parseInt(b, 10),
match[4] ? parseFloat(match[4]) : 1
];
}
};
/**
* Returns new color object, when given a color in RGBA format
* @static
* @function
* @memberOf fabric.Color
* @param {String} color
* @return {fabric.Color}
*/
fabric.Color.fromRgba = Color.fromRgb;
/**
* Returns new color object, when given a color in HSL format
* @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%)
* @memberOf fabric.Color
* @return {fabric.Color}
*/
fabric.Color.fromHsl = function(color) {
return Color.fromSource(Color.sourceFromHsl(color));
};
/**
* Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format.
* Adapted from <a href="https://rawgithub.com/mjijackson/mjijackson.github.com/master/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript.html">https://github.com/mjijackson</a>
* @memberOf fabric.Color
* @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1)
* @return {Array} source
* @see http://http://www.w3.org/TR/css3-color/#hsl-color
*/
fabric.Color.sourceFromHsl = function(color) {
var match = color.match(Color.reHSLa);
if (!match) return;
var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360,
s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1),
l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1),
r, g, b;
if (s === 0) {
r = g = b = l;
}
else {
var q = l <= 0.5 ? l * (s + 1) : l + s - l * s;
var p = l * 2 - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [
Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
match[4] ? parseFloat(match[4]) : 1
];
};
/**
* Returns new color object, when given a color in HSLA format
* @static
* @function
* @memberOf fabric.Color
* @param {String} color
* @return {fabric.Color}
*/
fabric.Color.fromHsla = Color.fromHsl;
/**
* Returns new color object, when given a color in HEX format
* @static
* @memberOf fabric.Color
* @param {String} color Color value ex: FF5555
* @return {fabric.Color}
*/
fabric.Color.fromHex = function(color) {
return Color.fromSource(Color.sourceFromHex(color));
};
/**
* Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HEX format
* @static
* @memberOf fabric.Color
* @param {String} color ex: FF5555
* @return {Array} source
*/
fabric.Color.sourceFromHex = function(color) {
if (color.match(Color.reHex)) {
var value = color.slice(color.indexOf('#') + 1),
isShortNotation = (value.length === 3),
r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2),
g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4),
b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6);
return [
parseInt(r, 16),
parseInt(g, 16),
parseInt(b, 16),
1
];
}
};
/**
* Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5])
* @static
* @memberOf fabric.Color
* @param {Array} source
* @return {fabric.Color}
*/
fabric.Color.fromSource = function(source) {
var oColor = new Color();
oColor.setSource(source);
return oColor;
};
})(typeof exports !== 'undefined' ? exports : this);
(function() {
/* _FROM_SVG_START_ */
function getColorStop(el) {
var style = el.getAttribute('style'),
offset = el.getAttribute('offset'),
color, opacity;
// convert percents to absolute values
offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1);
if (style) {
var keyValuePairs = style.split(/\s*;\s*/);
if (keyValuePairs[keyValuePairs.length-1] === '') {
keyValuePairs.pop();
}
for (var i = keyValuePairs.length; i--; ) {
var split = keyValuePairs[i].split(/\s*:\s*/),
key = split[0].trim(),
value = split[1].trim();
if (key === 'stop-color') {
color = value;
}
else if (key === 'stop-opacity') {
opacity = value;
}
}
}
if (!color) {
color = el.getAttribute('stop-color') || 'rgb(0,0,0)';
}
if (!opacity) {
opacity = el.getAttribute('stop-opacity');
}
// convert rgba color to rgb color - alpha value has no affect in svg
color = new fabric.Color(color).toRgb();
return {
offset: offset,
color: color,
opacity: isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity)
};
}
/* _FROM_SVG_END_ */
/**
* Gradient class
* @class fabric.Gradient
*/
fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ {
/**
* Constructor
* @param {Object} [options] Options object with type, coords, gradientUnits and colorStops
* @return {fabric.Gradient} thisArg
*/
initialize: function(options) {
options || (options = { });
var coords = { };
this.id = fabric.Object.__uid++;
this.type = options.type || 'linear';
coords = {
x1: options.coords.x1 || 0,
y1: options.coords.y1 || 0,
x2: options.coords.x2 || 0,
y2: options.coords.y2 || 0
};
if (this.type === 'radial') {
coords.r1 = options.coords.r1 || 0;
coords.r2 = options.coords.r2 || 0;
}
this.coords = coords;
this.gradientUnits = options.gradientUnits || 'objectBoundingBox';
this.colorStops = options.colorStops.slice();
},
/**
* Adds another colorStop
* @param {Object} colorStop Object with offset and color
* @return {fabric.Gradient} thisArg
*/
addColorStop: function(colorStop) {
for (var position in colorStop) {
var color = new fabric.Color(colorStop[position]);
this.colorStops.push({offset: position, color: color.toRgb(), opacity: color.getAlpha()});
}
return this;
},
/**
* Returns object representation of a gradient
* @return {Object}
*/
toObject: function() {
return {
type: this.type,
coords: this.coords,
gradientUnits: this.gradientUnits,
colorStops: this.colorStops
};
},
/**
* Returns an instance of CanvasGradient
* @param {CanvasRenderingContext2D} ctx Context to render on
* @return {CanvasGradient}
*/
toLive: function(ctx) {
var gradient;
if (!this.type) return;
if (this.type === 'linear') {
gradient = ctx.createLinearGradient(
this.coords.x1, this.coords.y1, this.coords.x2, this.coords.y2);
}
else if (this.type === 'radial') {
gradient = ctx.createRadialGradient(
this.coords.x1, this.coords.y1, this.coords.r1, this.coords.x2, this.coords.y2, this.coords.r2);
}
for (var i = 0, len = this.colorStops.length; i < len; i++) {
var color = this.colorStops[i].color,
opacity = this.colorStops[i].opacity,
offset = this.colorStops[i].offset;
if (typeof opacity !== 'undefined') {
color = new fabric.Color(color).setAlpha(opacity).toRgba();
}
gradient.addColorStop(parseFloat(offset), color);
}
return gradient;
}
});
fabric.util.object.extend(fabric.Gradient, {
/* _FROM_SVG_START_ */
/**
* Returns {@link fabric.Gradient} instance from an SVG element
* @static
* @memberof fabric.Gradient
* @param {SVGGradientElement} el SVG gradient element
* @param {fabric.Object} instance
* @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) {
/**
* @example:
*
* <linearGradient id="linearGrad1">
* <stop offset="0%" stop-color="white"/>
* <stop offset="100%" stop-color="black"/>
* </linearGradient>
*
* OR
*
* <linearGradient id="linearGrad2">
* <stop offset="0" style="stop-color:rgb(255,255,255)"/>
* <stop offset="1" style="stop-color:rgb(0,0,0)"/>
* </linearGradient>
*
* OR
*
* <radialGradient id="radialGrad1">
* <stop offset="0%" stop-color="white" stop-opacity="1" />
* <stop offset="50%" stop-color="black" stop-opacity="0.5" />
* <stop offset="100%" stop-color="white" stop-opacity="1" />
* </radialGradient>
*
* OR
*
* <radialGradient id="radialGrad2">
* <stop offset="0" stop-color="rgb(255,255,255)" />
* <stop offset="0.5" stop-color="rgb(0,0,0)" />
* <stop offset="1" stop-color="rgb(255,255,255)" />
* </radialGradient>
*
*/
var colorStopEls = el.getElementsByTagName('stop'),
type = (el.nodeName === 'linearGradient' ? 'linear' : 'radial'),
gradientUnits = el.getAttribute('gradientUnits') || 'objectBoundingBox',
colorStops = [],
coords = { };
if (type === 'linear') {
coords = {
x1: el.getAttribute('x1') || 0,
y1: el.getAttribute('y1') || 0,
x2: el.getAttribute('x2') || '100%',
y2: el.getAttribute('y2') || 0
};
}
else if (type === 'radial') {
coords = {
x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%',
y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%',
r1: 0,
x2: el.getAttribute('cx') || '50%',
y2: el.getAttribute('cy') || '50%',
r2: el.getAttribute('r') || '50%'
};
}
for (var i = colorStopEls.length; i--; ) {
colorStops.push(getColorStop(colorStopEls[i]));
}
_convertPercentUnitsToValues(instance, coords);
return new fabric.Gradient({
type: type,
coords: coords,
gradientUnits: gradientUnits,
colorStops: colorStops
});
},
/* _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);
return new fabric.Gradient(options);
}
});
/**
* @private
*/
function _convertPercentUnitsToValues(object, options) {
for (var prop in options) {
if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) {
var percents = parseFloat(options[prop], 10);
if (prop === 'x1' || prop === 'x2' || prop === 'r2') {
options[prop] = fabric.util.toFixed(object.width * percents / 100, 2);
}
else if (prop === 'y1' || prop === 'y2') {
options[prop] = fabric.util.toFixed(object.height * percents / 100, 2);
}
}
// normalize rendering point (should be from top/left corner rather than center of the shape)
if (prop === 'x1' || prop === 'x2') {
options[prop] -= fabric.util.toFixed(object.width / 2, 2);
}
else if (prop === 'y1' || prop === 'y2') {
options[prop] -= fabric.util.toFixed(object.height / 2, 2);
}
}
}
})();
/**
* Pattern class
* @class fabric.Pattern
*/
fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ {
/**
* Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat)
* @type String
* @default
*/
repeat: 'repeat',
/**
* Pattern horizontal offset from object's left/top corner
* @type Number
* @default
*/
offsetX: 0,
/**
* Pattern vertical offset from object's left/top corner
* @type Number
* @default
*/
offsetY: 0,
/**
* Constructor
* @param {Object} [options] Options object
* @return {fabric.Pattern} thisArg
*/
initialize: function(options) {
options || (options = { });
this.id = fabric.Object.__uid++;
if (options.source) {
if (typeof options.source === 'string') {
// function string
if (typeof fabric.util.getFunctionBody(options.source) !== 'undefined') {
this.source = new Function(fabric.util.getFunctionBody(options.source));
}
else {
// img src string
var _this = this;
this.source = fabric.util.createImage();
fabric.util.loadImage(options.source, function(img) {
_this.source = img;
});
}
}
else {
// img element
this.source = options.source;
}
}
if (options.repeat) {
this.repeat = options.repeat;
}
if (options.offsetX) {
this.offsetX = options.offsetX;
}
if (options.offsetY) {
this.offsetY = options.offsetY;
}
},
/**
* Returns object representation of a pattern
* @return {Object} Object representation of a pattern instance
*/
toObject: function() {
var source;
// callback
if (typeof this.source === 'function') {
source = String(this.source);
}
// <img> element
else if (typeof this.source.src === 'string') {
source = this.source.src;
}
return {
source: source,
repeat: this.repeat,
offsetX: this.offsetX,
offsetY: this.offsetY
};
},
/**
* Returns an instance of CanvasPattern
* @param {CanvasRenderingContext2D} ctx Context to create pattern
* @return {CanvasPattern}
*/
toLive: function(ctx) {
var source = typeof this.source === 'function' ? this.source() : this.source;
return ctx.createPattern(source, this.repeat);
}
});
(function(global) {
var fabric = global.fabric || (global.fabric = { });
if (fabric.Shadow) {
fabric.warn('fabric.Shadow is already defined.');
return;
}
/**
* Shadow class
* @class fabric.Shadow
*/
fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ {
/**
* Shadow color
* @type String
* @default
*/
color: 'rgb(0,0,0)',
/**
* Shadow blur
* @type Number
*/
blur: 0,
/**
* Shadow horizontal offset
* @type Number
* @default
*/
offsetX: 0,
/**
* Shadow vertical offset
* @type Number
* @default
*/
offsetY: 0,
/**
* Whether the shadow should affect stroke operations
* @type Boolean
* @default
*/
affectStroke: false,
/**
* Constructor
* @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)")
* @return {fabric.Shadow} thisArg
*/
initialize: function(options) {
if (typeof options === 'string') {
options = this._parseShadow(options);
}
for (var prop in options) {
this[prop] = options[prop];
}
this.id = fabric.Object.__uid++;
},
/**
* @private
* @param {String} shadow Shadow value to parse
* @return {Object} Shadow object with color, offsetX, offsetY and blur
*/
_parseShadow: function(shadow) {
var shadowStr = shadow.trim();
var offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [ ],
color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)';
return {
color: color.trim(),
offsetX: parseInt(offsetsAndBlur[1], 10) || 0,
offsetY: parseInt(offsetsAndBlur[2], 10) || 0,
blur: parseInt(offsetsAndBlur[3], 10) || 0
};
},
/**
* Returns a string representation of an instance
* @see http://www.w3.org/TR/css-text-decor-3/#text-shadow
* @return {String} Returns CSS3 text-shadow declaration
*/
toString: function() {
return [this.offsetX, this.offsetY, this.blur, this.color].join('px ');
},
/**
* Returns object representation of a shadow
* @return {Object} Object representation of a shadow instance
*/
toObject: function() {
return {
color: this.color,
blur: this.blur,
offsetX: this.offsetX,
offsetY: this.offsetY
};
}
});
/**
* Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px")
* @static
* @field
* @memberOf fabric.Shadow
*/
fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/;
})(typeof exports !== 'undefined' ? exports : this);
(function () {
if (fabric.StaticCanvas) {
fabric.warn('fabric.StaticCanvas is already defined.');
return;
}
// aliases for faster resolution
var extend = fabric.util.object.extend,
getElementOffset = fabric.util.getElementOffset,
removeFromArray = fabric.util.removeFromArray,
removeListener = fabric.util.removeListener,
CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element');
/**
* Static canvas class
* @class fabric.StaticCanvas
* @mixes fabric.Collection
* @mixes fabric.Observable
*/
fabric.StaticCanvas = fabric.util.createClass(/** @lends fabric.StaticCanvas.prototype */ {
/**
* Constructor
* @param {HTMLElement | String} el &lt;canvas> element to initialize instance on
* @param {Object} [options] Options object
* @return {Object} thisArg
*/
initialize: function(el, options) {
options || (options = { });
this._initStatic(el, options);
fabric.StaticCanvas.activeInstance = this;
},
/**
* Background color of canvas instance
* @type String
* @default
*/
backgroundColor: '',
/**
* Background image of canvas instance
* Should be set via {@link fabric.StaticCanvas#setBackgroundImage}
* @type String
* @default
*/
backgroundImage: '',
/**
* Opacity of the background image of the canvas instance
* @type Float
* @default
*/
backgroundImageOpacity: 1,
/**
* Indicates whether the background image should be stretched to fit the
* dimensions of the canvas instance.
* @type Boolean
* @default
*/
backgroundImageStretch: true,
/**
* Overlay image of canvas instance
* Should be set via {@link fabric.StaticCanvas#setOverlayImage}
* @type String
* @default
*/
overlayImage: '',
/**
* Left offset of overlay image (if present)
* @type Number
* @default
*/
overlayImageLeft: 0,
/**
* Top offset of overlay image (if present)
* @type Number
* @default
*/
overlayImageTop: 0,
/**
* Indicates whether toObject/toDatalessObject should include default values
* @type Boolean
* @default
*/
includeDefaultValues: true,
/**
* Indicates whether objects' state should be saved
* @type Boolean
* @default
*/
stateful: true,
/**
* Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove} should also re-render canvas.
* Disabling this option could give a great performance boost when adding/removing a lot of objects to/from canvas at once
* (followed by a manual rendering after addition/deletion)
* @type Boolean
* @default
*/
renderOnAddRemove: true,
/**
* Function that determines clipping of entire canvas area
* Being passed context as first argument. See clipping canvas area in {@link https://github.com/kangax/fabric.js/wiki/FAQ}
* @type Function
* @default
*/
clipTo: null,
/**
* Indicates whether object controls (borders/controls) are rendered above overlay image
* @type Boolean
* @default
*/
controlsAboveOverlay: false,
/**
* Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas
* @type Boolean
* @default
*/
allowTouchScrolling: false,
/**
* Callback; invoked right before object is about to be scaled/rotated
* @param {fabric.Object} target Object that's about to be scaled/rotated
*/
onBeforeScaleRotate: function () {
/* NOOP */
},
/**
* @private
* @param {HTMLElement | String} el &lt;canvas> element to initialize instance on
* @param {Object} [options] Options object
*/
_initStatic: function(el, options) {
this._objects = [];
this._createLowerCanvas(el);
this._initOptions(options);
if (options.overlayImage) {
this.setOverlayImage(options.overlayImage, this.renderAll.bind(this));
}
if (options.backgroundImage) {
this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this));
}
if (options.backgroundColor) {
this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this));
}
this.calcOffset();
},
/**
* Calculates canvas element offset relative to the document
* This method is also attached as "resize" event handler of window
* @return {fabric.Canvas} instance
* @chainable
*/
calcOffset: function () {
this._offset = getElementOffset(this.lowerCanvasEl);
return this;
},
/**
* Sets overlay image for this canvas
* @param {String} url url of an image to set overlay to
* @param {Function} callback callback to invoke when image is loaded and set as an overlay
* @param {Object} [options] Optional options to set for the overlay image
* @param {Number} [options.overlayImageLeft] Left offset of overlay image
* @param {Number} [options.overlayImageTop] Top offset of overlay image
* @return {fabric.Canvas} thisArg
* @chainable
*/
setOverlayImage: function (url, callback, options) { // TODO (kangax): test callback
fabric.util.loadImage(url, function(img) {
this.overlayImage = img;
if (options && ('overlayImageLeft' in options)) {
this.overlayImageLeft = options.overlayImageLeft;
}
if (options && ('overlayImageTop' in options)) {
this.overlayImageTop = options.overlayImageTop;
}
callback && callback();
}, this);
return this;
},
/**
* Sets background image for this canvas
* @param {String} url url of an image to set background to
* @param {Function} callback callback to invoke when image is loaded and set as background
* @param {Object} [options] Optional options to set for the background image
* @param {Float} [options.backgroundImageOpacity] Opacity of the background image of the canvas instance
* @param {Boolean} [options.backgroundImageStretch] Indicates whether the background image should be stretched to fit the canvas
* @return {fabric.Canvas} thisArg
* @chainable
*/
setBackgroundImage: function (url, callback, options) {
fabric.util.loadImage(url, function(img) {
this.backgroundImage = img;
if (options && ('backgroundImageOpacity' in options)) {
this.backgroundImageOpacity = options.backgroundImageOpacity;
}
if (options && ('backgroundImageStretch' in options)) {
this.backgroundImageStretch = options.backgroundImageStretch;
}
callback && callback();
}, this);
return this;
},
/**
* Sets background color for this canvas
* @param {String|fabric.Pattern} backgroundColor Color of pattern to set background color to
* @param {Function} callback callback to invoke when background color is set
* @return {fabric.Canvas} thisArg
* @chainable
*/
setBackgroundColor: function(backgroundColor, callback) {
if (backgroundColor.source) {
var _this = this;
fabric.util.loadImage(backgroundColor.source, function(img) {
_this.backgroundColor = new fabric.Pattern({
source: img,
repeat: backgroundColor.repeat
});
callback && callback();
});
}
else {
this.backgroundColor = backgroundColor;
callback && callback();
}
return this;
},
/**
* @private
*/
_createCanvasElement: function() {
var element = fabric.document.createElement('canvas');
if (!element.style) {
element.style = { };
}
if (!element) {
throw CANVAS_INIT_ERROR;
}
this._initCanvasElement(element);
return element;
},
/**
* @private
* @param {HTMLElement} element
*/
_initCanvasElement: function(element) {
fabric.util.createCanvasElement(element);
if (typeof element.getContext === 'undefined') {
throw CANVAS_INIT_ERROR;
}
},
/**
* @private
* @param {Object} [options] Options object
*/
_initOptions: function (options) {
for (var prop in options) {
this[prop] = options[prop];
}
this.width = parseInt(this.lowerCanvasEl.width, 10) || 0;
this.height = parseInt(this.lowerCanvasEl.height, 10) || 0;
if (!this.lowerCanvasEl.style) return;
this.lowerCanvasEl.style.width = this.width + 'px';
this.lowerCanvasEl.style.height = this.height + 'px';
},
/**
* Creates a bottom canvas
* @private
* @param {HTMLElement} [canvasEl]
*/
_createLowerCanvas: function (canvasEl) {
this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement();
this._initCanvasElement(this.lowerCanvasEl);
fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas');
if (this.interactive) {
this._applyCanvasStyle(this.lowerCanvasEl);
}
this.contextContainer = this.lowerCanvasEl.getContext('2d');
},
/**
* Returns canvas width (in px)
* @return {Number}
*/
getWidth: function () {
return this.width;
},
/**
* Returns canvas height (in px)
* @return {Number}
*/
getHeight: function () {
return this.height;
},
/**
* Sets width of this canvas instance
* @param {Number} width value to set width to
* @return {fabric.Canvas} instance
* @chainable true
*/
setWidth: function (value) {
return this._setDimension('width', value);
},
/**
* Sets height of this canvas instance
* @param {Number} height value to set height to
* @return {fabric.Canvas} instance
* @chainable true
*/
setHeight: function (value) {
return this._setDimension('height', value);
},
/**
* Sets dimensions (width, height) of this canvas instance
* @param {Object} dimensions Object with width/height properties
* @param {Number} [dimensions.width] Width of canvas element
* @param {Number} [dimensions.height] Height of canvas element
* @return {fabric.Canvas} thisArg
* @chainable
*/
setDimensions: function(dimensions) {
for (var prop in dimensions) {
this._setDimension(prop, dimensions[prop]);
}
return this;
},
/**
* Helper for setting width/height
* @private
* @param {String} prop property (width|height)
* @param {Number} value value to set property to
* @return {fabric.Canvas} instance
* @chainable true
*/
_setDimension: function (prop, value) {
this.lowerCanvasEl[prop] = value;
this.lowerCanvasEl.style[prop] = value + 'px';
if (this.upperCanvasEl) {
this.upperCanvasEl[prop] = value;
this.upperCanvasEl.style[prop] = value + 'px';
}
if (this.cacheCanvasEl) {
this.cacheCanvasEl[prop] = value;
}
if (this.wrapperEl) {
this.wrapperEl.style[prop] = value + 'px';
}
this[prop] = value;
this.calcOffset();
this.renderAll();
return this;
},
/**
* Returns &lt;canvas> element corresponding to this instance
* @return {HTMLCanvasElement}
*/
getElement: function () {
return this.lowerCanvasEl;
},
/**
* Returns currently selected object, if any
* @return {fabric.Object}
*/
getActiveObject: function() {
return null;
},
/**
* Returns currently selected group of object, if any
* @return {fabric.Group}
*/
getActiveGroup: function() {
return null;
},
/**
* Given a context, renders an object on that context
* @param {CanvasRenderingContext2D} ctx Context to render object on
* @param {fabric.Object} object Object to render
* @private
*/
_draw: function (ctx, object) {
if (!object) return;
if (this.controlsAboveOverlay) {
var hasBorders = object.hasBorders, hasControls = object.hasControls;
object.hasBorders = object.hasControls = false;
object.render(ctx);
object.hasBorders = hasBorders;
object.hasControls = hasControls;
}
else {
object.render(ctx);
}
},
/**
* @private
* @param {fabric.Object} obj Object that was added
*/
_onObjectAdded: function(obj) {
this.stateful && obj.setupState();
obj.setCoords();
obj.canvas = this;
this.fire('object:added', { target: obj });
obj.fire('added');
},
/**
* @private
* @param {fabric.Object} obj Object that was removed
*/
_onObjectRemoved: function(obj) {
this.fire('object:removed', { target: obj });
obj.fire('removed');
},
/**
* Returns an array of objects this instance has
* @return {Array}
*/
getObjects: function () {
return this._objects;
},
/**
* Clears specified context of canvas element
* @param {CanvasRenderingContext2D} ctx Context to clear
* @return {fabric.Canvas} thisArg
* @chainable
*/
clearContext: function(ctx) {
ctx.clearRect(0, 0, this.width, this.height);
return this;
},
/**
* Returns context of canvas where objects are drawn
* @return {CanvasRenderingContext2D}
*/
getContext: function () {
return this.contextContainer;
},
/**
* Clears all contexts (background, main, top) of an instance
* @return {fabric.Canvas} thisArg
* @chainable
*/
clear: function () {
this._objects.length = 0;
if (this.discardActiveGroup) {
this.discardActiveGroup();
}
if (this.discardActiveObject) {
this.discardActiveObject();
}
this.clearContext(this.contextContainer);
if (this.contextTop) {
this.clearContext(this.contextTop);
}
this.fire('canvas:cleared');
this.renderAll();
return this;
},
/**
* Renders both the top canvas and the secondary container canvas.
* @param {Boolean} [allOnTop] Whether we want to force all images to be rendered on the top canvas
* @return {fabric.Canvas} instance
* @chainable
*/
renderAll: function (allOnTop) {
var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer'];
if (this.contextTop && this.selection && !this._groupSelector) {
this.clearContext(this.contextTop);
}
if (!allOnTop) {
this.clearContext(canvasToDrawOn);
}
this.fire('before:render');
if (this.clipTo) {
fabric.util.clipContext(this, canvasToDrawOn);
}
if (this.backgroundColor) {
canvasToDrawOn.fillStyle = this.backgroundColor.toLive
? this.backgroundColor.toLive(canvasToDrawOn)
: this.backgroundColor;
canvasToDrawOn.fillRect(
this.backgroundColor.offsetX || 0,
this.backgroundColor.offsetY || 0,
this.width,
this.height);
}
if (typeof this.backgroundImage === 'object') {
this._drawBackroundImage(canvasToDrawOn);
}
var activeGroup = this.getActiveGroup();
for (var i = 0, length = this._objects.length; i < length; ++i) {
if (!activeGroup ||
(activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) {
this._draw(canvasToDrawOn, this._objects[i]);
}
}
// delegate rendering to group selection (if one exists)
if (activeGroup) {
//Store objects in group preserving order, then replace
var sortedObjects = [];
this.forEachObject(function (object) {
if (activeGroup.contains(object)) {
sortedObjects.push(object);
}
});
activeGroup._set('objects', sortedObjects);
this._draw(canvasToDrawOn, activeGroup);
}
if (this.clipTo) {
canvasToDrawOn.restore();
}
if (this.overlayImage) {
canvasToDrawOn.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop);
}
if (this.controlsAboveOverlay && this.interactive) {
this.drawControls(canvasToDrawOn);
}
this.fire('after:render');
return this;
},
/**
* @private
* @param {CanvasRenderingContext2D} canvasToDrawOn Context to render on
*/
_drawBackroundImage: function(canvasToDrawOn) {
canvasToDrawOn.save();
canvasToDrawOn.globalAlpha = this.backgroundImageOpacity;
if (this.backgroundImageStretch) {
canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height);
}
else {
canvasToDrawOn.drawImage(this.backgroundImage, 0, 0);
}
canvasToDrawOn.restore();
},
/**
* Method to render only the top canvas.
* Also used to render the group selection box.
* @return {fabric.Canvas} thisArg
* @chainable
*/
renderTop: function () {
var ctx = this.contextTop || this.contextContainer;
this.clearContext(ctx);
// we render the top context - last object
if (this.selection && this._groupSelector) {
this._drawSelection();
}
// delegate rendering to group selection if one exists
// used for drawing selection borders/controls
var activeGroup = this.getActiveGroup();
if (activeGroup) {
activeGroup.render(ctx);
}
if (this.overlayImage) {
ctx.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop);
}
this.fire('after:render');
return this;
},
/**
* Returns coordinates of a center of canvas.
* Returned value is an object with top and left properties
* @return {Object} object with "top" and "left" number values
*/
getCenter: function () {
return {
top: this.getHeight() / 2,
left: this.getWidth() / 2
};
},
/**
* Centers object horizontally.
* You might need to call `setCoords` on an object after centering, to update controls area.
* @param {fabric.Object} object Object to center
* @return {fabric.Canvas} thisArg
*/
centerObjectH: function (object) {
object.set('left', this.getCenter().left);
this.renderAll();
return this;
},
/**
* Centers object vertically.
* You might need to call `setCoords` on an object after centering, to update controls area.
* @param {fabric.Object} object Object to center
* @return {fabric.Canvas} thisArg
* @chainable
*/
centerObjectV: function (object) {
object.set('top', this.getCenter().top);
this.renderAll();
return this;
},
/**
* Centers object vertically and horizontally.
* You might need to call `setCoords` on an object after centering, to update controls area.
* @param {fabric.Object} object Object to center
* @return {fabric.Canvas} thisArg
* @chainable
*/
centerObject: function (object) {
return this.centerObjectH(object).centerObjectV(object);
},
/**
* Returs dataless JSON representation of canvas
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {String} json string
*/
toDatalessJSON: function (propertiesToInclude) {
return this.toDatalessObject(propertiesToInclude);
},
/**
* Returns object representation of canvas
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toObject: function (propertiesToInclude) {
return this._toObjectMethod('toObject', propertiesToInclude);
},
/**
* Returns dataless object representation of canvas
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toDatalessObject: function (propertiesToInclude) {
return this._toObjectMethod('toDatalessObject', propertiesToInclude);
},
/**
* @private
*/
_toObjectMethod: function (methodName, propertiesToInclude) {
var activeGroup = this.getActiveGroup();
if (activeGroup) {
this.discardActiveGroup();
}
var data = {
objects: this.getObjects().map(function (instance) {
// TODO (kangax): figure out how to clean this up
var originalValue;
if (!this.includeDefaultValues) {
originalValue = instance.includeDefaultValues;
instance.includeDefaultValues = false;
}
var object = instance[methodName](propertiesToInclude);
if (!this.includeDefaultValues) {
instance.includeDefaultValues = originalValue;
}
return object;
}, this),
background: (this.backgroundColor && this.backgroundColor.toObject)
? this.backgroundColor.toObject()
: this.backgroundColor
};
if (this.backgroundImage) {
data.backgroundImage = this.backgroundImage.src;
data.backgroundImageOpacity = this.backgroundImageOpacity;
data.backgroundImageStretch = this.backgroundImageStretch;
}
if (this.overlayImage) {
data.overlayImage = this.overlayImage.src;
data.overlayImageLeft = this.overlayImageLeft;
data.overlayImageTop = this.overlayImageTop;
}
fabric.util.populateWithProperties(this, data, propertiesToInclude);
if (activeGroup) {
this.setActiveGroup(new fabric.Group(activeGroup.getObjects()));
activeGroup.forEachObject(function(o) { o.set('active', true) });
}
return data;
},
/**
* Removes an object from canvas and returns it
* @param {fabric.Object} object Object to remove
* @return {fabric.Object} removed object
*/
remove: function (object) {
// removing active object should fire "selection:cleared" events
if (this.getActiveObject() === object) {
this.fire('before:selection:cleared', { target: object });
this.discardActiveObject();
this.fire('selection:cleared');
}
return fabric.Collection.remove.call(this, object);
},
/**
* Moves an object to the bottom of the stack of drawn objects
* @param {fabric.Object} object Object to send to back
* @return {fabric.Canvas} thisArg
* @chainable
*/
sendToBack: function (object) {
removeFromArray(this._objects, object);
this._objects.unshift(object);
return this.renderAll && this.renderAll();
},
/**
* Moves an object to the top of the stack of drawn objects
* @param {fabric.Object} object Object to send
* @return {fabric.Canvas} thisArg
* @chainable
*/
bringToFront: function (object) {
removeFromArray(this._objects, object);
this._objects.push(object);
return this.renderAll && this.renderAll();
},
/**
* Moves an object down in stack of drawn objects
* @param {fabric.Object} object Object to send
* @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object
* @return {fabric.Canvas} thisArg
* @chainable
*/
sendBackwards: function (object, intersecting) {
var idx = this._objects.indexOf(object);
// if object is not on the bottom of stack
if (idx !== 0) {
var newIdx;
if (intersecting) {
newIdx = idx;
// traverse down the stack looking for the nearest intersecting object
for (var i=idx-1; i>=0; --i) {
var isIntersecting = object.intersectsWithObject(this._objects[i]) ||
object.isContainedWithinObject(this._objects[i]) ||
this._objects[i].isContainedWithinObject(object);
if (isIntersecting) {
newIdx = i;
break;
}
}
}
else {
newIdx = idx-1;
}
removeFromArray(this._objects, object);
this._objects.splice(newIdx, 0, object);
this.renderAll && this.renderAll();
}
return this;
},
/**
* Moves an object up in stack of drawn objects
* @param {fabric.Object} object Object to send
* @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object
* @return {fabric.Canvas} thisArg
* @chainable
*/
bringForward: function (object, intersecting) {
var idx = this._objects.indexOf(object);
// if object is not on top of stack (last item in an array)
if (idx !== this._objects.length-1) {
var newIdx;
if (intersecting) {
newIdx = idx;
// traverse up the stack looking for the nearest intersecting object
for (var i = idx + 1; i < this._objects.length; ++i) {
var isIntersecting = object.intersectsWithObject(this._objects[i]) ||
object.isContainedWithinObject(this._objects[i]) ||
this._objects[i].isContainedWithinObject(object);
if (isIntersecting) {
newIdx = i;
break;
}
}
}
else {
newIdx = idx+1;
}
removeFromArray(this._objects, object);
this._objects.splice(newIdx, 0, object);
this.renderAll && this.renderAll();
}
return this;
},
/**
* Moves an object to specified level in stack of drawn objects
* @param {fabric.Object} object Object to send
* @param {Number} index Position to move to
* @return {fabric.Canvas} thisArg
* @chainable
*/
moveTo: function (object, index) {
removeFromArray(this._objects, object);
this._objects.splice(index, 0, object);
return this.renderAll && this.renderAll();
},
/**
* Clears a canvas element and removes all event handlers.
* @return {fabric.Canvas} thisArg
* @chainable
*/
dispose: function () {
this.clear();
if (!this.interactive) return this;
if (fabric.isTouchSupported) {
removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
if (typeof Event !== 'undefined' && 'remove' in Event) {
Event.remove(this.upperCanvasEl, 'gesture', this._onGesture);
}
}
else {
removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
removeListener(fabric.window, 'resize', this._onResize);
}
return this;
},
/**
* Returns a string representation of an instance
* @return {String} string representation of an instance
*/
toString: function () {
return '#<fabric.Canvas (' + this.complexity() + '): '+
'{ objects: ' + this.getObjects().length + ' }>';
}
});
extend(fabric.StaticCanvas.prototype, fabric.Observable);
extend(fabric.StaticCanvas.prototype, fabric.Collection);
extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter);
extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ {
/**
* @static
* @type String
* @default
*/
EMPTY_JSON: '{"objects": [], "background": "white"}',
/**
* Takes &lt;canvas> element and transforms its data in such way that it becomes grayscale
* @static
* @param {HTMLCanvasElement} canvasEl
*/
toGrayscale: function (canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
iLen = imageData.width,
jLen = imageData.height,
index, average, i, j;
for (i = 0; i < iLen; i++) {
for (j = 0; j < jLen; j++) {
index = (i * 4) * jLen + (j * 4);
average = (data[index] + data[index + 1] + data[index + 2]) / 3;
data[index] = average;
data[index + 1] = average;
data[index + 2] = average;
}
}
context.putImageData(imageData, 0, 0);
},
/**
* Provides a way to check support of some of the canvas methods
* (either those of HTMLCanvasElement itself, or rendering context)
*
* @param methodName {String} Method to check support for;
* Could be one of "getImageData", "toDataURL", "toDataURLWithQuality" or "setLineDash"
* @return {Boolean | null} `true` if method is supported (or at least exists),
* `null` if canvas element or context can not be initialized
*/
supports: function (methodName) {
var el = fabric.util.createCanvasElement();
if (!el || !el.getContext) {
return null;
}
var ctx = el.getContext('2d');
if (!ctx) {
return null;
}
switch (methodName) {
case 'getImageData':
return typeof ctx.getImageData !== 'undefined';
case 'setLineDash':
return typeof ctx.setLineDash !== 'undefined';
case 'toDataURL':
return typeof el.toDataURL !== 'undefined';
case 'toDataURLWithQuality':
try {
el.toDataURL('image/jpeg', 0);
return true;
}
catch (e) { }
return false;
default:
return null;
}
}
});
/**
* Returs JSON representation of canvas
* @function
* @param {Array} propertiesToInclude Any properties that you might want to additionally include in the output
* @return {String} json string
*/
fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject;
})();
/**
* BaseBrush class
* @class fabric.BaseBrush
*/
fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ {
/**
* Color of a brush
* @type String
* @default
*/
color: 'rgb(0, 0, 0)',
/**
* Width of a brush
* @type Number
* @default
*/
width: 1,
/**
* Shadow object representing shadow of this shape.
* <b>Backwards incompatibility note:</b> This property replaces "shadowColor" (String), "shadowOffsetX" (Number),
* "shadowOffsetY" (Number) and "shadowBlur" (Number) since v1.2.12
* @type fabric.Shadow
* @default
*/
shadow: null,
/**
* Line endings style of a brush (one of "butt", "round", "square")
* @type String
* @default
*/
strokeLineCap: 'round',
/**
* Corner style of a brush (one of "bevil", "round", "miter")
* @type String
* @default
*/
strokeLineJoin: 'round',
/**
* Sets shadow of an object
* @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)")
* @return {fabric.Object} thisArg
* @chainable
*/
setShadow: function(options) {
this.shadow = new fabric.Shadow(options);
return this;
},
/**
* Sets brush styles
* @private
*/
_setBrushStyles: function() {
var ctx = this.canvas.contextTop;
ctx.strokeStyle = this.color;
ctx.lineWidth = this.width;
ctx.lineCap = this.strokeLineCap;
ctx.lineJoin = this.strokeLineJoin;
},
/**
* Sets brush shadow styles
* @private
*/
_setShadow: function() {
if (!this.shadow) return;
var ctx = this.canvas.contextTop;
ctx.shadowColor = this.shadow.color;
ctx.shadowBlur = this.shadow.blur;
ctx.shadowOffsetX = this.shadow.offsetX;
ctx.shadowOffsetY = this.shadow.offsetY;
},
/**
* Removes brush shadow styles
* @private
*/
_resetShadow: function() {
var ctx = this.canvas.contextTop;
ctx.shadowColor = '';
ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
}
});
(function() {
var utilMin = fabric.util.array.min,
utilMax = fabric.util.array.max;
/**
* PencilBrush class
* @class fabric.PencilBrush
* @extends fabric.BaseBrush
*/
fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ {
/**
* Constructor
* @param {fabric.Canvas} canvas
* @return {fabric.PencilBrush} Instance of a pencil brush
*/
initialize: function(canvas) {
this.canvas = canvas;
this._points = [ ];
},
/**
* Inovoked on mouse down
* @param {Object} pointer
*/
onMouseDown: function(pointer) {
this._prepareForDrawing(pointer);
// capture coordinates immediately
// this allows to draw dots (when movement never occurs)
this._captureDrawingPath(pointer);
this._render();
},
/**
* Inovoked on mouse move
* @param {Object} pointer
*/
onMouseMove: function(pointer) {
this._captureDrawingPath(pointer);
// redraw curve
// clear top canvas
this.canvas.clearContext(this.canvas.contextTop);
this._render();
},
/**
* Invoked on mouse up
*/
onMouseUp: function() {
this._finalizeAndAddPath();
},
/**
* @param {Object} pointer
*/
_prepareForDrawing: function(pointer) {
var p = new fabric.Point(pointer.x, pointer.y);
this._reset();
this._addPoint(p);
this.canvas.contextTop.moveTo(p.x, p.y);
},
/**
* @private
* @param {fabric.Point} point
*/
_addPoint: function(point) {
this._points.push(point);
},
/**
* Clear points array and set contextTop canvas
* style.
*
* @private
*
*/
_reset: function() {
this._points.length = 0;
this._setBrushStyles();
this._setShadow();
},
/**
* @private
*
* @param point {pointer} (fabric.util.pointer) actual mouse position
* related to the canvas.
*/
_captureDrawingPath: function(pointer) {
var pointerPoint = new fabric.Point(pointer.x, pointer.y);
this._addPoint(pointerPoint);
},
/**
* Draw a smooth path on the topCanvas using quadraticCurveTo
*
* @private
*/
_render: function() {
var ctx = this.canvas.contextTop;
ctx.beginPath();
var p1 = this._points[0];
var p2 = this._points[1];
//if we only have 2 points in the path and they are the same
//it means that the user only clicked the canvas without moving the mouse
//then we should be drawing a dot. A path isn't drawn between two identical dots
//that's why we set them apart a bit
if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) {
p1.x -= 0.5;
p2.x += 0.5;
}
ctx.moveTo(p1.x, p1.y);
for (var i = 1, len = this._points.length; i < len; i++) {
// we pick the point between pi+1 & pi+2 as the
// end point and p1 as our control point.
var midPoint = p1.midPointFrom(p2);
ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
p1 = this._points[i];
p2 = this._points[i+1];
}
// Draw last line as a straight line while
// we wait for the next point to be able to calculate
// the bezier control point
ctx.lineTo(p1.x, p1.y);
ctx.stroke();
},
/**
* Return an SVG path based on our captured points and their bounding box
*
* @private
*/
_getSVGPathData: function() {
this.box = this.getPathBoundingBox(this._points);
return this.convertPointsToSVGPath(
this._points, this.box.minx, this.box.maxx, this.box.miny, this.box.maxy);
},
/**
* Returns bounding box of a path based on given points
* @param {Array} points
* @return {Object} object with minx, miny, maxx, maxy
*/
getPathBoundingBox: function(points) {
var xBounds = [],
yBounds = [],
p1 = points[0],
p2 = points[1],
startPoint = p1;
for (var i = 1, len = points.length; i < len; i++) {
var midPoint = p1.midPointFrom(p2);
// with startPoint, p1 as control point, midpoint as end point
xBounds.push(startPoint.x);
xBounds.push(midPoint.x);
yBounds.push(startPoint.y);
yBounds.push(midPoint.y);
p1 = points[i];
p2 = points[i+1];
startPoint = midPoint;
} // end for
xBounds.push(p1.x);
yBounds.push(p1.y);
return {
minx: utilMin(xBounds),
miny: utilMin(yBounds),
maxx: utilMax(xBounds),
maxy: utilMax(yBounds)
};
},
/**
* Converts points to SVG path
* @param {Array} points Array of points
* @return {String} SVG path
*/
convertPointsToSVGPath: function(points, minX, maxX, minY) {
var path = [];
var p1 = new fabric.Point(points[0].x - minX, points[0].y - minY);
var p2 = new fabric.Point(points[1].x - minX, points[1].y - minY);
path.push('M ', points[0].x - minX, ' ', points[0].y - minY, ' ');
for (var i = 1, len = points.length; i < len; i++) {
var midPoint = p1.midPointFrom(p2);
// p1 is our bezier control point
// midpoint is our endpoint
// start point is p(i-1) value.
path.push('Q ', p1.x, ' ', p1.y, ' ', midPoint.x, ' ', midPoint.y, ' ');
p1 = new fabric.Point(points[i].x - minX, points[i].y - minY);
if ((i+1) < points.length) {
p2 = new fabric.Point(points[i+1].x - minX, points[i+1].y - minY);
}
}
path.push('L ', p1.x, ' ', p1.y, ' ');
return path;
},
/**
* Creates fabric.Path object to add on canvas
* @param {String} pathData Path data
* @return {fabric.Path} path to add on canvas
*/
createPath: function(pathData) {
var path = new fabric.Path(pathData);
path.fill = null;
path.stroke = this.color;
path.strokeWidth = this.width;
path.strokeLineCap = this.strokeLineCap;
path.strokeLineJoin = this.strokeLineJoin;
if (this.shadow) {
this.shadow.affectStroke = true;
path.setShadow(this.shadow);
}
return path;
},
/**
* On mouseup after drawing the path on contextTop canvas
* we use the points captured to create an new fabric path object
* and add it to the fabric canvas.
*
*/
_finalizeAndAddPath: function() {
var ctx = this.canvas.contextTop;
ctx.closePath();
var pathData = this._getSVGPathData().join('');
if (pathData === "M 0 0 Q 0 0 0 0 L 0 0") {
// do not create 0 width/height paths, as they are
// rendered inconsistently across browsers
// Firefox 4, for example, renders a dot,
// whereas Chrome 10 renders nothing
this.canvas.renderAll();
return;
}
// set path origin coordinates based on our bounding box
var originLeft = this.box.minx + (this.box.maxx - this.box.minx) /2;
var originTop = this.box.miny + (this.box.maxy - this.box.miny) /2;
this.canvas.contextTop.arc(originLeft, originTop, 3, 0, Math.PI * 2, false);
var path = this.createPath(pathData);
path.set({ left: originLeft, top: originTop });
this.canvas.add(path);
path.setCoords();
this.canvas.clearContext(this.canvas.contextTop);
this._resetShadow();
this.canvas.renderAll();
// fire event 'path' created
this.canvas.fire('path:created', { path: path });
}
});
})();
/**
* CircleBrush class
* @class fabric.CircleBrush
*/
fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ {
/**
* Width of a brush
* @type Number
* @default
*/
width: 10,
/**
* Constructor
* @param {fabric.Canvas} canvas
* @return {fabric.CircleBrush} Instance of a circle brush
*/
initialize: function(canvas) {
this.canvas = canvas;
this.points = [ ];
},
/**
* Invoked inside on mouse down and mouse move
* @param {Object} pointer
*/
drawDot: function(pointer) {
var point = this.addPoint(pointer);
var ctx = this.canvas.contextTop;
ctx.fillStyle = point.fill;
ctx.beginPath();
ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false);
ctx.closePath();
ctx.fill();
},
/**
* Invoked on mouse down
*/
onMouseDown: function(pointer) {
this.points.length = 0;
this.canvas.clearContext(this.canvas.contextTop);
this._setShadow();
this.drawDot(pointer);
},
/**
* Invoked on mouse move
* @param {Object} pointer
*/
onMouseMove: function(pointer) {
this.drawDot(pointer);
},
/**
* Invoked on mouse up
*/
onMouseUp: function() {
var originalRenderOnAddRemove = this.canvas.renderOnAddRemove;
this.canvas.renderOnAddRemove = false;
var circles = [ ];
for (var i = 0, len = this.points.length; i < len; i++) {
var point = this.points[i];
var circle = new fabric.Circle({
radius: point.radius,
left: point.x,
top: point.y,
fill: point.fill
});
this.shadow && circle.setShadow(this.shadow);
circles.push(circle);
}
var group = new fabric.Group(circles);
this.canvas.add(group);
this.canvas.fire('path:created', { path: group });
this.canvas.clearContext(this.canvas.contextTop);
this._resetShadow();
this.canvas.renderOnAddRemove = originalRenderOnAddRemove;
this.canvas.renderAll();
},
/**
* @param {Object} pointer
* @return {fabric.Point} Just added pointer point
*/
addPoint: function(pointer) {
var pointerPoint = new fabric.Point(pointer.x, pointer.y);
var circleRadius = fabric.util.getRandomInt(
Math.max(0, this.width - 20), this.width + 20) / 2;
var circleColor = new fabric.Color(this.color)
.setAlpha(fabric.util.getRandomInt(0, 100) / 100)
.toRgba();
pointerPoint.radius = circleRadius;
pointerPoint.fill = circleColor;
this.points.push(pointerPoint);
return pointerPoint;
}
});
/**
* SprayBrush class
* @class fabric.SprayBrush
*/
fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ {
/**
* Width of a spray
* @type Number
* @default
*/
width: 10,
/**
* Density of a spray (number of dots per chunk)
* @type Number
* @default
*/
density: 20,
/**
* Width of spray dots
* @type Number
* @default
*/
dotWidth: 1,
/**
* Width variance of spray dots
* @type Number
* @default
*/
dotWidthVariance: 1,
/**
* Whether opacity of a dot should be random
* @type Boolean
* @default
*/
randomOpacity: false,
/**
* Constructor
* @param {fabric.Canvas} canvas
* @return {fabric.SprayBrush} Instance of a spray brush
*/
initialize: function(canvas) {
this.canvas = canvas;
this.sprayChunks = [ ];
},
/**
* Invoked on mouse down
* @param {Object} pointer
*/
onMouseDown: function(pointer) {
this.sprayChunks.length = 0;
this.canvas.clearContext(this.canvas.contextTop);
this._setShadow();
this.addSprayChunk(pointer);
this.render();
},
/**
* Invoked on mouse move
* @param {Object} pointer
*/
onMouseMove: function(pointer) {
this.addSprayChunk(pointer);
this.render();
},
/**
* Invoked on mouse up
*/
onMouseUp: function() {
var originalRenderOnAddRemove = this.canvas.renderOnAddRemove;
this.canvas.renderOnAddRemove = false;
var rects = [ ];
for (var i = 0, ilen = this.sprayChunks.length; i < ilen; i++) {
var sprayChunk = this.sprayChunks[i];
for (var j = 0, jlen = sprayChunk.length; j < jlen; j++) {
var rect = new fabric.Rect({
width: sprayChunk[j].width,
height: sprayChunk[j].width,
left: sprayChunk[j].x + 1,
top: sprayChunk[j].y + 1,
fill: this.color
});
this.shadow && rect.setShadow(this.shadow);
rects.push(rect);
}
}
var group = new fabric.Group(rects);
this.canvas.add(group);
this.canvas.fire('path:created', { path: group });
this.canvas.clearContext(this.canvas.contextTop);
this._resetShadow();
this.canvas.renderOnAddRemove = originalRenderOnAddRemove;
this.canvas.renderAll();
},
/**
* Renders brush
*/
render: function() {
var ctx = this.canvas.contextTop;
ctx.fillStyle = this.color;
ctx.save();
for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) {
var point = this.sprayChunkPoints[i];
if (typeof point.opacity !== 'undefined') {
ctx.globalAlpha = point.opacity;
}
ctx.fillRect(point.x, point.y, point.width, point.width);
}
ctx.restore();
},
/**
* @param {Object} pointer
*/
addSprayChunk: function(pointer) {
this.sprayChunkPoints = [ ];
var x, y, width, radius = this.width / 2;
for (var i = 0; i < this.density; i++) {
x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius);
y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius);
if (this.dotWidthVariance) {
width = fabric.util.getRandomInt(
// bottom clamp width to 1
Math.max(1, this.dotWidth - this.dotWidthVariance),
this.dotWidth + this.dotWidthVariance);
}
else {
width = this.dotWidth;
}
var point = { x: x, y: y, width: width };
if (this.randomOpacity) {
point.opacity = fabric.util.getRandomInt(0, 100) / 100;
}
this.sprayChunkPoints.push(point);
}
this.sprayChunks.push(this.sprayChunkPoints);
}
});
/**
* PatternBrush class
* @class fabric.PatternBrush
* @extends fabric.BaseBrush
*/
fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ {
getPatternSrc: function() {
var dotWidth = 20,
dotDistance = 5,
patternCanvas = fabric.document.createElement('canvas'),
patternCtx = patternCanvas.getContext('2d');
patternCanvas.width = patternCanvas.height = dotWidth + dotDistance;
patternCtx.fillStyle = this.color;
patternCtx.beginPath();
patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false);
patternCtx.closePath();
patternCtx.fill();
return patternCanvas;
},
getPatternSrcFunction: function() {
return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"');
},
/**
* Creates "pattern" instance property
*/
getPattern: function() {
return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat');
},
/**
* Sets brush styles
*/
_setBrushStyles: function() {
this.callSuper('_setBrushStyles');
this.canvas.contextTop.strokeStyle = this.getPattern();
},
/**
* Creates path
*/
createPath: function(pathData) {
var path = this.callSuper('createPath', pathData);
path.stroke = new fabric.Pattern({
source: this.source || this.getPatternSrcFunction()
});
return path;
}
});
(function() {
var getPointer = fabric.util.getPointer,
degreesToRadians = fabric.util.degreesToRadians,
radiansToDegrees = fabric.util.radiansToDegrees,
atan2 = Math.atan2,
abs = Math.abs,
min = Math.min,
max = Math.max,
STROKE_OFFSET = 0.5;
/**
* Canvas class
* @class fabric.Canvas
* @extends fabric.StaticCanvas
*/
fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ {
/**
* Constructor
* @param {HTMLElement | String} el &lt;canvas> element to initialize instance on
* @param {Object} [options] Options object
* @return {Object} thisArg
*/
initialize: function(el, options) {
options || (options = { });
this._initStatic(el, options);
this._initInteractive();
this._createCacheCanvas();
fabric.Canvas.activeInstance = this;
},
/**
* When true, objects can be transformed by one side (unproportionally)
* @type Boolean
* @default
*/
uniScaleTransform: false,
/**
* When true, objects use center point as the origin of transformation
* @type Boolean
* @default
*/
centerTransform: false,
/**
* Indicates that canvas is interactive. This property should not be changed.
* @type Boolean
* @default
*/
interactive: true,
/**
* Indicates whether group selection should be enabled
* @type Boolean
* @default
*/
selection: true,
/**
* Color of selection
* @type String
* @default
*/
selectionColor: 'rgba(100, 100, 255, 0.3)', // blue
/**
* Default dash array pattern
* If not empty the selection border is dashed
* @type Array
*/
selectionDashArray: [ ],
/**
* Color of the border of selection (usually slightly darker than color of selection itself)
* @type String
* @default
*/
selectionBorderColor: 'rgba(255, 255, 255, 0.3)',
/**
* Width of a line used in object/group selection
* @type Number
* @default
*/
selectionLineWidth: 1,
/**
* Default cursor value used when hovering over an object on canvas
* @type String
* @default
*/
hoverCursor: 'move',
/**
* Default cursor value used when moving an object on canvas
* @type String
* @default
*/
moveCursor: 'move',
/**
* Default cursor value used for the entire canvas
* @type String
* @default
*/
defaultCursor: 'default',
/**
* Cursor value used during free drawing
* @type String
* @default
*/
freeDrawingCursor: 'crosshair',
/**
* Cursor value used for rotation point
* @type String
* @default
*/
rotationCursor: 'crosshair',
/**
* Default element class that's given to wrapper (div) element of canvas
* @type String
* @default
*/
containerClass: 'canvas-container',
/**
* When true, object detection happens on per-pixel basis rather than on per-bounding-box
* @type Boolean
* @default
*/
perPixelTargetFind: false,
/**
* Number of pixels around target pixel to tolerate (consider active) during object detection
* @type Number
* @default
*/
targetFindTolerance: 0,
/**
* When true, target detection is skipped when hovering over canvas. This can be used to improve performance.
* @type Boolean
* @default
*/
skipTargetFind: false,
/**
* @private
*/
_initInteractive: function() {
this._currentTransform = null;
this._groupSelector = null;
this._initWrapperElement();
this._createUpperCanvas();
this._initEvents();
this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this);
this.calcOffset();
},
/**
* Resets the current transform to its original values and chooses the type of resizing based on the event
* @private
* @param {Event} e Event object fired on mousemove
*/
_resetCurrentTransform: function(e) {
var t = this._currentTransform;
t.target.set('scaleX', t.original.scaleX);
t.target.set('scaleY', t.original.scaleY);
t.target.set('left', t.original.left);
t.target.set('top', t.original.top);
if (e.altKey || this.centerTransform || t.target.centerTransform) {
if (t.originX !== 'center') {
if (t.originX === 'right') {
t.mouseXSign = -1;
}
else {
t.mouseXSign = 1;
}
}
if (t.originY !== 'center') {
if (t.originY === 'bottom') {
t.mouseYSign = -1;
}
else {
t.mouseYSign = 1;
}
}
t.originX = 'center';
t.originY = 'center';
}
else {
t.originX = t.original.originX;
t.originY = t.original.originY;
}
},
/**
* Checks if point is contained within an area of given object
* @param {Event} e Event object
* @param {fabric.Object} target Object to test against
* @return {Boolean} true if point is contained within an area of given object
*/
containsPoint: function (e, target) {
var pointer = this.getPointer(e),
xy = this._normalizePointer(target, pointer);
// http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html
// http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
return (target.containsPoint(xy) || target._findTargetCorner(e, this._offset));
},
/**
* @private
*/
_normalizePointer: function (object, pointer) {
var activeGroup = this.getActiveGroup(),
x = pointer.x,
y = pointer.y;
var isObjectInGroup = (
activeGroup &&
object.type !== 'group' &&
activeGroup.contains(object)
);
if (isObjectInGroup) {
x -= activeGroup.left;
y -= activeGroup.top;
}
return { x: x, y: y };
},
/**
* Returns true if object is transparent at a certain location
* @param {fabric.Object} target Object to check
* @param {Number} x Left coordinate
* @param {Number} y Top coordinate
* @return {Boolean}
*/
isTargetTransparent: function (target, x, y) {
var cacheContext = this.contextCache;
var hasBorders = target.hasBorders,
transparentCorners = target.transparentCorners;
target.hasBorders = target.transparentCorners = false;
this._draw(cacheContext, target);
target.hasBorders = hasBorders;
target.transparentCorners = transparentCorners;
// If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0
if (this.targetFindTolerance > 0) {
if (x > this.targetFindTolerance) {
x -= this.targetFindTolerance;
}
else {
x = 0;
}
if (y > this.targetFindTolerance) {
y -= this.targetFindTolerance;
}
else {
y = 0;
}
}
var isTransparent = true;
var imageData = cacheContext.getImageData(
x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1);
// Split image data - for tolerance > 1, pixelDataSize = 4;
for (var i = 3, l = imageData.data.length; i < l; i += 4) {
var temp = imageData.data[i];
isTransparent = temp <= 0;
if (isTransparent === false) break; //Stop if colour found
}
imageData = null;
this.clearContext(cacheContext);
return isTransparent;
},
/**
* @private
* @param {Event} e
* @param {fabric.Object} target
*/
_shouldClearSelection: function (e, target) {
var activeGroup = this.getActiveGroup();
return (
!target || (
target &&
activeGroup &&
!activeGroup.contains(target) &&
activeGroup !== target &&
!e.shiftKey) || (
target &&
!target.selectable)
);
},
/**
* @private
* @param {Event} e
* @param {fabric.Object} target
*/
_setupCurrentTransform: function (e, target) {
if (!target) return;
var action = 'drag',
corner,
pointer = getPointer(e, target.canvas.upperCanvasEl);
corner = target._findTargetCorner(e, this._offset);
if (corner) {
action = (corner === 'ml' || corner === 'mr')
? 'scaleX'
: (corner === 'mt' || corner === 'mb')
? 'scaleY'
: corner === 'mtr'
? 'rotate'
: 'scale';
}
var originX = target.originX, originY = target.originY;
if (corner === 'ml' || corner === 'tl' || corner === 'bl') {
originX = "right";
}
else if (corner === 'mr' || corner === 'tr' || corner === 'br') {
originX = "left";
}
if (corner === 'tl' || corner === 'mt' || corner === 'tr') {
originY = "bottom";
}
else if (corner === 'bl' || corner === 'mb' || corner === 'br') {
originY = "top";
}
// if (corner === 'mtr') {
// originX = 'center';
// originY = 'center';
// }
// var center = target.getCenterPoint();
this._currentTransform = {
target: target,
action: action,
scaleX: target.scaleX,
scaleY: target.scaleY,
offsetX: pointer.x - target.left,
offsetY: pointer.y - target.top,
originX: originX,
originY: originY,
ex: pointer.x,
ey: pointer.y,
left: target.left,
top: target.top,
theta: degreesToRadians(target.angle),
width: target.width * target.scaleX,
mouseXSign: 1,
mouseYSign: 1
};
this._currentTransform.original = {
left: target.left,
top: target.top,
scaleX: target.scaleX,
scaleY: target.scaleY,
originX: originX,
originY: originY
};
this._resetCurrentTransform(e);
},
/**
* @private
* @param {Event} e
* @param {fabric.Object} target
* @return {Boolean}
*/
_shouldHandleGroupLogic: function(e, target) {
var activeObject = this.getActiveObject();
return e.shiftKey &&
(this.getActiveGroup() || (activeObject && activeObject !== target))
&& this.selection;
},
/**
* @private
* @param {Event} e
* @param {fabric.Object} target
*/
_handleGroupLogic: function (e, target) {
if (target === this.getActiveGroup()) {
// if it's a group, find target again, this time skipping group
target = this.findTarget(e, true);
// if even object is not found, bail out
if (!target || target.isType('group')) {
return;
}
}
var activeGroup = this.getActiveGroup();
if (activeGroup) {
if (activeGroup.contains(target)) {
activeGroup.removeWithUpdate(target);
this._resetObjectTransform(activeGroup);
target.set('active', false);
if (activeGroup.size() === 1) {
// remove group alltogether if after removal it only contains 1 object
this.discardActiveGroup();
}
}
else {
activeGroup.addWithUpdate(target);
this._resetObjectTransform(activeGroup);
}
this.fire('selection:created', { target: activeGroup, e: e });
activeGroup.set('active', true);
}
else {
// group does not exist
if (this._activeObject) {
// only if there's an active object
if (target !== this._activeObject) {
// and that object is not the actual target
var objects = this.getObjects();
var isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target);
var group = new fabric.Group(
isActiveLower ? [ target, this._activeObject ] : [ this._activeObject, target ]);
this.setActiveGroup(group);
this._activeObject = null;
activeGroup = this.getActiveGroup();
this.fire('selection:created', { target: activeGroup, e: e });
}
}
// activate target object in any case
target.set('active', true);
}
if (activeGroup) {
activeGroup.saveCoords();
}
},
/**
* Translates object by "setting" its left/top
* @private
* @param x {Number} pointer's x coordinate
* @param y {Number} pointer's y coordinate
*/
_translateObject: function (x, y) {
var target = this._currentTransform.target;
if (!target.get('lockMovementX')) {
target.set('left', x - this._currentTransform.offsetX);
}
if (!target.get('lockMovementY')) {
target.set('top', y - this._currentTransform.offsetY);
}
},
/**
* Scales object by invoking its scaleX/scaleY methods
* @private
* @param x {Number} pointer's x coordinate
* @param y {Number} pointer's y coordinate
* @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object.
* When not provided, an object is scaled by both dimensions equally
*/
_scaleObject: function (x, y, by) {
var t = this._currentTransform,
offset = this._offset,
target = t.target;
var lockScalingX = target.get('lockScalingX'),
lockScalingY = target.get('lockScalingY');
if (lockScalingX && lockScalingY) return;
// Get the constraint point
var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY);
var localMouse = target.toLocalPoint(new fabric.Point(x - offset.left, y - offset.top), t.originX, t.originY);
if (t.originX === 'right') {
localMouse.x *= -1;
}
else if (t.originX === 'center') {
localMouse.x *= t.mouseXSign * 2;
if (localMouse.x < 0) {
t.mouseXSign = -t.mouseXSign;
}
}
if (t.originY === 'bottom') {
localMouse.y *= -1;
}
else if (t.originY === 'center') {
localMouse.y *= t.mouseYSign * 2;
if (localMouse.y < 0) {
t.mouseYSign = -t.mouseYSign;
}
}
// adjust the mouse coordinates when dealing with padding
if (abs(localMouse.x) > target.padding) {
if (localMouse.x < 0 ) {
localMouse.x += target.padding;
} else {
localMouse.x -= target.padding;
}
} else { // mouse is within the padding, set to 0
localMouse.x = 0;
}
if (abs(localMouse.y) > target.padding) {
if (localMouse.y < 0 ) {
localMouse.y += target.padding;
} else {
localMouse.y -= target.padding;
}
} else {
localMouse.y = 0;
}
// Actually scale the object
var newScaleX = target.scaleX, newScaleY = target.scaleY;
if (by === 'equally' && !lockScalingX && !lockScalingY) {
var dist = localMouse.y + localMouse.x;
var lastDist = (target.height + (target.strokeWidth)) * t.original.scaleY +
(target.width + (target.strokeWidth)) * t.original.scaleX;
// We use t.scaleX/Y instead of target.scaleX/Y because the object may have a min scale and we'll loose the proportions
newScaleX = t.original.scaleX * dist/lastDist;
newScaleY = t.original.scaleY * dist/lastDist;
target.set('scaleX', newScaleX);
target.set('scaleY', newScaleY);
}
else if (!by) {
newScaleX = localMouse.x/(target.width+target.strokeWidth);
newScaleY = localMouse.y/(target.height+target.strokeWidth);
lockScalingX || target.set('scaleX', newScaleX);
lockScalingY || target.set('scaleY', newScaleY);
}
else if (by === 'x' && !target.get('lockUniScaling')) {
newScaleX = localMouse.x/(target.width + target.strokeWidth);
lockScalingX || target.set('scaleX', newScaleX);
}
else if (by === 'y' && !target.get('lockUniScaling')) {
newScaleY = localMouse.y/(target.height + target.strokeWidth);
lockScalingY || target.set('scaleY', newScaleY);
}
// Check if we flipped
if (newScaleX < 0)
{
if (t.originX === 'left')
t.originX = 'right';
else if (t.originX === 'right')
t.originX = 'left';
}
if (newScaleY < 0)
{
if (t.originY === 'top')
t.originY = 'bottom';
else if (t.originY === 'bottom')
t.originY = 'top';
}
// Make sure the constraints apply
target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
},
/**
* Rotates object by invoking its rotate method
* @private
* @param x {Number} pointer's x coordinate
* @param y {Number} pointer's y coordinate
*/
_rotateObject: function (x, y) {
var t = this._currentTransform,
o = this._offset;
if (t.target.get('lockRotation')) return;
var lastAngle = atan2(t.ey - t.top - o.top, t.ex - t.left - o.left),
curAngle = atan2(y - t.top - o.top, x - t.left - o.left),
angle = radiansToDegrees(curAngle - lastAngle + t.theta);
// normalize angle to positive value
if (angle < 0) {
angle = 360 + angle;
}
t.target.angle = angle;
},
/**
* @private
*/
_setCursor: function (value) {
this.upperCanvasEl.style.cursor = value;
},
/**
* @private
*/
_resetObjectTransform: function (target) {
target.scaleX = 1;
target.scaleY = 1;
target.setAngle(0);
},
/**
* @private
*/
_drawSelection: function () {
var ctx = this.contextTop,
groupSelector = this._groupSelector,
left = groupSelector.left,
top = groupSelector.top,
aleft = abs(left),
atop = abs(top);
ctx.fillStyle = this.selectionColor;
ctx.fillRect(
groupSelector.ex - ((left > 0) ? 0 : -left),
groupSelector.ey - ((top > 0) ? 0 : -top),
aleft,
atop
);
ctx.lineWidth = this.selectionLineWidth;
ctx.strokeStyle = this.selectionBorderColor;
// selection border
if (this.selectionDashArray.length > 1) {
var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0: aleft);
var py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0: atop);
ctx.beginPath();
fabric.util.drawDashedLine(ctx, px, py, px+aleft, py, this.selectionDashArray);
fabric.util.drawDashedLine(ctx, px, py+atop-1, px+aleft, py+atop-1, this.selectionDashArray);
fabric.util.drawDashedLine(ctx, px, py, px, py+atop, this.selectionDashArray);
fabric.util.drawDashedLine(ctx, px+aleft-1, py, px+aleft-1, py+atop, this.selectionDashArray);
ctx.closePath();
ctx.stroke();
}
else {
ctx.strokeRect(
groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft),
groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop),
aleft,
atop
);
}
},
/**
* @private
*/
_findSelectedObjects: function (e) {
var group = [ ],
x1 = this._groupSelector.ex,
y1 = this._groupSelector.ey,
x2 = x1 + this._groupSelector.left,
y2 = y1 + this._groupSelector.top,
currentObject,
selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)),
selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)),
isClick = x1 === x2 && y1 === y2;
for (var i = this._objects.length; i--; ) {
currentObject = this._objects[i];
if (!currentObject) continue;
if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) ||
currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) ||
currentObject.containsPoint(selectionX1Y1) ||
currentObject.containsPoint(selectionX2Y2)) {
if (this.selection && currentObject.selectable) {
currentObject.set('active', true);
group.push(currentObject);
// only add one object if it's a click
if (isClick) break;
}
}
}
// do not create group for 1 element only
if (group.length === 1) {
this.setActiveObject(group[0], e);
}
else if (group.length > 1) {
group = new fabric.Group(group.reverse());
this.setActiveGroup(group);
group.saveCoords();
this.fire('selection:created', { target: group });
this.renderAll();
}
},
/**
* Method that determines what object we are clicking on
* @param {Event} e mouse event
* @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through
*/
findTarget: function (e, skipGroup) {
if (this.skipTargetFind) return;
var target,
pointer = this.getPointer(e);
if (this.controlsAboveOverlay &&
this.lastRenderedObjectWithControlsAboveOverlay &&
this.lastRenderedObjectWithControlsAboveOverlay.visible &&
this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) &&
this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)) {
target = this.lastRenderedObjectWithControlsAboveOverlay;
return target;
}
// first check current group (if one exists)
var activeGroup = this.getActiveGroup();
if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) {
target = activeGroup;
return target;
}
// then check all of the objects on canvas
// Cache all targets where their bounding box contains point.
var possibleTargets = [];
for (var i = this._objects.length; i--; ) {
if (this._objects[i] &&
this._objects[i].visible &&
this._objects[i].selectable &&
this.containsPoint(e, this._objects[i])) {
if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) {
possibleTargets[possibleTargets.length] = this._objects[i];
}
else {
target = this._objects[i];
this.relatedTarget = target;
break;
}
}
}
for (var j = 0, len = possibleTargets.length; j < len; j++) {
pointer = this.getPointer(e);
var isTransparent = this.isTargetTransparent(possibleTargets[j], pointer.x, pointer.y);
if (!isTransparent) {
target = possibleTargets[j];
this.relatedTarget = target;
break;
}
}
return target;
},
/**
* Returns pointer coordinates relative to canvas.
* @param {Event} e
* @return {Object} object with "x" and "y" number values
*/
getPointer: function (e) {
var pointer = getPointer(e, this.upperCanvasEl);
return {
x: pointer.x - this._offset.left,
y: pointer.y - this._offset.top
};
},
/**
* @private
* @param {HTMLElement|String} canvasEl Canvas element
* @throws {CANVAS_INIT_ERROR} If canvas can not be initialized
*/
_createUpperCanvas: function () {
var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, '');
this.upperCanvasEl = this._createCanvasElement();
fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass);
this.wrapperEl.appendChild(this.upperCanvasEl);
this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl);
this._applyCanvasStyle(this.upperCanvasEl);
this.contextTop = this.upperCanvasEl.getContext('2d');
},
/**
* @private
*/
_createCacheCanvas: function () {
this.cacheCanvasEl = this._createCanvasElement();
this.cacheCanvasEl.setAttribute('width', this.width);
this.cacheCanvasEl.setAttribute('height', this.height);
this.contextCache = this.cacheCanvasEl.getContext('2d');
},
/**
* @private
* @param {Number} width
* @param {Number} height
*/
_initWrapperElement: function () {
this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', {
'class': this.containerClass
});
fabric.util.setStyle(this.wrapperEl, {
width: this.getWidth() + 'px',
height: this.getHeight() + 'px',
position: 'relative'
});
fabric.util.makeElementUnselectable(this.wrapperEl);
},
/**
* @private
* @param {Element} element
*/
_applyCanvasStyle: function (element) {
var width = this.getWidth() || element.width,
height = this.getHeight() || element.height;
fabric.util.setStyle(element, {
position: 'absolute',
width: width + 'px',
height: height + 'px',
left: 0,
top: 0
});
element.width = width;
element.height = height;
fabric.util.makeElementUnselectable(element);
},
/**
* Copys the the entire inline style from one element (fromEl) to another (toEl)
* @private
* @param {Element} fromEl Element style is copied from
* @param {Element} toEl Element copied style is applied to
*/
_copyCanvasStyle: function (fromEl, toEl) {
toEl.style.cssText = fromEl.style.cssText;
},
/**
* Returns context of canvas where object selection is drawn
* @return {CanvasRenderingContext2D}
*/
getSelectionContext: function() {
return this.contextTop;
},
/**
* Returns &lt;canvas> element on which object selection is drawn
* @return {HTMLCanvasElement}
*/
getSelectionElement: function () {
return this.upperCanvasEl;
},
/**
* Sets given object as the only active object on canvas
* @param {fabric.Object} object Object to set as an active one
* @param {Event} [e] Event (passed along when firing "object:selected")
* @return {fabric.Canvas} thisArg
* @chainable
*/
setActiveObject: function (object, e) {
if (this._activeObject) {
this._activeObject.set('active', false);
}
this._activeObject = object;
object.set('active', true);
this.renderAll();
this.fire('object:selected', { target: object, e: e });
object.fire('selected', { e: e });
return this;
},
/**
* Returns currently active object
* @return {fabric.Object} active object
*/
getActiveObject: function () {
return this._activeObject;
},
/**
* Discards currently active object
* @return {fabric.Canvas} thisArg
* @chainable
*/
discardActiveObject: function () {
if (this._activeObject) {
this._activeObject.set('active', false);
}
this._activeObject = null;
return this;
},
/**
* Sets active group to a speicified one
* @param {fabric.Group} group Group to set as a current one
* @return {fabric.Canvas} thisArg
* @chainable
*/
setActiveGroup: function (group) {
this._activeGroup = group;
if (group) {
group.canvas = this;
group.set('active', true);
}
return this;
},
/**
* Returns currently active group
* @return {fabric.Group} Current group
*/
getActiveGroup: function () {
return this._activeGroup;
},
/**
* Removes currently active group
* @return {fabric.Canvas} thisArg
*/
discardActiveGroup: function () {
var g = this.getActiveGroup();
if (g) {
g.destroy();
}
return this.setActiveGroup(null);
},
/**
* Deactivates all objects on canvas, removing any active group or object
* @return {fabric.Canvas} thisArg
*/
deactivateAll: function () {
var allObjects = this.getObjects(),
i = 0,
len = allObjects.length;
for ( ; i < len; i++) {
allObjects[i].set('active', false);
}
this.discardActiveGroup();
this.discardActiveObject();
return this;
},
/**
* Deactivates all objects and dispatches appropriate events
* @return {fabric.Canvas} thisArg
*/
deactivateAllWithDispatch: function () {
var activeObject = this.getActiveGroup() || this.getActiveObject();
if (activeObject) {
this.fire('before:selection:cleared', { target: activeObject });
}
this.deactivateAll();
if (activeObject) {
this.fire('selection:cleared');
}
return this;
},
/**
* Draws objects' controls (borders/controls)
* @param {CanvasRenderingContext2D} ctx Context to render controls on
*/
drawControls: function(ctx) {
var activeGroup = this.getActiveGroup();
if (activeGroup) {
ctx.save();
fabric.Group.prototype.transform.call(activeGroup, ctx);
activeGroup.drawBorders(ctx).drawControls(ctx);
ctx.restore();
}
else {
for (var i = 0, len = this._objects.length; i < len; ++i) {
if (!this._objects[i] || !this._objects[i].active) continue;
ctx.save();
fabric.Object.prototype.transform.call(this._objects[i], ctx);
this._objects[i].drawBorders(ctx).drawControls(ctx);
ctx.restore();
this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i];
}
}
}
});
// copying static properties manually to work around Opera's bug,
// where "prototype" property is enumerable and overrides existing prototype
for (var prop in fabric.StaticCanvas) {
if (prop !== 'prototype') {
fabric.Canvas[prop] = fabric.StaticCanvas[prop];
}
}
if (fabric.isTouchSupported) {
/** @ignore */
fabric.Canvas.prototype._setCursorFromEvent = function() { };
}
/**
* @class fabric.Element
* @alias fabric.Canvas
* @deprecated Use {@link fabric.Canvas} instead.
* @constructor
*/
fabric.Element = fabric.Canvas;
})();
(function(){
var cursorMap = [
'n-resize',
'ne-resize',
'e-resize',
'se-resize',
's-resize',
'sw-resize',
'w-resize',
'nw-resize'
],
cursorOffset = {
'mt': 0, // n
'tr': 1, // ne
'mr': 2, // e
'br': 3, // se
'mb': 4, // s
'bl': 5, // sw
'ml': 6, // w
'tl': 7 // nw
},
addListener = fabric.util.addListener,
removeListener = fabric.util.removeListener,
getPointer = fabric.util.getPointer;
fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ {
/**
* Adds mouse listeners to canvas
* @private
*/
_initEvents: function () {
var _this = this;
this._onMouseDown = this._onMouseDown.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
this._onResize = this._onResize.bind(this);
this._onGesture = function(e, s) {
_this.__onTransformGesture(e, s);
};
addListener(fabric.window, 'resize', this._onResize);
if (fabric.isTouchSupported) {
addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
if (typeof Event !== 'undefined' && 'add' in Event) {
Event.add(this.upperCanvasEl, 'gesture', this._onGesture);
}
}
else {
addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
}
},
/**
* @private
* @param {Event} e Event object fired on mousedown
*/
_onMouseDown: function (e) {
this.__onMouseDown(e);
!fabric.isTouchSupported && addListener(fabric.document, 'mouseup', this._onMouseUp);
fabric.isTouchSupported && addListener(fabric.document, 'touchend', this._onMouseUp);
!fabric.isTouchSupported && addListener(fabric.document, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && addListener(fabric.document, 'touchmove', this._onMouseMove);
!fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
},
/**
* @private
* @param {Event} e Event object fired on mouseup
*/
_onMouseUp: function (e) {
this.__onMouseUp(e);
!fabric.isTouchSupported && removeListener(fabric.document, 'mouseup', this._onMouseUp);
fabric.isTouchSupported && removeListener(fabric.document, 'touchend', this._onMouseUp);
!fabric.isTouchSupported && removeListener(fabric.document, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', this._onMouseMove);
!fabric.isTouchSupported && addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
fabric.isTouchSupported && addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
},
/**
* @private
* @param {Event} e Event object fired on mousemove
*/
_onMouseMove: function (e) {
!this.allowTouchScrolling && e.preventDefault && e.preventDefault();
this.__onMouseMove(e);
},
/**
* @private
*/
_onResize: function () {
this.calcOffset();
},
/**
* Decides whether the canvas should be redrawn in mouseup and mousedown events.
* @private
* @param {Object} target
* @param {Object} pointer
*/
_shouldRender: function(target, pointer) {
var activeObject = this.getActiveGroup() || this.getActiveObject();
return (
(target && (
target.isMoving ||
target !== activeObject)) ||
(!target && activeObject) ||
(pointer &&
this._previousPointer &&
this.selection && (
pointer.x !== this._previousPointer.x ||
pointer.y !== this._previousPointer.y))
);
},
/**
* Method that defines the actions when mouse is released on canvas.
* The method resets the currentTransform parameters, store the image corner
* position in the image object and render the canvas on top.
* @private
* @param {Event} e Event object fired on mouseup
*/
__onMouseUp: function (e) {
var target,
pointer,
render;
if (this.isDrawingMode && this._isCurrentlyDrawing) {
this._isCurrentlyDrawing = false;
if (this.clipTo) {
this.contextTop.restore();
}
this.freeDrawingBrush.onMouseUp();
this.fire('mouse:up', { e: e });
return;
}
if (this._currentTransform) {
var transform = this._currentTransform;
target = transform.target;
if (target._scaling) {
target._scaling = false;
}
target.setCoords();
// only fire :modified event if target coordinates were changed during mousedown-mouseup
if (this.stateful && target.hasStateChanged()) {
this.fire('object:modified', { target: target });
target.fire('modified');
}
if (this._previousOriginX && this._previousOriginY) {
var originPoint = target.translateToOriginPoint(
target.getCenterPoint(),
this._previousOriginX,
this._previousOriginY);
target.originX = this._previousOriginX;
target.originY = this._previousOriginY;
target.left = originPoint.x;
target.top = originPoint.y;
this._previousOriginX = null;
this._previousOriginY = null;
}
}
else {
pointer = this.getPointer(e);
}
render = this._shouldRender(target, pointer);
if (this.selection && this._groupSelector) {
// group selection was completed, determine its bounds
this._findSelectedObjects(e);
}
var activeGroup = this.getActiveGroup();
if (activeGroup) {
activeGroup.setObjectsCoords();
activeGroup.isMoving = false;
this._setCursor(this.defaultCursor);
}
// clear selection and current transformation
this._groupSelector = null;
this._currentTransform = null;
if (target) {
target.isMoving = false;
}
render && this.renderAll();
this._setCursorFromEvent(e, target);
var _this = this;
setTimeout(function () {
_this._setCursorFromEvent(e, target);
}, 50);
this.fire('mouse:up', { target: target, e: e });
target && target.fire('mouseup', { e: e });
},
/**
* @private
* @param {Event} e Event object fired on mousedown
*/
_onMouseDownInDrawingMode: function(e) {
this._isCurrentlyDrawing = true;
this.discardActiveObject().renderAll();
if (this.clipTo) {
fabric.util.clipContext(this, this.contextTop);
}
this.freeDrawingBrush.onMouseDown(this.getPointer(e));
this.fire('mouse:down', { e: e });
},
/**
* Method that defines the actions when mouse is clic ked on canvas.
* The method inits the currentTransform parameters and renders all the
* canvas so the current image can be placed on the top canvas and the rest
* in on the container one.
* @private
* @param {Event} e Event object fired on mousedown
*/
__onMouseDown: function (e) {
// accept only left clicks
var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1;
if (!isLeftClick && !fabric.isTouchSupported) return;
if (this.isDrawingMode) {
this._onMouseDownInDrawingMode(e);
return;
}
// ignore if some object is being transformed at this moment
if (this._currentTransform) return;
var target = this.findTarget(e),
pointer = this.getPointer(e),
corner,
render;
// save pointer for check in __onMouseUp event
this._previousPointer = pointer;
render = this._shouldRender(target, pointer);
if (this._shouldClearSelection(e, target)) {
if (this.selection) {
this._groupSelector = {
ex: pointer.x,
ey: pointer.y,
top: 0,
left: 0
};
}
this.deactivateAllWithDispatch();
target && target.selectable && this.setActiveObject(target, e);
}
else if (this._shouldHandleGroupLogic(e, target)) {
this._handleGroupLogic(e, target);
target = this.getActiveGroup();
}
else {
// determine if it's a drag or rotate case
this.stateful && target.saveState();
if ((corner = target._findTargetCorner(e, this._offset))) {
this.onBeforeScaleRotate(target);
}
if (target !== this.getActiveGroup() && target !== this.getActiveObject()) {
this.deactivateAll();
this.setActiveObject(target, e);
}
this._setupCurrentTransform(e, target);
}
// we must renderAll so that active image is placed on the top canvas
render && this.renderAll();
this.fire('mouse:down', { target: target, e: e });
target && target.fire('mousedown', { e: e });
if (corner === 'mtr' && target.centerTransform) {
this._setOriginToCenter(target);
}
},
/**
* @private
*/
_setOriginToCenter: function(target) {
this._previousOriginX = this._currentTransform.target.originX;
this._previousOriginY = this._currentTransform.target.originY;
var center = target.getCenterPoint();
target.originX = 'center';
target.originY = 'center';
target.left = center.x;
target.top = center.y;
this._currentTransform.left = target.left;
this._currentTransform.top = target.top;
},
/**
* Method that defines the actions when mouse is hovering the canvas.
* The currentTransform parameter will definde whether the user is rotating/scaling/translating
* an image or neither of them (only hovering). A group selection is also possible and would cancel
* all any other type of action.
* In case of an image transformation only the top canvas will be rendered.
* @private
* @param {Event} e Event object fired on mousemove
*/
__onMouseMove: function (e) {
var target, pointer;
if (this.isDrawingMode) {
if (this._isCurrentlyDrawing) {
pointer = this.getPointer(e);
this.freeDrawingBrush.onMouseMove(pointer);
}
this.upperCanvasEl.style.cursor = this.freeDrawingCursor;
this.fire('mouse:move', { e: e });
return;
}
var groupSelector = this._groupSelector;
// We initially clicked in an empty area, so we draw a box for multiple selection.
if (groupSelector) {
pointer = getPointer(e, this.upperCanvasEl);
groupSelector.left = pointer.x - this._offset.left - groupSelector.ex;
groupSelector.top = pointer.y - this._offset.top - groupSelector.ey;
this.renderTop();
}
else if (!this._currentTransform) {
// alias style to elimintate unnecessary lookup
var style = this.upperCanvasEl.style;
// Here we are hovering the canvas then we will determine
// what part of the pictures we are hovering to change the caret symbol.
// We won't do that while dragging or rotating in order to improve the
// performance.
target = this.findTarget(e);
if (!target || target && !target.selectable) {
// no target - set default cursor
style.cursor = this.defaultCursor;
}
else {
// set proper cursor
this._setCursorFromEvent(e, target);
}
}
else {
// object is being transformed (scaled/rotated/moved/etc.)
pointer = getPointer(e, this.upperCanvasEl);
var x = pointer.x,
y = pointer.y,
reset = false,
transform = this._currentTransform;
target = transform.target;
target.isMoving = true;
if ((transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') &&
// Switch from a normal resize to center-based
((e.altKey && (transform.originX !== 'center' || transform.originY !== 'center')) ||
// Switch from center-based resize to normal one
(!e.altKey && transform.originX === 'center' && transform.originY === 'center'))
) {
this._resetCurrentTransform(e);
reset = true;
}
if (transform.action === 'rotate') {
this._rotateObject(x, y);
this.fire('object:rotating', { target: target, e: e });
target.fire('rotating', { e: e });
}
else if (transform.action === 'scale') {
// rotate object only if shift key is not pressed
// and if it is not a group we are transforming
if ((e.shiftKey || this.uniScaleTransform) && !target.get('lockUniScaling')) {
transform.currentAction = 'scale';
this._scaleObject(x, y);
}
else {
// Switch from a normal resize to proportional
if (!reset && transform.currentAction === 'scale') {
this._resetCurrentTransform(e);
}
transform.currentAction = 'scaleEqually';
this._scaleObject(x, y, 'equally');
}
this.fire('object:scaling', { target: target, e: e });
target.fire('scaling', { e: e });
}
else if (transform.action === 'scaleX') {
this._scaleObject(x, y, 'x');
this.fire('object:scaling', { target: target, e: e});
target.fire('scaling', { e: e });
}
else if (transform.action === 'scaleY') {
this._scaleObject(x, y, 'y');
this.fire('object:scaling', { target: target, e: e});
target.fire('scaling', { e: e });
}
else {
this._translateObject(x, y);
this.fire('object:moving', { target: target, e: e});
target.fire('moving', { e: e });
this._setCursor(this.moveCursor);
}
this.renderAll();
}
this.fire('mouse:move', { target: target, e: e });
target && target.fire('mousemove', { e: e });
},
/**
* Sets the cursor depending on where the canvas is being hovered.
* Note: very buggy in Opera
* @param {Event} e Event object
* @param {Object} target Object that the mouse is hovering, if so.
*/
_setCursorFromEvent: function (e, target) {
var s = this.upperCanvasEl.style;
if (!target) {
s.cursor = this.defaultCursor;
return false;
}
else {
var activeGroup = this.getActiveGroup();
// only show proper corner when group selection is not active
var corner = target._findTargetCorner
&& (!activeGroup || !activeGroup.contains(target))
&& target._findTargetCorner(e, this._offset);
if (!corner) {
s.cursor = target.hoverCursor || this.hoverCursor;
}
else {
if (corner in cursorOffset) {
var n = Math.round((target.getAngle() % 360) / 45);
if (n<0) {
n += 8; // full circle ahead
}
n += cursorOffset[corner];
// normalize n to be from 0 to 7
n %= 8;
s.cursor = cursorMap[n];
}
else if (corner === 'mtr' && target.hasRotatingPoint) {
s.cursor = this.rotationCursor;
}
else {
s.cursor = this.defaultCursor;
return false;
}
}
}
return true;
}
});
})();
fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ {
/**
* Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately
* @param {Object} [options] Options object
* @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png"
* @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg.
* @param {Number} [options.multiplier=1] Multiplier to scale by
* @param {Number} [options.left] Cropping left offset
* @param {Number} [options.top] Cropping top offset
* @param {Number} [options.width] Cropping width
* @param {Number} [options.height] Cropping height
* @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format
*/
toDataURL: function (options) {
options || (options = { });
var format = options.format || 'png',
quality = options.quality || 1,
multiplier = options.multiplier || 1,
cropping = {
left: options.left,
top: options.top,
width: options.width,
height: options.height
};
if (multiplier !== 1) {
return this.__toDataURLWithMultiplier(format, quality, cropping, multiplier);
}
else {
return this.__toDataURL(format, quality, cropping);
}
},
/**
* @private
*/
__toDataURL: function(format, quality, cropping) {
this.renderAll(true);
var canvasEl = this.upperCanvasEl || this.lowerCanvasEl;
var croppedCanvasEl = this.__getCroppedCanvas(canvasEl, cropping);
// to avoid common confusion https://github.com/kangax/fabric.js/issues/806
if (format === 'jpg') {
format = 'jpeg';
}
var data = (fabric.StaticCanvas.supports('toDataURLWithQuality'))
? (croppedCanvasEl || canvasEl).toDataURL('image/' + format, quality)
: (croppedCanvasEl || canvasEl).toDataURL('image/' + format);
this.contextTop && this.clearContext(this.contextTop);
this.renderAll();
if (croppedCanvasEl) {
croppedCanvasEl = null;
}
return data;
},
/**
* @private
*/
__getCroppedCanvas: function(canvasEl, cropping) {
var croppedCanvasEl,
croppedCtx;
var shouldCrop = 'left' in cropping ||
'top' in cropping ||
'width' in cropping ||
'height' in cropping;
if (shouldCrop) {
croppedCanvasEl = fabric.util.createCanvasElement();
croppedCtx = croppedCanvasEl.getContext('2d');
croppedCanvasEl.width = cropping.width || this.width;
croppedCanvasEl.height = cropping.height || this.height;
croppedCtx.drawImage(canvasEl, -cropping.left || 0, -cropping.top || 0);
}
return croppedCanvasEl;
},
/**
* @private
*/
__toDataURLWithMultiplier: function(format, quality, cropping, multiplier) {
var origWidth = this.getWidth(),
origHeight = this.getHeight(),
scaledWidth = origWidth * multiplier,
scaledHeight = origHeight * multiplier,
activeObject = this.getActiveObject(),
activeGroup = this.getActiveGroup(),
ctx = this.contextTop || this.contextContainer;
this.setWidth(scaledWidth).setHeight(scaledHeight);
ctx.scale(multiplier, multiplier);
if (cropping.left) {
cropping.left *= multiplier;
}
if (cropping.top) {
cropping.top *= multiplier;
}
if (cropping.width) {
cropping.width *= multiplier;
}
if (cropping.height) {
cropping.height *= multiplier;
}
if (activeGroup) {
// not removing group due to complications with restoring it with correct state afterwords
this._tempRemoveBordersControlsFromGroup(activeGroup);
}
else if (activeObject && this.deactivateAll) {
this.deactivateAll();
}
// restoring width, height for `renderAll` to draw
// background properly (while context is scaled)
this.width = origWidth;
this.height = origHeight;
this.renderAll(true);
var data = this.__toDataURL(format, quality, cropping);
ctx.scale(1 / multiplier, 1 / multiplier);
this.setWidth(origWidth).setHeight(origHeight);
if (activeGroup) {
this._restoreBordersControlsOnGroup(activeGroup);
}
else if (activeObject && this.setActiveObject) {
this.setActiveObject(activeObject);
}
this.contextTop && this.clearContext(this.contextTop);
this.renderAll();
return data;
},
/**
* Exports canvas element to a dataurl image (allowing to change image size via multiplier).
* @deprecated since 1.0.13
* @param {String} format (png|jpeg)
* @param {Number} multiplier
* @param {Number} quality (0..1)
* @return {String}
*/
toDataURLWithMultiplier: function (format, multiplier, quality) {
return this.toDataURL({
format: format,
multiplier: multiplier,
quality: quality
});
},
/**
* @private
*/
_tempRemoveBordersControlsFromGroup: function(group) {
group.origHasControls = group.hasControls;
group.origBorderColor = group.borderColor;
group.hasControls = true;
group.borderColor = 'rgba(0,0,0,0)';
group.forEachObject(function(o) {
o.origBorderColor = o.borderColor;
o.borderColor = 'rgba(0,0,0,0)';
});
},
/**
* @private
*/
_restoreBordersControlsOnGroup: function(group) {
group.hideControls = group.origHideControls;
group.borderColor = group.origBorderColor;
group.forEachObject(function(o) {
o.borderColor = o.origBorderColor;
delete o.origBorderColor;
});
}
});
fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ {
/**
* Populates canvas with data from the specified dataless JSON
* JSON format must conform to the one of `fabric.Canvas#toDatalessJSON`
* @deprecated since 1.2.2
* @param {String|Object} json JSON string or object
* @param {Function} callback Callback, invoked when json is parsed
* and corresponding objects (e.g: fabric.Image)
* are initialized
* @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created.
* @return {fabric.Canvas} instance
* @chainable
*/
loadFromDatalessJSON: function (json, callback, reviver) {
return this.loadFromJSON(json, callback, reviver);
},
/**
* Populates canvas with data from the specified JSON
* JSON format must conform to the one of `fabric.Canvas#toJSON`
* @param {String|Object} json JSON string or object
* @param {Function} callback Callback, invoked when json is parsed
* and corresponding objects (e.g: fabric.Image)
* are initialized
* @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created.
* @return {fabric.Canvas} instance
* @chainable
*/
loadFromJSON: function (json, callback, reviver) {
if (!json) return;
// serialize if it wasn't already
var serialized = (typeof json === 'string')
? JSON.parse(json)
: json;
this.clear();
var _this = this;
this._enlivenObjects(serialized.objects, function () {
_this._setBgOverlayImages(serialized, callback);
}, reviver);
return this;
},
_setBgOverlayImages: function(serialized, callback) {
var _this = this,
backgroundPatternLoaded,
backgroundImageLoaded,
overlayImageLoaded;
var cbIfLoaded = function () {
callback && backgroundImageLoaded && overlayImageLoaded && backgroundPatternLoaded && callback();
};
if (serialized.backgroundImage) {
this.setBackgroundImage(serialized.backgroundImage, function() {
_this.backgroundImageOpacity = serialized.backgroundImageOpacity;
_this.backgroundImageStretch = serialized.backgroundImageStretch;
_this.renderAll();
backgroundImageLoaded = true;
cbIfLoaded();
});
}
else {
backgroundImageLoaded = true;
}
if (serialized.overlayImage) {
this.setOverlayImage(serialized.overlayImage, function() {
_this.overlayImageLeft = serialized.overlayImageLeft || 0;
_this.overlayImageTop = serialized.overlayImageTop || 0;
_this.renderAll();
overlayImageLoaded = true;
cbIfLoaded();
});
}
else {
overlayImageLoaded = true;
}
if (serialized.background) {
this.setBackgroundColor(serialized.background, function() {
_this.renderAll();
backgroundPatternLoaded = true;
cbIfLoaded();
});
}
else {
backgroundPatternLoaded = true;
}
if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background) {
callback && callback();
}
},
/**
* @private
* @param {Array} objects
* @param {Function} callback
* @param {Function} [reviver]
*/
_enlivenObjects: function (objects, callback, reviver) {
var _this = this;
if (objects.length === 0) {
callback && callback();
}
var renderOnAddRemove = this.renderOnAddRemove;
this.renderOnAddRemove = false;
fabric.util.enlivenObjects(objects, function(enlivenedObjects) {
enlivenedObjects.forEach(function(obj, index) {
_this.insertAt(obj, index, true);
});
_this.renderOnAddRemove = renderOnAddRemove;
callback && callback();
}, null, reviver);
},
/**
* @private
* @param {String} format
* @param {Function} callback
*/
_toDataURL: function (format, callback) {
this.clone(function (clone) {
callback(clone.toDataURL(format));
});
},
/**
* @private
* @param {String} format
* @param {Number} multiplier
* @param {Function} callback
*/
_toDataURLWithMultiplier: function (format, multiplier, callback) {
this.clone(function (clone) {
callback(clone.toDataURLWithMultiplier(format, multiplier));
});
},
/**
* Clones canvas instance
* @param {Object} [callback] Receives cloned instance as a first argument
*/
clone: function (callback) {
var data = JSON.stringify(this);
this.cloneWithoutData(function(clone) {
clone.loadFromJSON(data, function() {
callback && callback(clone);
});
});
},
/**
* Clones canvas instance without cloning existing data.
* This essentially copies canvas dimensions, clipping properties, etc.
* but leaves data empty (so that you can populate it with your own)
* @param {Object} [callback] Receives cloned instance as a first argument
*/
cloneWithoutData: function(callback) {
var el = fabric.document.createElement('canvas');
el.width = this.getWidth();
el.height = this.getHeight();
var clone = new fabric.Canvas(el);
clone.clipTo = this.clipTo;
if (this.backgroundImage) {
clone.setBackgroundImage(this.backgroundImage.src, function() {
clone.renderAll();
callback && callback(clone);
});
clone.backgroundImageOpacity = this.backgroundImageOpacity;
clone.backgroundImageStretch = this.backgroundImageStretch;
}
else {
callback && callback(clone);
}
}
});
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
toFixed = fabric.util.toFixed,
capitalize = fabric.util.string.capitalize,
degreesToRadians = fabric.util.degreesToRadians,
supportsLineDash = fabric.StaticCanvas.supports('setLineDash');
if (fabric.Object) {
return;
}
/**
* Root object class from which all 2d shape classes inherit from
* @class fabric.Object
*/
fabric.Object = fabric.util.createClass(/** @lends fabric.Object.prototype */ {
/**
* Type of an object (rect, circle, path, etc.)
* @type String
* @default
*/
type: 'object',
/**
* Horizontal origin of transformation of an object (one of "left", "right", "center")
* @type String
* @default
*/
originX: 'center',
/**
* Vertical origin of transformation of an object (one of "top", "bottom", "center")
* @type String
* @default
*/
originY: 'center',
/**
* Top position of an object. Note that by default it's relative to object center. You can change this by setting originY={top/center/bottom}
* @type Number
* @default
*/
top: 0,
/**
* Left position of an object. Note that by default it's relative to object center. You can change this by setting originX={left/center/right}
* @type Number
* @default
*/
left: 0,
/**
* Object width
* @type Number
* @default
*/
width: 0,
/**
* Object height
* @type Number
* @default
*/
height: 0,
/**
* Object scale factor (horizontal)
* @type Number
* @default
*/
scaleX: 1,
/**
* Object scale factor (vertical)
* @type Number
* @default
*/
scaleY: 1,
/**
* When true, an object is rendered as flipped horizontally
* @type Boolean
* @default
*/
flipX: false,
/**
* When true, an object is rendered as flipped vertically
* @type Boolean
* @default
*/
flipY: false,
/**
* Opacity of an object
* @type Number
* @default
*/
opacity: 1,
/**
* Angle of rotation of an object (in degrees)
* @type Number
* @default
*/
angle: 0,
/**
* Size of object's controlling corners (in pixels)
* @type Number
* @default
*/
cornerSize: 12,
/**
* When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill)
* @type Boolean
* @default
*/
transparentCorners: true,
/**
* Default cursor value used when hovering over this object on canvas
* @type String
* @default
*/
hoverCursor: null,
/**
* Padding between object and its controlling borders (in pixels)
* @type Number
* @default
*/
padding: 0,
/**
* Color of controlling borders of an object (when it's active)
* @type String
* @default
*/
borderColor: 'rgba(102,153,255,0.75)',
/**
* Color of controlling corners of an object (when it's active)
* @type String
* @default
*/
cornerColor: 'rgba(102,153,255,0.5)',
/**
* When true, this object will use center point as the origin of transformation
* when being resized via the controls
* @type Boolean
*/
centerTransform: false,
/**
* Color of object's fill
* @type String
* @default
*/
fill: 'rgb(0,0,0)',
/**
* Fill rule used to fill an object
* @type String
* @default
*/
fillRule: 'source-over',
/**
* Overlay fill (takes precedence over fill value)
* @type String
* @default
*/
overlayFill: null,
/**
* When defined, an object is rendered via stroke and this property specifies its color
* @type String
* @default
*/
stroke: null,
/**
* Width of a stroke used to render this object
* @type Number
* @default
*/
strokeWidth: 1,
/**
* Array specifying dash pattern of an object's stroke (stroke must be defined)
* @type Array
*/
strokeDashArray: null,
/**
* Line endings style of an object's stroke (one of "butt", "round", "square")
* @type String
* @default
*/
strokeLineCap: 'butt',
/**
* Corner style of an object's stroke (one of "bevil", "round", "miter")
* @type String
* @default
*/
strokeLineJoin: 'miter',
/**
* Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke
* @type Number
* @default
*/
strokeMiterLimit: 10,
/**
* Shadow object representing shadow of this shape
* @type fabric.Shadow
* @default
*/
shadow: null,
/**
* Opacity of object's controlling borders when object is active and moving
* @type Number
* @default
*/
borderOpacityWhenMoving: 0.4,
/**
* Scale factor of object's controlling borders
* @type Number
* @default
*/
borderScaleFactor: 1,
/**
* Transform matrix (similar to SVG's transform matrix)
* @type Array
*/
transformMatrix: null,
/**
* Minimum allowed scale value of an object
* @type Number
* @default
*/
minScaleLimit: 0.01,
/**
* When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection).
* All events propagate through it.
* @type Boolean
* @default
*/
selectable: true,
/**
* When set to `false`, an object is not rendered on canvas
* @type Boolean
* @default
*/
visible: true,
/**
* When set to `false`, object's controls are not displayed and can not be used to manipulate object
* @type Boolean
* @default
*/
hasControls: true,
/**
* When set to `false`, object's controlling borders are not rendered
* @type Boolean
* @default
*/
hasBorders: true,
/**
* When set to `false`, object's controlling rotating point will not be visible or selectable
* @type Boolean
* @default
*/
hasRotatingPoint: true,
/**
* Offset for object's controlling rotating point (when enabled via `hasRotatingPoint`)
* @type Number
* @default
*/
rotatingPointOffset: 40,
/**
* When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box
* @type Boolean
* @default
*/
perPixelTargetFind: false,
/**
* When `false`, default object's values are not included in its serialization
* @type Boolean
* @default
*/
includeDefaultValues: true,
/**
* Function that determines clipping of an object (context is passed as a first argument)
* @type Function
*/
clipTo: null,
/**
* When `true`, object horizontal movement is locked
* @type Boolean
* @default
*/
lockMovementX: false,
/**
* When `true`, object vertical movement is locked
* @type Boolean
* @default
*/
lockMovementY: false,
/**
* When `true`, object rotation is locked
* @type Boolean
* @default
*/
lockRotation: false,
/**
* When `true`, object horizontal scaling is locked
* @type Boolean
* @default
*/
lockScalingX: false,
/**
* When `true`, object vertical scaling is locked
* @type Boolean
* @default
*/
lockScalingY: false,
/**
* When `true`, object non-uniform scaling is locked
* @type Boolean
* @default
*/
lockUniScaling: false,
/**
* List of properties to consider when checking if state
* of an object is changed (fabric.Object#hasStateChanged)
* as well as for history (undo/redo) purposes
* @type Array
*/
stateProperties: (
'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' +
'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' +
'angle opacity fill fillRule overlayFill shadow clipTo visible'
).split(' '),
/**
* Constructor
* @param {Object} [options] Options object
*/
initialize: function(options) {
if (options) {
this.setOptions(options);
}
},
/**
* @private
*/
_initGradient: function(options) {
if (options.fill && options.fill.colorStops && !(options.fill instanceof fabric.Gradient)) {
this.set('fill', new fabric.Gradient(options.fill));
}
},
/**
* @private
*/
_initPattern: function(options) {
if (options.fill && options.fill.source && !(options.fill instanceof fabric.Pattern)) {
this.set('fill', new fabric.Pattern(options.fill));
}
if (options.stroke && options.stroke.source && !(options.stroke instanceof fabric.Pattern)) {
this.set('stroke', new fabric.Pattern(options.stroke));
}
},
/**
* @private
*/
_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);
}
},
/**
* Sets object's properties from options
* @param {Object} [options] Options object
*/
setOptions: function(options) {
for (var prop in options) {
this.set(prop, options[prop]);
}
this._initGradient(options);
this._initPattern(options);
this._initClipping(options);
},
/**
* Transforms context when rendering an object
* @param {CanvasRenderingContext2D} ctx Context
* @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node
*/
transform: function(ctx, fromLeft) {
ctx.globalAlpha = this.opacity;
var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint();
ctx.translate(center.x, center.y);
ctx.rotate(degreesToRadians(this.angle));
ctx.scale(
this.scaleX * (this.flipX ? -1 : 1),
this.scaleY * (this.flipY ? -1 : 1)
);
},
/**
* Returns an object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} Object representation of an instance
*/
toObject: function(propertiesToInclude) {
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
var object = {
type: this.type,
originX: this.originX,
originY: this.originY,
left: toFixed(this.left, NUM_FRACTION_DIGITS),
top: toFixed(this.top, NUM_FRACTION_DIGITS),
width: toFixed(this.width, NUM_FRACTION_DIGITS),
height: toFixed(this.height, NUM_FRACTION_DIGITS),
fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill,
overlayFill: this.overlayFill,
stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke,
strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS),
strokeDashArray: this.strokeDashArray,
strokeLineCap: this.strokeLineCap,
strokeLineJoin: this.strokeLineJoin,
strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS),
scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS),
scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS),
angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS),
flipX: this.flipX,
flipY: this.flipY,
opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS),
shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow,
visible: this.visible,
clipTo: this.clipTo && String(this.clipTo)
};
if (!this.includeDefaultValues) {
object = this._removeDefaultValues(object);
}
fabric.util.populateWithProperties(this, object, propertiesToInclude);
return object;
},
/**
* Returns (dataless) object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} Object representation of an instance
*/
toDatalessObject: function(propertiesToInclude) {
// will be overwritten by subclasses
return this.toObject(propertiesToInclude);
},
/**
* @private
* @param {Object} object
*/
_removeDefaultValues: function(object) {
this.stateProperties.forEach(function(prop) {
if (object[prop] === this.constructor.prototype[prop]) {
delete object[prop];
}
}, this);
return object;
},
/**
* Returns a string representation of an instance
* @return {String}
*/
toString: function() {
return "#<fabric." + capitalize(this.type) + ">";
},
/**
* Basic getter
* @param {String} property Property name
* @return {Any} value of a property
*/
get: function(property) {
return this[property];
},
/**
* Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
* @param {String|Object} key Property name or object (if object, iterate over the object properties)
* @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one)
* @return {fabric.Object} thisArg
* @chainable
*/
set: function(key, value) {
if (typeof key === 'object') {
for (var prop in key) {
this._set(prop, key[prop]);
}
}
else {
if (typeof value === 'function' && key !== 'clipTo') {
this._set(key, value(this.get(key)));
}
else {
this._set(key, value);
}
}
return this;
},
/**
* @private
* @param {String} key
* @param {Any} value
* @return {fabric.Object} thisArg
*/
_set: function(key, value) {
var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY');
if (shouldConstrainValue) {
value = this._constrainScale(value);
}
if (key === 'scaleX' && value < 0) {
this.flipX = !this.flipX;
value *= -1;
}
else if (key === 'scaleY' && value < 0) {
this.flipY = !this.flipY;
value *= -1;
}
else if (key === 'width' || key === 'height') {
this.minScaleLimit = toFixed(Math.min(0.1, 1/Math.max(this.width, this.height)), 2);
}
else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) {
value = new fabric.Shadow(value);
}
this[key] = value;
return this;
},
/**
* Toggles specified property from `true` to `false` or from `false` to `true`
* @param {String} property Property to toggle
* @return {fabric.Object} thisArg
* @chainable
*/
toggle: function(property) {
var value = this.get(property);
if (typeof value === 'boolean') {
this.set(property, !value);
}
return this;
},
/**
* Sets sourcePath of an object
* @param {String} value Value to set sourcePath to
* @return {fabric.Object} thisArg
* @chainable
*/
setSourcePath: function(value) {
this.sourcePath = value;
return this;
},
/**
* Renders an object on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Boolean} [noTransform] When true, context is not transformed
*/
render: function(ctx, noTransform) {
// do not render if width/height are zeros or object is not visible
if (this.width === 0 || this.height === 0 || !this.visible) return;
ctx.save();
var m = this.transformMatrix;
if (m && !this.group) {
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
if (!noTransform) {
this.transform(ctx);
}
if (this.stroke) {
ctx.lineWidth = this.strokeWidth;
ctx.lineCap = this.strokeLineCap;
ctx.lineJoin = this.strokeLineJoin;
ctx.miterLimit = this.strokeMiterLimit;
ctx.strokeStyle = this.stroke.toLive
? this.stroke.toLive(ctx)
: this.stroke;
}
if (this.overlayFill) {
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
ctx.fillStyle = this.fill.toLive
? this.fill.toLive(ctx)
: this.fill;
}
if (m && this.group) {
ctx.translate(-this.group.width/2, -this.group.height/2);
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
this._setShadow(ctx);
this.clipTo && fabric.util.clipContext(this, ctx);
this._render(ctx, noTransform);
this.clipTo && ctx.restore();
this._removeShadow(ctx);
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.drawControls(ctx);
}
ctx.restore();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_setShadow: function(ctx) {
if (!this.shadow) return;
ctx.shadowColor = this.shadow.color;
ctx.shadowBlur = this.shadow.blur;
ctx.shadowOffsetX = this.shadow.offsetX;
ctx.shadowOffsetY = this.shadow.offsetY;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_removeShadow: function(ctx) {
ctx.shadowColor = '';
ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderFill: function(ctx) {
if (!this.fill) return;
if (this.fill.toLive) {
ctx.save();
ctx.translate(
-this.width / 2 + this.fill.offsetX || 0,
-this.height / 2 + this.fill.offsetY || 0);
}
ctx.fill();
if (this.fill.toLive) {
ctx.restore();
}
if (this.shadow && !this.shadow.affectStroke) {
this._removeShadow(ctx);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderStroke: function(ctx) {
if (!this.stroke) return;
ctx.save();
if (this.strokeDashArray) {
// Spec requires the concatenation of two copies the dash list when the number of elements is odd
if (1 & this.strokeDashArray.length) {
this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray);
}
if (supportsLineDash) {
ctx.setLineDash(this.strokeDashArray);
this._stroke && this._stroke(ctx);
}
else {
this._renderDashedStroke && this._renderDashedStroke(ctx);
}
ctx.stroke();
}
else {
this._stroke ? this._stroke(ctx) : ctx.stroke();
}
this._removeShadow(ctx);
ctx.restore();
},
/**
* Clones an instance
* @param {Function} callback Callback is invoked with a clone as a first argument
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {fabric.Object} clone of an instance
*/
clone: function(callback, propertiesToInclude) {
if (this.constructor.fromObject) {
return this.constructor.fromObject(this.toObject(propertiesToInclude), callback);
}
return new fabric.Object(this.toObject(propertiesToInclude));
},
/**
* Creates an instance of fabric.Image out of an object
* @param callback {Function} callback, invoked with an instance as a first argument
* @return {fabric.Object} thisArg
*/
cloneAsImage: function(callback) {
var dataUrl = this.toDataURL();
fabric.util.loadImage(dataUrl, function(img) {
if (callback) {
callback(new fabric.Image(img));
}
});
return this;
},
/**
* Converts an object into a data-url-like string
* @param {Object} options Options object
* @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png"
* @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg.
* @param {Number} [options.multiplier=1] Multiplier to scale by
* @param {Number} [options.left] Cropping left offset
* @param {Number} [options.top] Cropping top offset
* @param {Number} [options.width] Cropping width
* @param {Number} [options.height] Cropping height
* @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format
*/
toDataURL: function(options) {
options || (options = { });
var el = fabric.util.createCanvasElement(),
boundingRect = this.getBoundingRect();
el.width = boundingRect.width;
el.height = boundingRect.height;
fabric.util.wrapElement(el, 'div');
var canvas = new fabric.Canvas(el);
// to avoid common confusion https://github.com/kangax/fabric.js/issues/806
if (options.format === 'jpg') {
options.format = 'jpeg';
}
if (options.format === 'jpeg') {
canvas.backgroundColor = '#fff';
}
var origParams = {
active: this.get('active'),
left: this.getLeft(),
top: this.getTop()
};
this.set('active', false);
this.setPositionByOrigin(new fabric.Point(el.width / 2, el.height / 2), 'center', 'center');
canvas.add(this);
var data = canvas.toDataURL(options);
this.set(origParams).setCoords();
canvas.dispose();
canvas = null;
return data;
},
/**
* Returns true if specified type is identical to the type of an instance
* @param type {String} type Type to check against
* @return {Boolean}
*/
isType: function(type) {
return this.type === type;
},
/**
* Makes object's color grayscale
* @return {fabric.Object} thisArg
*/
toGrayscale: function() {
var fillValue = this.get('fill');
if (fillValue) {
this.set('overlayFill', new fabric.Color(fillValue).toGrayscale().toRgb());
}
return this;
},
/**
* Returns complexity of an instance
* @return {Number} complexity of this instance
*/
complexity: function() {
return 0;
},
/**
* Returns a JSON representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} JSON
*/
toJSON: function(propertiesToInclude) {
// delegate, not alias
return this.toObject(propertiesToInclude);
},
/**
* Sets gradient (fill or stroke) of an object
* <b>Backwards incompatibility note:</b> This method was named "setGradientFill" until v1.1.0
* @param {String} property Property name 'stroke' or 'fill'
* @param {Object} [options] Options object
* @param {String} [options.type] Type of gradient 'radial' or 'linear'
* @param {Number} [options.x1=0] x-coordinate of start point
* @param {Number} [options.y1=0] y-coordinate of start point
* @param {Number} [options.x2=0] x-coordinate of end point
* @param {Number} [options.y2=0] y-coordinate of end point
* @param {Number} [options.r1=0] Radius of start point (only for radial gradients)
* @param {Number} [options.r2=0] Radius of end point (only for radial gradients)
* @param {Object} [options.colorStops] Color stops object eg. {0: 'ff0000', 1: '000000'}
* @return {fabric.Object} thisArg
* @chainable
*/
setGradient: function(property, options) {
options || (options = { });
var gradient = {colorStops: []};
gradient.type = options.type || (options.r1 || options.r2 ? 'radial' : 'linear');
gradient.coords = {
x1: options.x1,
y1: options.y1,
x2: options.x2,
y2: options.y2
};
if (options.r1 || options.r2) {
gradient.coords.r1 = options.r1;
gradient.coords.r2 = options.r2;
}
for (var position in options.colorStops) {
var color = new fabric.Color(options.colorStops[position]);
gradient.colorStops.push({offset: position, color: color.toRgb(), opacity: color.getAlpha()});
}
return this.set(property, fabric.Gradient.forObject(this, gradient));
},
/**
* Sets pattern fill of an object
* @param {Object} options Options object
* @param {(String|HTMLImageElement)} options.source Pattern source
* @param {String} [options.repeat=repeat] Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat)
* @param {Number} [options.offsetX=0] Pattern horizontal offset from object's left/top corner
* @param {Number} [options.offsetY=0] Pattern vertical offset from object's left/top corner
* @return {fabric.Object} thisArg
* @chainable
*/
setPatternFill: function(options) {
return this.set('fill', new fabric.Pattern(options));
},
/**
* Sets shadow of an object
* @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)")
* @param {String} [options.color=rgb(0,0,0)] Shadow color
* @param {Number} [options.blur=0] Shadow blur
* @param {Number} [options.offsetX=0] Shadow horizontal offset
* @param {Number} [options.offsetY=0] Shadow vertical offset
* @return {fabric.Object} thisArg
* @chainable
*/
setShadow: function(options) {
return this.set('shadow', new fabric.Shadow(options));
},
/**
* Sets "color" of an instance (alias of `set('fill', &hellip;)`)
* @param {String} color Color value
* @return {fabric.Text} thisArg
* @chainable
*/
setColor: function(color) {
this.set('fill', color);
return this;
},
/**
* Centers object horizontally on canvas to which it was added last.
* You might need to call `setCoords` on an object after centering, to update controls area.
* @return {fabric.Object} thisArg
* @chainable
*/
centerH: function () {
this.canvas.centerObjectH(this);
return this;
},
/**
* Centers object vertically on canvas to which it was added last.
* You might need to call `setCoords` on an object after centering, to update controls area.
* @return {fabric.Object} thisArg
* @chainable
*/
centerV: function () {
this.canvas.centerObjectV(this);
return this;
},
/**
* Centers object vertically and horizontally on canvas to which is was added last
* You might need to call `setCoords` on an object after centering, to update controls area.
* @return {fabric.Object} thisArg
* @chainable
*/
center: function () {
return this.centerH().centerV();
},
/**
* Removes object from canvas to which it was added last
* @return {fabric.Object} thisArg
* @chainable
*/
remove: function() {
return this.canvas.remove(this);
},
/**
* Moves an object to the bottom of the stack of drawn objects
* @return {fabric.Object} thisArg
* @chainable
*/
sendToBack: function() {
if (this.group) {
fabric.StaticCanvas.prototype.sendToBack.call(this.group, this);
}
else {
this.canvas.sendToBack(this);
}
return this;
},
/**
* Moves an object to the top of the stack of drawn objects
* @return {fabric.Object} thisArg
* @chainable
*/
bringToFront: function() {
if (this.group) {
fabric.StaticCanvas.prototype.bringToFront.call(this.group, this);
}
else {
this.canvas.bringToFront(this);
}
return this;
},
/**
* Moves an object down in stack of drawn objects
* @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object
* @return {fabric.Object} thisArg
* @chainable
*/
sendBackwards: function(intersecting) {
if (this.group) {
fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting);
}
else {
this.canvas.sendBackwards(this, intersecting);
}
return this;
},
/**
* Moves an object up in stack of drawn objects
* @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object
* @return {fabric.Object} thisArg
* @chainable
*/
bringForward: function(intersecting) {
if (this.group) {
fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting);
}
else {
this.canvas.bringForward(this, intersecting);
}
return this;
},
/**
* Moves an object to specified level in stack of drawn objects
* @param {Number} index New position of object
* @return {fabric.Object} thisArg
* @chainable
*/
moveTo: function(index) {
if (this.group) {
fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index);
}
else {
this.canvas.moveTo(this, index);
}
return this;
}
});
fabric.util.createAccessors(fabric.Object);
/**
* Alias for {@link fabric.Object.prototype.setAngle}
* @alias rotate -> setAngle
* @memberof fabric.Object
*/
fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle;
extend(fabric.Object.prototype, fabric.Observable);
/**
* Defines the number of fraction digits when serializing object values. You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc.
* @static
* @memberof fabric.Object
* @constant
* @type Number
*/
fabric.Object.NUM_FRACTION_DIGITS = 2;
/**
* @static
* @memberof fabric.Object
* @type Number
*/
fabric.Object.__uid = 0;
})(typeof exports !== 'undefined' ? exports : this);
(function() {
var degreesToRadians = fabric.util.degreesToRadians;
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
* Translates the coordinates from origin to center coordinates (based on the object's dimensions)
* @param {fabric.Point} point The point which corresponds to the originX and originY params
* @param {String} originX Horizontal origin: 'left', 'center' or 'right'
* @param {String} originY Vertical origin: 'top', 'center' or 'bottom'
* @return {fabric.Point}
*/
translateToCenterPoint: function(point, originX, originY) {
var cx = point.x, cy = point.y;
if ( originX === "left" ) {
cx = point.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) )/ 2;
}
else if ( originX === "right" ) {
cx = point.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2;
}
if ( originY === "top" ) {
cy = point.y +( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2;
}
else if ( originY === "bottom" ) {
cy = point.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) ) / 2;
}
// Apply the reverse rotation to the point (it's already scaled properly)
return fabric.util.rotatePoint(new fabric.Point(cx, cy), point, degreesToRadians(this.angle));
},
/**
* Translates the coordinates from center to origin coordinates (based on the object's dimensions)
* @param {fabric.Point} point The point which corresponds to center of the object
* @param {String} originX Horizontal origin: 'left', 'center' or 'right'
* @param {String} originY Vertical origin: 'top', 'center' or 'bottom'
* @return {fabric.Point}
*/
translateToOriginPoint: function(center, originX, originY) {
var x = center.x, y = center.y;
// Get the point coordinates
if ( originX === "left" ) {
x = center.x - ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2;
}
else if ( originX === "right" ) {
x = center.x + ( this.getWidth() + (this.strokeWidth*this.scaleX) ) / 2;
}
if ( originY === "top" ) {
y = center.y - ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2;
}
else if ( originY === "bottom" ) {
y = center.y + ( this.getHeight() + (this.strokeWidth*this.scaleY) )/ 2;
}
// Apply the rotation to the point (it's already scaled properly)
return fabric.util.rotatePoint(new fabric.Point(x, y), center, degreesToRadians(this.angle));
},
/**
* Returns the real center coordinates of the object
* @return {fabric.Point}
*/
getCenterPoint: function() {
var leftTop = new fabric.Point(this.left, this.top);
return this.translateToCenterPoint(leftTop, this.originX, this.originY);
},
/**
* Returns the coordinates of the object based on center coordinates
* @param {fabric.Point} point The point which corresponds to the originX and originY params
* @return {fabric.Point}
*/
// getOriginPoint: function(center) {
// return this.translateToOriginPoint(center, this.originX, this.originY);
// },
/**
* Returns the coordinates of the object as if it has a different origin
* @param {String} originX Horizontal origin: 'left', 'center' or 'right'
* @param {String} originY Vertical origin: 'top', 'center' or 'bottom'
* @return {fabric.Point}
*/
getPointByOrigin: function(originX, originY) {
var center = this.getCenterPoint();
return this.translateToOriginPoint(center, originX, originY);
},
/**
* Returns the point in local coordinates
* @param {fabric.Point} The point relative to the global coordinate system
* @return {fabric.Point}
*/
toLocalPoint: function(point, originX, originY) {
var center = this.getCenterPoint();
var x, y;
if (originX !== undefined && originY !== undefined) {
if ( originX === "left" ) {
x = center.x - (this.getWidth() + this.strokeWidth*this.scaleX) / 2;
}
else if ( originX === "right" ) {
x = center.x + (this.getWidth() + this.strokeWidth*this.scaleX)/ 2;
}
else {
x = center.x;
}
if ( originY === "top" ) {
y = center.y - (this.getHeight() + this.strokeWidth*this.scaleY) / 2;
}
else if ( originY === "bottom" ) {
y = center.y + (this.getHeight() + this.strokeWidth*this.scaleY)/ 2;
}
else {
y = center.y;
}
}
else {
x = this.left;
y = this.top;
}
return fabric.util.rotatePoint(new fabric.Point(point.x, point.y), center, -degreesToRadians(this.angle))
.subtractEquals(new fabric.Point(x, y));
},
/**
* Returns the point in global coordinates
* @param {fabric.Point} The point relative to the local coordinate system
* @return {fabric.Point}
*/
// toGlobalPoint: function(point) {
// return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top));
// },
/**
* Sets the position of the object taking into consideration the object's origin
* @param {fabric.Point} point The new position of the object
* @param {String} originX Horizontal origin: 'left', 'center' or 'right'
* @param {String} originY Vertical origin: 'top', 'center' or 'bottom'
* @return {void}
*/
setPositionByOrigin: function(pos, originX, originY) {
var center = this.translateToCenterPoint(pos, originX, originY);
var position = this.translateToOriginPoint(center, this.originX, this.originY);
this.set('left', position.x);
this.set('top', position.y);
},
/**
* @param {String} to One of left, center, right
*/
adjustPosition: function(to) {
var angle = degreesToRadians(this.angle);
var hypotHalf = this.getWidth() / 2;
var xHalf = Math.cos(angle) * hypotHalf;
var yHalf = Math.sin(angle) * hypotHalf;
var hypotFull = this.getWidth();
var xFull = Math.cos(angle) * hypotFull;
var yFull = Math.sin(angle) * hypotFull;
if (this.originX === 'center' && to === 'left' ||
this.originX === 'right' && to === 'center') {
// move half left
this.left -= xHalf;
this.top -= yHalf;
}
else if (this.originX === 'left' && to === 'center' ||
this.originX === 'center' && to === 'right') {
// move half right
this.left += xHalf;
this.top += yHalf;
}
else if (this.originX === 'left' && to === 'right') {
// move full right
this.left += xFull;
this.top += yFull;
}
else if (this.originX === 'right' && to === 'left') {
// move full left
this.left -= xFull;
this.top -= yFull;
}
this.setCoords();
this.originX = to;
},
/**
* @private
*/
_getLeftTopCoords: function() {
var angle = degreesToRadians(this.angle);
var hypotHalf = this.getWidth() / 2;
var xHalf = Math.cos(angle) * hypotHalf;
var yHalf = Math.sin(angle) * hypotHalf;
var x = this.left;
var y = this.top;
if (this.originX === 'center' || this.originX === 'right') {
x -= xHalf;
}
if (this.originY === 'center' || this.originY === 'bottom') {
y -= yHalf;
}
return { x: x, y: y };
}
});
})();
(function() {
var degreesToRadians = fabric.util.degreesToRadians;
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
* Object containing coordinates of object's controls
* @type Object
* @default
*/
oCoords: null,
/**
* Checks if object intersects with an area formed by 2 points
* @param {Object} pointTL top-left point of area
* @param {Object} pointBR bottom-right point of area
* @return {Boolean} true if object intersects with an area formed by 2 points
*/
intersectsWithRect: function(pointTL, pointBR) {
var oCoords = this.oCoords,
tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br = new fabric.Point(oCoords.br.x, oCoords.br.y);
var intersection = fabric.Intersection.intersectPolygonRectangle(
[tl, tr, br, bl],
pointTL,
pointBR
);
return intersection.status === 'Intersection';
},
/**
* Checks if object intersects with another object
* @param {Object} other Object to test
* @return {Boolean} true if object intersects with another object
*/
intersectsWithObject: function(other) {
// extracts coords
function getCoords(oCoords) {
return {
tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br: new fabric.Point(oCoords.br.x, oCoords.br.y)
};
}
var thisCoords = getCoords(this.oCoords),
otherCoords = getCoords(other.oCoords);
var intersection = fabric.Intersection.intersectPolygonPolygon(
[thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl],
[otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl]
);
return intersection.status === 'Intersection';
},
/**
* Checks if object is fully contained within area of another object
* @param {Object} other Object to test
* @return {Boolean} true if object is fully contained within area of another object
*/
isContainedWithinObject: function(other) {
var boundingRect = other.getBoundingRect(),
point1 = new fabric.Point(boundingRect.left, boundingRect.top),
point2 = new fabric.Point(boundingRect.left + boundingRect.width, boundingRect.top + boundingRect.height);
return this.isContainedWithinRect(point1, point2);
},
/**
* Checks if object is fully contained within area formed by 2 points
* @param {Object} pointTL top-left point of area
* @param {Object} pointBR bottom-right point of area
* @return {Boolean} true if object is fully contained within area formed by 2 points
*/
isContainedWithinRect: function(pointTL, pointBR) {
var boundingRect = this.getBoundingRect();
return (
boundingRect.left > pointTL.x &&
boundingRect.left + boundingRect.width < pointBR.x &&
boundingRect.top > pointTL.y &&
boundingRect.top + boundingRect.height < pointBR.y
);
},
/**
* Checks if point is inside the object
* @param {Object} point
* @return {Boolean} true if point is inside the object
*/
containsPoint: function(point) {
var lines = this._getImageLines(this.oCoords),
xPoints = this._findCrossPoints(point, lines);
// if xPoints is odd then point is inside the object
return (xPoints !== 0 && xPoints % 2 === 1);
},
/**
* Method that returns an object with the object edges in it, given the coordinates of the corners
* @private
* @param {Object} oCoords Coordinates of the object corners
*/
_getImageLines: function(oCoords) {
return {
topline: {
o: oCoords.tl,
d: oCoords.tr
},
rightline: {
o: oCoords.tr,
d: oCoords.br
},
bottomline: {
o: oCoords.br,
d: oCoords.bl
},
leftline: {
o: oCoords.bl,
d: oCoords.tl
}
};
},
/**
* Helper method to determine how many cross points are between the 4 object edges
* and the horizontal line determined by a point on canvas
* @private
* @param {Object} point
* @param {Object} oCoords Coordinates of the image being evaluated
*/
_findCrossPoints: function(point, oCoords) {
var b1, b2, a1, a2, xi, yi,
xcount = 0,
iLine;
for (var lineKey in oCoords) {
iLine = oCoords[lineKey];
// optimisation 1: line below point. no cross
if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) {
continue;
}
// optimisation 2: line above point. no cross
if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) {
continue;
}
// optimisation 3: vertical line case
if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) {
xi = iLine.o.x;
yi = point.y;
}
// calculate the intersection point
else {
b1 = 0;
b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x);
a1 = point.y- b1 * point.x;
a2 = iLine.o.y - b2 * iLine.o.x;
xi = - (a1 - a2) / (b1 - b2);
yi = a1 + b1 * xi;
}
// dont count xi < point.x cases
if (xi >= point.x) {
xcount += 1;
}
// optimisation 4: specific for square images
if (xcount === 2) {
break;
}
}
return xcount;
},
/**
* Returns width of an object's bounding rectangle
* @deprecated since 1.0.4
* @return {Number} width value
*/
getBoundingRectWidth: function() {
return this.getBoundingRect().width;
},
/**
* Returns height of an object's bounding rectangle
* @deprecated since 1.0.4
* @return {Number} height value
*/
getBoundingRectHeight: function() {
return this.getBoundingRect().height;
},
/**
* Returns coordinates of object's bounding rectangle (left, top, width, height)
* @return {Object} Object with left, top, width, height properties
*/
getBoundingRect: function() {
this.oCoords || this.setCoords();
var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x];
var minX = fabric.util.array.min(xCoords);
var maxX = fabric.util.array.max(xCoords);
var width = Math.abs(minX - maxX);
var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y];
var minY = fabric.util.array.min(yCoords);
var maxY = fabric.util.array.max(yCoords);
var height = Math.abs(minY - maxY);
return {
left: minX,
top: minY,
width: width,
height: height
};
},
/**
* Returns width of an object
* @return {Number} width value
*/
getWidth: function() {
return this.width * this.scaleX;
},
/**
* Returns height of an object
* @return {Number} height value
*/
getHeight: function() {
return this.height * this.scaleY;
},
/**
* Makes sure the scale is valid and modifies it if necessary
* @private
* @param {Number} value
* @return {Number}
*/
_constrainScale: function(value) {
if (Math.abs(value) < this.minScaleLimit) {
if (value < 0)
return -this.minScaleLimit;
else
return this.minScaleLimit;
}
return value;
},
/**
* Scales an object (equally by x and y)
* @param value {Number} scale factor
* @return {fabric.Object} thisArg
* @chainable
*/
scale: function(value) {
value = this._constrainScale(value);
if (value < 0) {
this.flipX = !this.flipX;
this.flipY = !this.flipY;
value *= -1;
}
this.scaleX = value;
this.scaleY = value;
this.setCoords();
return this;
},
/**
* Scales an object to a given width, with respect to bounding box (scaling by x/y equally)
* @param value {Number} new width value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToWidth: function(value) {
// adjust to bounding rect factor so that rotated shapes would fit as well
var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth();
return this.scale(value / this.width / boundingRectFactor);
},
/**
* Scales an object to a given height, with respect to bounding box (scaling by x/y equally)
* @param value {Number} new height value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToHeight: function(value) {
// adjust to bounding rect factor so that rotated shapes would fit as well
var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight();
return this.scale(value / this.height / boundingRectFactor);
},
/**
* Sets corner position coordinates based on current angle, width and height
* @return {fabric.Object} thisArg
* @chainable
*/
setCoords: function() {
var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0,
padding = this.padding,
theta = degreesToRadians(this.angle);
this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2;
this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2;
// If width is negative, make postive. Fixes path selection issue
if (this.currentWidth < 0) {
this.currentWidth = Math.abs(this.currentWidth);
}
var _hypotenuse = Math.sqrt(
Math.pow(this.currentWidth / 2, 2) +
Math.pow(this.currentHeight / 2, 2));
var _angle = Math.atan(isFinite(this.currentHeight / this.currentWidth) ? this.currentHeight / this.currentWidth : 0);
// offset added for rotate and scale actions
var offsetX = Math.cos(_angle + theta) * _hypotenuse,
offsetY = Math.sin(_angle + theta) * _hypotenuse,
sinTh = Math.sin(theta),
cosTh = Math.cos(theta);
var coords = this.getCenterPoint();
var tl = {
x: coords.x - offsetX,
y: coords.y - offsetY
};
var tr = {
x: tl.x + (this.currentWidth * cosTh),
y: tl.y + (this.currentWidth * sinTh)
};
var br = {
x: tr.x - (this.currentHeight * sinTh),
y: tr.y + (this.currentHeight * cosTh)
};
var bl = {
x: tl.x - (this.currentHeight * sinTh),
y: tl.y + (this.currentHeight * cosTh)
};
var ml = {
x: tl.x - (this.currentHeight/2 * sinTh),
y: tl.y + (this.currentHeight/2 * cosTh)
};
var mt = {
x: tl.x + (this.currentWidth/2 * cosTh),
y: tl.y + (this.currentWidth/2 * sinTh)
};
var mr = {
x: tr.x - (this.currentHeight/2 * sinTh),
y: tr.y + (this.currentHeight/2 * cosTh)
};
var mb = {
x: bl.x + (this.currentWidth/2 * cosTh),
y: bl.y + (this.currentWidth/2 * sinTh)
};
var mtr = {
x: mt.x,
y: mt.y
};
// debugging
// setTimeout(function() {
// canvas.contextTop.fillStyle = 'green';
// canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
// canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
// canvas.contextTop.fillRect(br.x, br.y, 3, 3);
// canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
// canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
// canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
// canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
// canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
// }, 50);
this.oCoords = {
// corners
tl: tl, tr: tr, br: br, bl: bl,
// middle
ml: ml, mt: mt, mr: mr, mb: mb,
// rotating point
mtr: mtr
};
// set coordinates of the draggable boxes in the corners used to scale/rotate the image
this._setCornerCoords && this._setCornerCoords();
return this;
}
});
})();
/*
Depends on `stateProperties`
*/
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
* Returns true if object state (one of its state properties) was changed
* @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called
*/
hasStateChanged: function() {
return this.stateProperties.some(function(prop) {
return this.get(prop) !== this.originalState[prop];
}, this);
},
/**
* Saves state of an object
* @param {Object} [options] Object with additional `stateProperties` array to include when saving state
* @return {fabric.Object} thisArg
*/
saveState: function(options) {
this.stateProperties.forEach(function(prop) {
this.originalState[prop] = this.get(prop);
}, this);
if (options && options.stateProperties) {
options.stateProperties.forEach(function(prop) {
this.originalState[prop] = this.get(prop);
}, this);
}
return this;
},
/**
* Setups state of an object
* @return {fabric.Object} thisArg
*/
setupState: function() {
this.originalState = { };
this.saveState();
return this;
}
});
(function(){
var getPointer = fabric.util.getPointer,
degreesToRadians = fabric.util.degreesToRadians;
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
* Determines which one of the four corners has been clicked
* @private
* @param {Event} e Event object
* @param {Object} offset Canvas offset
* @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
*/
_findTargetCorner: function(e, offset) {
if (!this.hasControls || !this.active) return false;
var pointer = getPointer(e, this.canvas.upperCanvasEl),
ex = pointer.x - offset.left,
ey = pointer.y - offset.top,
xPoints,
lines;
for (var i in this.oCoords) {
if (i === 'mtr' && !this.hasRotatingPoint) {
continue;
}
if (this.get('lockUniScaling') && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) {
continue;
}
lines = this._getImageLines(this.oCoords[i].corner);
// debugging
// canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2);
// canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2);
// canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2);
xPoints = this._findCrossPoints({x: ex, y: ey}, lines);
if (xPoints !== 0 && xPoints % 2 === 1) {
this.__corner = i;
return i;
}
}
return false;
},
/**
* Sets the coordinates of the draggable boxes in the corners of
* the image used to scale/rotate it.
* @private
*/
_setCornerCoords: function() {
var coords = this.oCoords,
theta = degreesToRadians(this.angle),
newTheta = degreesToRadians(45 - this.angle),
cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2,
cosHalfOffset = cornerHypotenuse * Math.cos(newTheta),
sinHalfOffset = cornerHypotenuse * Math.sin(newTheta),
sinTh = Math.sin(theta),
cosTh = Math.cos(theta);
coords.tl.corner = {
tl: {
x: coords.tl.x - sinHalfOffset,
y: coords.tl.y - cosHalfOffset
},
tr: {
x: coords.tl.x + cosHalfOffset,
y: coords.tl.y - sinHalfOffset
},
bl: {
x: coords.tl.x - cosHalfOffset,
y: coords.tl.y + sinHalfOffset
},
br: {
x: coords.tl.x + sinHalfOffset,
y: coords.tl.y + cosHalfOffset
}
};
coords.tr.corner = {
tl: {
x: coords.tr.x - sinHalfOffset,
y: coords.tr.y - cosHalfOffset
},
tr: {
x: coords.tr.x + cosHalfOffset,
y: coords.tr.y - sinHalfOffset
},
br: {
x: coords.tr.x + sinHalfOffset,
y: coords.tr.y + cosHalfOffset
},
bl: {
x: coords.tr.x - cosHalfOffset,
y: coords.tr.y + sinHalfOffset
}
};
coords.bl.corner = {
tl: {
x: coords.bl.x - sinHalfOffset,
y: coords.bl.y - cosHalfOffset
},
bl: {
x: coords.bl.x - cosHalfOffset,
y: coords.bl.y + sinHalfOffset
},
br: {
x: coords.bl.x + sinHalfOffset,
y: coords.bl.y + cosHalfOffset
},
tr: {
x: coords.bl.x + cosHalfOffset,
y: coords.bl.y - sinHalfOffset
}
};
coords.br.corner = {
tr: {
x: coords.br.x + cosHalfOffset,
y: coords.br.y - sinHalfOffset
},
bl: {
x: coords.br.x - cosHalfOffset,
y: coords.br.y + sinHalfOffset
},
br: {
x: coords.br.x + sinHalfOffset,
y: coords.br.y + cosHalfOffset
},
tl: {
x: coords.br.x - sinHalfOffset,
y: coords.br.y - cosHalfOffset
}
};
coords.ml.corner = {
tl: {
x: coords.ml.x - sinHalfOffset,
y: coords.ml.y - cosHalfOffset
},
tr: {
x: coords.ml.x + cosHalfOffset,
y: coords.ml.y - sinHalfOffset
},
bl: {
x: coords.ml.x - cosHalfOffset,
y: coords.ml.y + sinHalfOffset
},
br: {
x: coords.ml.x + sinHalfOffset,
y: coords.ml.y + cosHalfOffset
}
};
coords.mt.corner = {
tl: {
x: coords.mt.x - sinHalfOffset,
y: coords.mt.y - cosHalfOffset
},
tr: {
x: coords.mt.x + cosHalfOffset,
y: coords.mt.y - sinHalfOffset
},
bl: {
x: coords.mt.x - cosHalfOffset,
y: coords.mt.y + sinHalfOffset
},
br: {
x: coords.mt.x + sinHalfOffset,
y: coords.mt.y + cosHalfOffset
}
};
coords.mr.corner = {
tl: {
x: coords.mr.x - sinHalfOffset,
y: coords.mr.y - cosHalfOffset
},
tr: {
x: coords.mr.x + cosHalfOffset,
y: coords.mr.y - sinHalfOffset
},
bl: {
x: coords.mr.x - cosHalfOffset,
y: coords.mr.y + sinHalfOffset
},
br: {
x: coords.mr.x + sinHalfOffset,
y: coords.mr.y + cosHalfOffset
}
};
coords.mb.corner = {
tl: {
x: coords.mb.x - sinHalfOffset,
y: coords.mb.y - cosHalfOffset
},
tr: {
x: coords.mb.x + cosHalfOffset,
y: coords.mb.y - sinHalfOffset
},
bl: {
x: coords.mb.x - cosHalfOffset,
y: coords.mb.y + sinHalfOffset
},
br: {
x: coords.mb.x + sinHalfOffset,
y: coords.mb.y + cosHalfOffset
}
};
coords.mtr.corner = {
tl: {
x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset)
},
tr: {
x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset)
},
bl: {
x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset)
},
br: {
x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset),
y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset)
}
};
},
/**
* Draws borders of an object's bounding box.
* Requires public properties: width, height
* Requires public options: padding, borderColor
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawBorders: function(ctx) {
if (!this.hasBorders) return this;
var padding = this.padding,
padding2 = padding * 2,
strokeWidth = ~~(this.strokeWidth / 2) * 2; // Round down to even number
ctx.save();
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = this.borderColor;
var scaleX = 1 / this._constrainScale(this.scaleX),
scaleY = 1 / this._constrainScale(this.scaleY);
ctx.lineWidth = 1 / this.borderScaleFactor;
ctx.scale(scaleX, scaleY);
var w = this.getWidth(),
h = this.getHeight();
ctx.strokeRect(
~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) - 0.5, // offset needed to make lines look sharper
~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) - 0.5,
~~(w + padding2 + strokeWidth * this.scaleX) + 1, // double offset needed to make lines look sharper
~~(h + padding2 + strokeWidth * this.scaleY) + 1
);
if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) {
var rotateHeight = (
this.flipY
? h + (strokeWidth * this.scaleY) + (padding * 2)
: -h - (strokeWidth * this.scaleY) - (padding * 2)
) / 2;
ctx.beginPath();
ctx.moveTo(0, rotateHeight);
ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset));
ctx.closePath();
ctx.stroke();
}
ctx.restore();
return this;
},
/**
* Draws corners of an object's bounding box.
* Requires public properties: width, height, scaleX, scaleY
* Requires public options: cornerSize, padding
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawControls: function(ctx) {
if (!this.hasControls) return this;
var size = this.cornerSize,
size2 = size / 2,
strokeWidth2 = ~~(this.strokeWidth / 2), // half strokeWidth rounded down
left = -(this.width / 2),
top = -(this.height / 2),
_left,
_top,
sizeX = size / this.scaleX,
sizeY = size / this.scaleY,
paddingX = this.padding / this.scaleX,
paddingY = this.padding / this.scaleY,
scaleOffsetY = size2 / this.scaleY,
scaleOffsetX = size2 / this.scaleX,
scaleOffsetSizeX = (size2 - size) / this.scaleX,
scaleOffsetSizeY = (size2 - size) / this.scaleY,
height = this.height,
width = this.width,
methodName = this.transparentCorners ? 'strokeRect' : 'fillRect',
transparent = this.transparentCorners,
isVML = typeof G_vmlCanvasManager !== 'undefined';
ctx.save();
ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY);
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
ctx.strokeStyle = ctx.fillStyle = this.cornerColor;
// top-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// top-right
_left = left + width - scaleOffsetX + strokeWidth2 + paddingX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// bottom-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// bottom-right
_left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
if (!this.get('lockUniScaling')) {
// middle-top
_left = left + width/2 - scaleOffsetX;
_top = top - scaleOffsetY - strokeWidth2 - paddingY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-bottom
_left = left + width/2 - scaleOffsetX;
_top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-right
_left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
_top = top + height/2 - scaleOffsetY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
// middle-left
_left = left - scaleOffsetX - strokeWidth2 - paddingX;
_top = top + height/2 - scaleOffsetY;
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
}
// middle-top-rotate
if (this.hasRotatingPoint) {
_left = left + width/2 - scaleOffsetX;
_top = this.flipY ?
(top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY)
: (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY);
isVML || transparent || ctx.clearRect(_left, _top, sizeX, sizeY);
ctx[methodName](_left, _top, sizeX, sizeY);
}
ctx.restore();
return this;
}
});
})();
fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ {
/**
* Animation duration (in ms) for fx* methods
* @type Number
* @default
*/
FX_DURATION: 500,
/**
* Centers object horizontally with animation.
* @param {fabric.Object} object Object to center
* @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties
* @param {Function} [callbacks.onComplete] Invoked on completion
* @param {Function} [callbacks.onChange] Invoked on every step of animation
* @return {fabric.Canvas} thisArg
* @chainable
*/
fxCenterObjectH: function (object, callbacks) {
callbacks = callbacks || { };
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: object.get('left'),
endValue: this.getCenter().left,
duration: this.FX_DURATION,
onChange: function(value) {
object.set('left', value);
_this.renderAll();
onChange();
},
onComplete: function() {
object.setCoords();
onComplete();
}
});
return this;
},
/**
* Centers object vertically with animation.
* @param {fabric.Object} object Object to center
* @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties
* @param {Function} [callbacks.onComplete] Invoked on completion
* @param {Function} [callbacks.onChange] Invoked on every step of animation
* @return {fabric.Canvas} thisArg
* @chainable
*/
fxCenterObjectV: function (object, callbacks) {
callbacks = callbacks || { };
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: object.get('top'),
endValue: this.getCenter().top,
duration: this.FX_DURATION,
onChange: function(value) {
object.set('top', value);
_this.renderAll();
onChange();
},
onComplete: function() {
object.setCoords();
onComplete();
}
});
return this;
},
/**
* Same as `fabric.Canvas#remove` but animated
* @param {fabric.Object} object Object to remove
* @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties
* @param {Function} [callbacks.onComplete] Invoked on completion
* @param {Function} [callbacks.onChange] Invoked on every step of animation
* @return {fabric.Canvas} thisArg
* @chainable
*/
fxRemove: function (object, callbacks) {
callbacks = callbacks || { };
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: object.get('opacity'),
endValue: 0,
duration: this.FX_DURATION,
onStart: function() {
object.set('active', false);
},
onChange: function(value) {
object.set('opacity', value);
_this.renderAll();
onChange();
},
onComplete: function () {
_this.remove(object);
onComplete();
}
});
return this;
}
});
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
* Animates object's properties
* @param {String|Object} property to animate (if string) or properties to animate (if object)
* @param {Number|Object} value to animate property to (if string was given first) or options object
* @return {fabric.Object} thisArg
* @chainable
*
* As object — multiple properties
*
* object.animate({ left: ..., top: ... });
* object.animate({ left: ..., top: ... }, { duration: ... });
*
* As string — one property
*
* object.animate('left', ...);
* object.animate('left', { duration: ... });
*
*/
animate: function() {
if (arguments[0] && typeof arguments[0] === 'object') {
var propsToAnimate = [ ], prop, skipCallbacks;
for (prop in arguments[0]) {
propsToAnimate.push(prop);
}
for (var i = 0, len = propsToAnimate.length; i<len; i++) {
prop = propsToAnimate[i];
skipCallbacks = i !== len - 1;
this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks);
}
}
else {
this._animate.apply(this, arguments);
}
return this;
},
/**
* @private
* @param {String} property Property to animate
* @param {String} to Value to animate to
* @param {Object} [options] Options object
* @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked
*/
_animate: function(property, to, options, skipCallbacks) {
var obj = this, propPair;
to = to.toString();
if (!options) {
options = { };
}
else {
options = fabric.util.object.clone(options);
}
if (~property.indexOf('.')) {
propPair = property.split('.');
}
var currentValue = propPair
? this.get(propPair[0])[propPair[1]]
: this.get(property);
if (!('from' in options)) {
options.from = currentValue;
}
if (~to.indexOf('=')) {
to = currentValue + parseFloat(to.replace('=', ''));
}
else {
to = parseFloat(to);
}
fabric.util.animate({
startValue: options.from,
endValue: to,
byValue: options.by,
easing: options.easing,
duration: options.duration,
abort: options.abort && function() {
return options.abort.call(obj);
},
onChange: function(value) {
if (propPair) {
obj[propPair[0]][propPair[1]] = value;
}
else {
obj.set(property, value);
}
if (skipCallbacks) return;
options.onChange && options.onChange();
},
onComplete: function() {
if (skipCallbacks) return;
obj.setCoords();
options.onComplete && options.onComplete();
}
});
}
});
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
coordProps = { 'x1': 1, 'x2': 1, 'y1': 1, 'y2': 1 },
supportsLineDash = fabric.StaticCanvas.supports('setLineDash');
if (fabric.Line) {
fabric.warn('fabric.Line is already defined');
return;
}
/**
* Line class
* @class fabric.Line
* @extends fabric.Object
*/
fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'line',
/**
* Constructor
* @param {Array} [points] Array of points
* @param {Object} [options] Options object
* @return {fabric.Line} thisArg
*/
initialize: function(points, options) {
options = options || { };
if (!points) {
points = [0, 0, 0, 0];
}
this.callSuper('initialize', options);
this.set('x1', points[0]);
this.set('y1', points[1]);
this.set('x2', points[2]);
this.set('y2', points[3]);
this._setWidthHeight(options);
},
/**
* @private
* @param {Object} [options] Options
*/
_setWidthHeight: function(options) {
options || (options = { });
this.set('width', Math.abs(this.x2 - this.x1) || 1);
this.set('height', Math.abs(this.y2 - this.y1) || 1);
this.set('left', 'left' in options ? options.left : (Math.min(this.x1, this.x2) + this.width / 2));
this.set('top', 'top' in options ? options.top : (Math.min(this.y1, this.y2) + this.height / 2));
},
/**
* @private
* @param {String} key
* @param {Any} value
*/
_set: function(key, value) {
this[key] = value;
if (key in coordProps) {
this._setWidthHeight();
}
return this;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
ctx.beginPath();
var isInPathGroup = this.group && this.group.type !== 'group';
if (isInPathGroup && !this.transformMatrix) {
ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top);
}
if (!this.strokeDashArray || this.strokeDashArray && supportsLineDash) {
// move from center (of virtual box) to its left/top corner
// we can't assume x1, y1 is top left and x2, y2 is bottom right
var xMult = this.x1 <= this.x2 ? -1 : 1;
var yMult = this.y1 <= this.y2 ? -1 : 1;
ctx.moveTo(
this.width === 1 ? 0 : (xMult * this.width / 2),
this.height === 1 ? 0 : (yMult * this.height / 2));
ctx.lineTo(
this.width === 1 ? 0 : (xMult * -1 * this.width / 2),
this.height === 1 ? 0 : (yMult * -1 * this.height / 2));
}
ctx.lineWidth = this.strokeWidth;
// TODO: test this
// make sure setting "fill" changes color of a line
// (by copying fillStyle to strokeStyle, since line is stroked, not filled)
var origStrokeStyle = ctx.strokeStyle;
ctx.strokeStyle = this.stroke || ctx.fillStyle;
this._renderStroke(ctx);
ctx.strokeStyle = origStrokeStyle;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderDashedStroke: function(ctx) {
var
xMult = this.x1 <= this.x2 ? -1 : 1,
yMult = this.y1 <= this.y2 ? -1 : 1,
x = this.width === 1 ? 0 : xMult * this.width / 2,
y = this.height === 1 ? 0 : yMult * this.height / 2;
ctx.beginPath();
fabric.util.drawDashedLine(ctx, x, y, -x, -y, this.strokeDashArray);
ctx.closePath();
},
/**
* Returns object representation of an instance
* @methd toObject
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
x1: this.get('x1'),
y1: this.get('y1'),
x2: this.get('x2'),
y2: this.get('y2')
});
},
/**
* Returns complexity of an instance
* @return {Number} complexity
*/
complexity: function() {
return 1;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement})
* @static
* @memberOf fabric.Line
* @see http://www.w3.org/TR/SVG/shapes.html#LineElement
*/
fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' '));
/**
* Returns fabric.Line instance from an SVG element
* @static
* @memberOf fabric.Line
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @return {fabric.Line} instance of fabric.Line
*/
fabric.Line.fromElement = function(element, options) {
var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES);
var points = [
parsedAttributes.x1 || 0,
parsedAttributes.y1 || 0,
parsedAttributes.x2 || 0,
parsedAttributes.y2 || 0
];
return new fabric.Line(points, extend(parsedAttributes, options));
};
/* _FROM_SVG_END_ */
/**
* Returns fabric.Line instance from an object representation
* @static
* @memberOf fabric.Line
* @param {Object} object Object to create an instance from
* @return {fabric.Line} instance of fabric.Line
*/
fabric.Line.fromObject = function(object) {
var points = [object.x1, object.y1, object.x2, object.y2];
return new fabric.Line(points, object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
piBy2 = Math.PI * 2,
extend = fabric.util.object.extend;
if (fabric.Circle) {
fabric.warn('fabric.Circle is already defined.');
return;
}
/**
* Circle class
* @class fabric.Circle
* @extends fabric.Object
*/
fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'circle',
/**
* Constructor
* @param {Object} [options] Options object
* @return {fabric.Circle} thisArg
*/
initialize: function(options) {
options = options || { };
this.set('radius', options.radius || 0);
this.callSuper('initialize', options);
},
/**
* @private
* @param {String} key
* @param {Any} value
* @return {fabric.Circle} thisArg
*/
_set: function(key, value) {
this.callSuper('_set', key, value);
if (key === 'radius') {
this.setRadius(value);
}
return this;
},
/**
* Returns object representation of an instance
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
radius: this.get('radius')
});
},
/**
* @private
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx, noTransform) {
ctx.beginPath();
// multiply by currently set alpha (the one that was set by path group where this object is contained, for example)
ctx.globalAlpha = this.group ? (ctx.globalAlpha * this.opacity) : this.opacity;
ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.radius, 0, piBy2, false);
ctx.closePath();
this._renderFill(ctx);
this._renderStroke(ctx);
},
/**
* Returns horizontal radius of an object (according to how an object is scaled)
* @return {Number}
*/
getRadiusX: function() {
return this.get('radius') * this.get('scaleX');
},
/**
* Returns vertical radius of an object (according to how an object is scaled)
* @return {Number}
*/
getRadiusY: function() {
return this.get('radius') * this.get('scaleY');
},
/**
* Sets radius of an object (and updates width accordingly)
* @return {Number}
*/
setRadius: function(value) {
this.radius = value;
this.set('width', value * 2).set('height', value * 2);
},
/**
* Returns complexity of an instance
* @return {Number} complexity of this instance
*/
complexity: function() {
return 1;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement})
* @static
* @memberOf fabric.Circle
* @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement
*/
fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' '));
/**
* Returns {@link fabric.Circle} instance from an SVG element
* @static
* @memberOf fabric.Circle
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @throws {Error} If value of `r` attribute is missing or invalid
* @return {fabric.Circle} Instance of fabric.Circle
*/
fabric.Circle.fromElement = function(element, options) {
options || (options = { });
var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES);
if (!isValidRadius(parsedAttributes)) {
throw new Error('value of `r` attribute is required and can not be negative');
}
if ('left' in parsedAttributes) {
parsedAttributes.left -= (options.width / 2) || 0;
}
if ('top' in parsedAttributes) {
parsedAttributes.top -= (options.height / 2) || 0;
}
var obj = new fabric.Circle(extend(parsedAttributes, options));
obj.cx = parseFloat(element.getAttribute('cx')) || 0;
obj.cy = parseFloat(element.getAttribute('cy')) || 0;
return obj;
};
/**
* @private
*/
function isValidRadius(attributes) {
return (('radius' in attributes) && (attributes.radius > 0));
}
/* _FROM_SVG_END_ */
/**
* Returns {@link fabric.Circle} instance from an object representation
* @static
* @memberOf fabric.Circle
* @param {Object} object Object to create an instance from
* @return {Object} Instance of fabric.Circle
*/
fabric.Circle.fromObject = function(object) {
return new fabric.Circle(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { });
if (fabric.Triangle) {
fabric.warn('fabric.Triangle is already defined');
return;
}
/**
* Triangle class
* @class fabric.Triangle
* @extends fabric.Object
* @return {fabric.Triangle} thisArg
*/
fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'triangle',
/**
* Constructor
* @param {Object} [options] Options object
* @return {Object} thisArg
*/
initialize: function(options) {
options = options || { };
this.callSuper('initialize', options);
this.set('width', options.width || 100)
.set('height', options.height || 100);
},
/**
* @private
* @param ctx {CanvasRenderingContext2D} Context to render on
*/
_render: function(ctx) {
var widthBy2 = this.width / 2,
heightBy2 = this.height / 2;
ctx.beginPath();
ctx.moveTo(-widthBy2, heightBy2);
ctx.lineTo(0, -heightBy2);
ctx.lineTo(widthBy2, heightBy2);
ctx.closePath();
this._renderFill(ctx);
this._renderStroke(ctx);
},
/**
* @private
* @param ctx {CanvasRenderingContext2D} Context to render on
*/
_renderDashedStroke: function(ctx) {
var widthBy2 = this.width / 2,
heightBy2 = this.height / 2;
ctx.beginPath();
fabric.util.drawDashedLine(ctx, -widthBy2, heightBy2, 0, -heightBy2, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, 0, -heightBy2, widthBy2, heightBy2, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, widthBy2, heightBy2, -widthBy2, heightBy2, this.strokeDashArray);
ctx.closePath();
},
/**
* Returns complexity of an instance
* @return {Number} complexity of this instance
*/
complexity: function() {
return 1;
}
});
/**
* Returns fabric.Triangle instance from an object representation
* @static
* @memberOf fabric.Triangle
* @param object {Object} object to create an instance from
* @return {Object} instance of Canvas.Triangle
*/
fabric.Triangle.fromObject = function(object) {
return new fabric.Triangle(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global){
var fabric = global.fabric || (global.fabric = { }),
piBy2 = Math.PI * 2,
extend = fabric.util.object.extend;
if (fabric.Ellipse) {
fabric.warn('fabric.Ellipse is already defined.');
return;
}
/**
* Ellipse class
* @class fabric.Ellipse
* @extends fabric.Object
* @return {fabric.Ellipse} thisArg
*/
fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'ellipse',
/**
* Horizontal radius
* @type Number
* @default
*/
rx: 0,
/**
* Vertical radius
* @type Number
* @default
*/
ry: 0,
/**
* Constructor
* @param {Object} [options] Options object
* @return {fabric.Ellipse} thisArg
*/
initialize: function(options) {
options = options || { };
this.callSuper('initialize', options);
this.set('rx', options.rx || 0);
this.set('ry', options.ry || 0);
this.set('width', this.get('rx') * 2);
this.set('height', this.get('ry') * 2);
},
/**
* Returns object representation of an instance
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
rx: this.get('rx'),
ry: this.get('ry')
});
},
/**
* Renders this instance on a given context
* @param ctx {CanvasRenderingContext2D} context to render on
* @param noTransform {Boolean} context is not transformed when set to true
*/
render: function(ctx, noTransform) {
// do not use `get` for perf. reasons
if (this.rx === 0 || this.ry === 0) return;
return this.callSuper('render', ctx, noTransform);
},
/**
* @private
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx, noTransform) {
ctx.beginPath();
ctx.save();
ctx.globalAlpha = this.group ? (ctx.globalAlpha * this.opacity) : this.opacity;
if (this.transformMatrix && this.group) {
ctx.translate(this.cx, this.cy);
}
ctx.transform(1, 0, 0, this.ry/this.rx, 0, 0);
ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.rx, 0, piBy2, false);
this._renderFill(ctx);
this._renderStroke(ctx);
ctx.restore();
},
/**
* Returns complexity of an instance
* @return {Number} complexity
*/
complexity: function() {
return 1;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement})
* @static
* @memberOf fabric.Ellipse
* @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement
*/
fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' '));
/**
* Returns {@link fabric.Ellipse} instance from an SVG element
* @static
* @memberOf fabric.Ellipse
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @return {fabric.Ellipse}
*/
fabric.Ellipse.fromElement = function(element, options) {
options || (options = { });
var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES);
var cx = parsedAttributes.left;
var cy = parsedAttributes.top;
if ('left' in parsedAttributes) {
parsedAttributes.left -= (options.width / 2) || 0;
}
if ('top' in parsedAttributes) {
parsedAttributes.top -= (options.height / 2) || 0;
}
var ellipse = new fabric.Ellipse(extend(parsedAttributes, options));
ellipse.cx = cx || 0;
ellipse.cy = cy || 0;
return ellipse;
};
/* _FROM_SVG_END_ */
/**
* Returns {@link fabric.Ellipse} instance from an object representation
* @static
* @memberOf fabric.Ellipse
* @param {Object} object Object to create an instance from
* @return {fabric.Ellipse}
*/
fabric.Ellipse.fromObject = function(object) {
return new fabric.Ellipse(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
if (fabric.Rect) {
console.warn('fabric.Rect is already defined');
return;
}
var stateProperties = fabric.Object.prototype.stateProperties.concat();
stateProperties.push('rx', 'ry');
/**
* Rectangle class
* @class fabric.Rect
* @extends fabric.Object
* @return {fabric.Rect} thisArg
*/
fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ {
/**
* List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged})
* as well as for history (undo/redo) purposes
* @type Array
*/
stateProperties: stateProperties,
/**
* Type of an object
* @type String
* @default
*/
type: 'rect',
/**
* Horizontal border radius
* @type Number
* @default
*/
rx: 0,
/**
* Vertical border radius
* @type Number
* @default
*/
ry: 0,
/**
* Used to specify dash pattern for stroke on this object
* @type Array
*/
strokeDashArray: null,
/**
* Constructor
* @param {Object} [options] Options object
* @return {Object} thisArg
*/
initialize: function(options) {
options = options || { };
this.callSuper('initialize', options);
this._initRxRy();
this.x = options.x || 0;
this.y = options.y || 0;
},
/**
* Initializes rx/ry attributes
* @private
*/
_initRxRy: function() {
if (this.rx && !this.ry) {
this.ry = this.rx;
}
else if (this.ry && !this.rx) {
this.rx = this.ry;
}
},
/**
* @private
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx) {
var rx = this.rx || 0,
ry = this.ry || 0,
x = -this.width / 2,
y = -this.height / 2,
w = this.width,
h = this.height,
isInPathGroup = this.group && this.group.type !== 'group';
ctx.beginPath();
ctx.globalAlpha = isInPathGroup ? (ctx.globalAlpha * this.opacity) : this.opacity;
if (this.transformMatrix && isInPathGroup) {
ctx.translate(
this.width / 2 + this.x,
this.height / 2 + this.y);
}
if (!this.transformMatrix && isInPathGroup) {
ctx.translate(
-this.group.width / 2 + this.width / 2 + this.x,
-this.group.height / 2 + this.height / 2 + this.y);
}
var isRounded = rx !== 0 || ry !== 0;
ctx.moveTo(x+rx, y);
ctx.lineTo(x+w-rx, y);
isRounded && ctx.quadraticCurveTo(x+w, y, x+w, y+ry, x+w, y+ry);
ctx.lineTo(x+w, y+h-ry);
isRounded && ctx.quadraticCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h);
ctx.lineTo(x+rx,y+h);
isRounded && ctx.quadraticCurveTo(x,y+h,x,y+h-ry,x,y+h-ry);
ctx.lineTo(x,y+ry);
isRounded && ctx.quadraticCurveTo(x,y,x+rx,y,x+rx,y);
ctx.closePath();
this._renderFill(ctx);
this._renderStroke(ctx);
},
/**
* @private
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_renderDashedStroke: function(ctx) {
var x = -this.width/2,
y = -this.height/2,
w = this.width,
h = this.height;
ctx.beginPath();
fabric.util.drawDashedLine(ctx, x, y, x+w, y, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, x+w, y, x+w, y+h, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, x+w, y+h, x, y+h, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, x, y+h, x, y, this.strokeDashArray);
ctx.closePath();
},
/**
* Since coordinate system differs from that of SVG
* @private
*/
_normalizeLeftTopProperties: function(parsedAttributes) {
if ('left' in parsedAttributes) {
this.set('left', parsedAttributes.left + this.getWidth() / 2);
}
this.set('x', parsedAttributes.left || 0);
if ('top' in parsedAttributes) {
this.set('top', parsedAttributes.top + this.getHeight() / 2);
}
this.set('y', parsedAttributes.top || 0);
return this;
},
/**
* Returns object representation of an instance
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
rx: this.get('rx') || 0,
ry: this.get('ry') || 0,
x: this.get('x'),
y: this.get('y')
});
},
/**
* Returns complexity of an instance
* @return {Number} complexity
*/
complexity: function() {
return 1;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`)
* @static
* @memberOf fabric.Rect
* @see: http://www.w3.org/TR/SVG/shapes.html#RectElement
*/
fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' '));
/**
* @private
*/
function _setDefaultLeftTopValues(attributes) {
attributes.left = attributes.left || 0;
attributes.top = attributes.top || 0;
return attributes;
}
/**
* Returns {@link fabric.Rect} instance from an SVG element
* @static
* @memberOf fabric.Rect
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @return {fabric.Rect} Instance of fabric.Rect
*/
fabric.Rect.fromElement = function(element, options) {
if (!element) {
return null;
}
var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES);
parsedAttributes = _setDefaultLeftTopValues(parsedAttributes);
var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes));
rect._normalizeLeftTopProperties(parsedAttributes);
return rect;
};
/* _FROM_SVG_END_ */
/**
* Returns {@link fabric.Rect} instance from an object representation
* @static
* @memberOf fabric.Rect
* @param object {Object} object to create an instance from
* @return {Object} instance of fabric.Rect
*/
fabric.Rect.fromObject = function(object) {
return new fabric.Rect(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
toFixed = fabric.util.toFixed,
min = fabric.util.array.min;
if (fabric.Polyline) {
fabric.warn('fabric.Polyline is already defined');
return;
}
/**
* Polyline class
* @class fabric.Polyline
* @extends fabric.Object
*/
fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'polyline',
/**
* Constructor
* @param {Array} points Array of points
* @param {Object} [options] Options object
* @param {Boolean} [skipOffset] Whether points offsetting should be skipped
* @return {fabric.Polyline} thisArg
*/
initialize: function(points, options, skipOffset) {
options = options || { };
this.set('points', points);
this.callSuper('initialize', options);
this._calcDimensions(skipOffset);
},
/**
* @private
* @param {Boolean} [skipOffset] Whether points offsetting should be skipped
*/
_calcDimensions: function(skipOffset) {
return fabric.Polygon.prototype._calcDimensions.call(this, skipOffset);
},
/**
* Returns object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} Object representation of an instance
*/
toObject: function(propertiesToInclude) {
return fabric.Polygon.prototype.toObject.call(this, propertiesToInclude);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
var point;
ctx.beginPath();
ctx.moveTo(this.points[0].x, this.points[0].y);
for (var i = 0, len = this.points.length; i < len; i++) {
point = this.points[i];
ctx.lineTo(point.x, point.y);
}
this._renderFill(ctx);
this._renderStroke(ctx);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderDashedStroke: function(ctx) {
var p1, p2;
ctx.beginPath();
for (var i = 0, len = this.points.length; i < len; i++) {
p1 = this.points[i];
p2 = this.points[i+1] || p1;
fabric.util.drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, this.strokeDashArray);
}
},
/**
* Returns complexity of an instance
* @return {Number} complexity of this instance
*/
complexity: function() {
return this.get('points').length;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement})
* @static
* @memberOf fabric.Polyline
* @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement
*/
fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat();
/**
* Returns fabric.Polyline instance from an SVG element
* @static
* @memberOf fabric.Polyline
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @return {fabric.Polyline} Instance of fabric.Polyline
*/
fabric.Polyline.fromElement = function(element, options) {
if (!element) {
return null;
}
options || (options = { });
var points = fabric.parsePointsAttribute(element.getAttribute('points')),
parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES),
minX = min(points, 'x'),
minY = min(points, 'y');
minX = minX < 0 ? minX : 0;
minY = minX < 0 ? minY : 0;
for (var i = 0, len = points.length; i < len; i++) {
// normalize coordinates, according to containing box (dimensions of which are passed via `options`)
points[i].x -= (options.width / 2 + minX) || 0;
points[i].y -= (options.height / 2 + minY) || 0;
}
return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options), true);
};
/* _FROM_SVG_END_ */
/**
* Returns fabric.Polyline instance from an object representation
* @static
* @memberOf fabric.Polyline
* @param object {Object} object Object to create an instance from
* @return {fabric.Polyline} Instance of fabric.Polyline
*/
fabric.Polyline.fromObject = function(object) {
var points = object.points;
return new fabric.Polyline(points, object, true);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
min = fabric.util.array.min,
max = fabric.util.array.max,
toFixed = fabric.util.toFixed;
if (fabric.Polygon) {
fabric.warn('fabric.Polygon is already defined');
return;
}
/**
* Polygon class
* @class fabric.Polygon
* @extends fabric.Object
*/
fabric.Polygon = fabric.util.createClass(fabric.Object, /** @lends fabric.Polygon.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'polygon',
/**
* Constructor
* @param {Array} points Array of points
* @param {Object} [options] Options object
* @param {Boolean} [skipOffset] Whether points offsetting should be skipped
* @return {fabric.Polygon} thisArg
*/
initialize: function(points, options, skipOffset) {
options = options || { };
this.points = points;
this.callSuper('initialize', options);
this._calcDimensions(skipOffset);
},
/**
* @private
* @param {Boolean} [skipOffset] Whether points offsetting should be skipped
*/
_calcDimensions: function(skipOffset) {
var points = this.points,
minX = min(points, 'x'),
minY = min(points, 'y'),
maxX = max(points, 'x'),
maxY = max(points, 'y');
this.width = (maxX - minX) || 1;
this.height = (maxY - minY) || 1;
this.minX = minX;
this.minY = minY;
if (skipOffset) return;
var halfWidth = this.width / 2 + this.minX,
halfHeight = this.height / 2 + this.minY;
// change points to offset polygon into a bounding box
this.points.forEach(function(p) {
p.x -= halfWidth;
p.y -= halfHeight;
}, this);
},
/**
* Returns object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} Object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
points: this.points.concat()
});
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
var point;
ctx.beginPath();
ctx.moveTo(this.points[0].x, this.points[0].y);
for (var i = 0, len = this.points.length; i < len; i++) {
point = this.points[i];
ctx.lineTo(point.x, point.y);
}
this._renderFill(ctx);
if (this.stroke || this.strokeDashArray) {
ctx.closePath();
this._renderStroke(ctx);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderDashedStroke: function(ctx) {
var p1, p2;
ctx.beginPath();
for (var i = 0, len = this.points.length; i < len; i++) {
p1 = this.points[i];
p2 = this.points[i+1] || this.points[0];
fabric.util.drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, this.strokeDashArray);
}
ctx.closePath();
},
/**
* Returns complexity of an instance
* @return {Number} complexity of this instance
*/
complexity: function() {
return this.points.length;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`)
* @static
* @memberOf fabric.Polygon
* @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement
*/
fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat();
/**
* Returns {@link fabric.Polygon} instance from an SVG element
* @static
* @memberOf fabric.Polygon
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @return {fabric.Polygon} Instance of fabric.Polygon
*/
fabric.Polygon.fromElement = function(element, options) {
if (!element) {
return null;
}
options || (options = { });
var points = fabric.parsePointsAttribute(element.getAttribute('points')),
parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES),
minX = min(points, 'x'),
minY = min(points, 'y');
minX = minX < 0 ? minX : 0;
minY = minX < 0 ? minY : 0;
for (var i = 0, len = points.length; i < len; i++) {
// normalize coordinates, according to containing box (dimensions of which are passed via `options`)
points[i].x -= (options.width / 2 + minX) || 0;
points[i].y -= (options.height / 2 + minY) || 0;
}
return new fabric.Polygon(points, extend(parsedAttributes, options), true);
};
/* _FROM_SVG_END_ */
/**
* Returns fabric.Polygon instance from an object representation
* @static
* @memberOf fabric.Polygon
* @param object {Object} object Object to create an instance from
* @return {fabric.Polygon} Instance of fabric.Polygon
*/
fabric.Polygon.fromObject = function(object) {
return new fabric.Polygon(object.points, object, true);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var commandLengths = {
m: 2,
l: 2,
h: 1,
v: 1,
c: 6,
s: 4,
q: 4,
t: 2,
a: 7
};
var fabric = global.fabric || (global.fabric = { }),
min = fabric.util.array.min,
max = fabric.util.array.max,
extend = fabric.util.object.extend,
_toString = Object.prototype.toString,
drawArc = fabric.util.drawArc;
if (fabric.Path) {
fabric.warn('fabric.Path is already defined');
return;
}
/**
* @private
*/
function getX(item) {
if (item[0] === 'H') {
return item[1];
}
return item[item.length - 2];
}
/**
* @private
*/
function getY(item) {
if (item[0] === 'V') {
return item[1];
}
return item[item.length - 1];
}
/**
* Path class
* @class fabric.Path
* @extends fabric.Object
*/
fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'path',
/**
* Constructor
* @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens)
* @param {Object} [options] Options object
* @return {fabric.Path} thisArg
*/
initialize: function(path, options) {
options = options || { };
this.setOptions(options);
if (!path) {
throw new Error('`path` argument is required');
}
var fromArray = _toString.call(path) === '[object Array]';
this.path = fromArray
? path
// one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values)
: path.match && path.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi);
if (!this.path) return;
if (!fromArray) {
this.path = this._parsePath();
}
this._initializePath(options);
if (options.sourcePath) {
this.setSourcePath(options.sourcePath);
}
},
/**
* @private
* @param {Object} [options] Options object
*/
_initializePath: function (options) {
var isWidthSet = 'width' in options && options.width != null,
isHeightSet = 'height' in options && options.width != null,
isLeftSet = 'left' in options,
isTopSet = 'top' in options,
origLeft = isLeftSet ? this.left : 0,
origTop = isTopSet ? this.top : 0;
if (!isWidthSet || !isHeightSet) {
extend(this, this._parseDimensions());
if (isWidthSet) {
this.width = options.width;
}
if (isHeightSet) {
this.height = options.height;
}
}
else { //Set center location relative to given height/width if not specified
if (!isTopSet) {
this.top = this.height / 2;
}
if (!isLeftSet) {
this.left = this.width / 2;
}
}
this.pathOffset = this.pathOffset || this._calculatePathOffset(origLeft, origTop); //Save top-left coords as offset
},
/**
* @private
* @param {Boolean} positionSet When false, path offset is returned otherwise 0
*/
_calculatePathOffset: function (origLeft, origTop) {
return {
x: this.left - origLeft - (this.width / 2),
y: this.top - origTop - (this.height / 2)
};
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx context to render path on
*/
_render: function(ctx) {
var current, // current instruction
previous = null,
x = 0, // current x
y = 0, // current y
controlX = 0, // current control point x
controlY = 0, // current control point y
tempX,
tempY,
tempControlX,
tempControlY,
l = -((this.width / 2) + this.pathOffset.x),
t = -((this.height / 2) + this.pathOffset.y);
for (var i = 0, len = this.path.length; i < len; ++i) {
current = this.path[i];
switch (current[0]) { // first letter
case 'l': // lineto, relative
x += current[1];
y += current[2];
ctx.lineTo(x + l, y + t);
break;
case 'L': // lineto, absolute
x = current[1];
y = current[2];
ctx.lineTo(x + l, y + t);
break;
case 'h': // horizontal lineto, relative
x += current[1];
ctx.lineTo(x + l, y + t);
break;
case 'H': // horizontal lineto, absolute
x = current[1];
ctx.lineTo(x + l, y + t);
break;
case 'v': // vertical lineto, relative
y += current[1];
ctx.lineTo(x + l, y + t);
break;
case 'V': // verical lineto, absolute
y = current[1];
ctx.lineTo(x + l, y + t);
break;
case 'm': // moveTo, relative
x += current[1];
y += current[2];
// draw a line if previous command was moveTo as well (otherwise, it will have no effect)
ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t);
break;
case 'M': // moveTo, absolute
x = current[1];
y = current[2];
// draw a line if previous command was moveTo as well (otherwise, it will have no effect)
ctx[(previous && (previous[0] === 'm' || previous[0] === 'M')) ? 'lineTo' : 'moveTo'](x + l, y + t);
break;
case 'c': // bezierCurveTo, relative
tempX = x + current[5];
tempY = y + current[6];
controlX = x + current[3];
controlY = y + current[4];
ctx.bezierCurveTo(
x + current[1] + l, // x1
y + current[2] + t, // y1
controlX + l, // x2
controlY + t, // y2
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
break;
case 'C': // bezierCurveTo, absolute
x = current[5];
y = current[6];
controlX = current[3];
controlY = current[4];
ctx.bezierCurveTo(
current[1] + l,
current[2] + t,
controlX + l,
controlY + t,
x + l,
y + t
);
break;
case 's': // shorthand cubic bezierCurveTo, relative
// transform to absolute x,y
tempX = x + current[3];
tempY = y + current[4];
// calculate reflection of previous control points
controlX = controlX ? (2 * x - controlX) : x;
controlY = controlY ? (2 * y - controlY) : y;
ctx.bezierCurveTo(
controlX + l,
controlY + t,
x + current[1] + l,
y + current[2] + t,
tempX + l,
tempY + t
);
// set control point to 2nd one of this command
// "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point."
controlX = x + current[1];
controlY = y + current[2];
x = tempX;
y = tempY;
break;
case 'S': // shorthand cubic bezierCurveTo, absolute
tempX = current[3];
tempY = current[4];
// calculate reflection of previous control points
controlX = 2*x - controlX;
controlY = 2*y - controlY;
ctx.bezierCurveTo(
controlX + l,
controlY + t,
current[1] + l,
current[2] + t,
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
// set control point to 2nd one of this command
// "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point."
controlX = current[1];
controlY = current[2];
break;
case 'q': // quadraticCurveTo, relative
// transform to absolute x,y
tempX = x + current[3];
tempY = y + current[4];
controlX = x + current[1];
controlY = y + current[2];
ctx.quadraticCurveTo(
controlX + l,
controlY + t,
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
break;
case 'Q': // quadraticCurveTo, absolute
tempX = current[3];
tempY = current[4];
ctx.quadraticCurveTo(
current[1] + l,
current[2] + t,
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
controlX = current[1];
controlY = current[2];
break;
case 't': // shorthand quadraticCurveTo, relative
// transform to absolute x,y
tempX = x + current[1];
tempY = y + current[2];
if (previous[0].match(/[QqTt]/) === null) {
// If there is no previous command or if the previous command was not a Q, q, T or t,
// assume the control point is coincident with the current point
controlX = x;
controlY = y;
}
else if (previous[0] === 't') {
// calculate reflection of previous control points for t
controlX = 2 * x - tempControlX;
controlY = 2 * y - tempControlY;
}
else if (previous[0] === 'q') {
// calculate reflection of previous control points for q
controlX = 2 * x - controlX;
controlY = 2 * y - controlY;
}
tempControlX = controlX;
tempControlY = controlY;
ctx.quadraticCurveTo(
controlX + l,
controlY + t,
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
controlX = x + current[1];
controlY = y + current[2];
break;
case 'T':
tempX = current[1];
tempY = current[2];
// calculate reflection of previous control points
controlX = 2 * x - controlX;
controlY = 2 * y - controlY;
ctx.quadraticCurveTo(
controlX + l,
controlY + t,
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
break;
case 'a':
// TODO: optimize this
drawArc(ctx, x + l, y + t, [
current[1],
current[2],
current[3],
current[4],
current[5],
current[6] + x + l,
current[7] + y + t
]);
x += current[6];
y += current[7];
break;
case 'A':
// TODO: optimize this
drawArc(ctx, x + l, y + t, [
current[1],
current[2],
current[3],
current[4],
current[5],
current[6] + l,
current[7] + t
]);
x = current[6];
y = current[7];
break;
case 'z':
case 'Z':
ctx.closePath();
break;
}
previous = current;
}
},
/**
* Renders path on a specified context
* @param {CanvasRenderingContext2D} ctx context to render path on
* @param {Boolean} [noTransform] When true, context is not transformed
*/
render: function(ctx, noTransform) {
// do not render if object is not visible
if (!this.visible) return;
ctx.save();
var m = this.transformMatrix;
if (m) {
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
if (!noTransform) {
this.transform(ctx);
}
// ctx.globalCompositeOperation = this.fillRule;
if (this.overlayFill) {
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
ctx.fillStyle = this.fill.toLive
? this.fill.toLive(ctx)
: this.fill;
}
if (this.stroke) {
ctx.lineWidth = this.strokeWidth;
ctx.lineCap = this.strokeLineCap;
ctx.lineJoin = this.strokeLineJoin;
ctx.miterLimit = this.strokeMiterLimit;
ctx.strokeStyle = this.stroke.toLive
? this.stroke.toLive(ctx)
: this.stroke;
}
this._setShadow(ctx);
this.clipTo && fabric.util.clipContext(this, ctx);
ctx.beginPath();
this._render(ctx);
this._renderFill(ctx);
this._renderStroke(ctx);
this.clipTo && ctx.restore();
this._removeShadow(ctx);
if (!noTransform && this.active) {
this.drawBorders(ctx);
this.drawControls(ctx);
}
ctx.restore();
},
/**
* Returns string representation of an instance
* @return {String} string representation of an instance
*/
toString: function() {
return '#<fabric.Path (' + this.complexity() +
'): { "top": ' + this.top + ', "left": ' + this.left + ' }>';
},
/**
* Returns object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
var o = extend(this.callSuper('toObject', propertiesToInclude), {
path: this.path,
pathOffset: this.pathOffset
});
if (this.sourcePath) {
o.sourcePath = this.sourcePath;
}
if (this.transformMatrix) {
o.transformMatrix = this.transformMatrix;
}
return o;
},
/**
* Returns dataless object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toDatalessObject: function(propertiesToInclude) {
var o = this.toObject(propertiesToInclude);
if (this.sourcePath) {
o.path = this.sourcePath;
}
delete o.sourcePath;
return o;
},
/**
* Returns number representation of an instance complexity
* @return {Number} complexity of this instance
*/
complexity: function() {
return this.path.length;
},
/**
* @private
*/
_parsePath: function() {
var result = [ ],
coords = [ ],
currentPath,
parsed,
re = /(-?\.\d+)|(-?\d+(\.\d+)?)/g,
match,
coordsStr;
for (var i = 0, coordsParsed, len = this.path.length; i < len; i++) {
currentPath = this.path[i];
coordsStr = currentPath.slice(1).trim();
coords.length = 0;
while ((match = re.exec(coordsStr))) {
coords.push(match[0]);
}
coordsParsed = [ currentPath.charAt(0) ];
for (var j = 0, jlen = coords.length; j < jlen; j++) {
parsed = parseFloat(coords[j]);
if (!isNaN(parsed)) {
coordsParsed.push(parsed);
}
}
var command = coordsParsed[0].toLowerCase(),
commandLength = commandLengths[command];
if (coordsParsed.length - 1 > commandLength) {
for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) {
result.push([ coordsParsed[0] ].concat(coordsParsed.slice(k, k + commandLength)));
}
}
else {
result.push(coordsParsed);
}
}
return result;
},
/**
* @private
*/
_parseDimensions: function() {
var aX = [],
aY = [],
previousX,
previousY,
isLowerCase = false,
x,
y;
this.path.forEach(function(item, i) {
if (item[0] !== 'H') {
previousX = (i === 0) ? getX(item) : getX(this.path[i-1]);
}
if (item[0] !== 'V') {
previousY = (i === 0) ? getY(item) : getY(this.path[i-1]);
}
// lowercased letter denotes relative position;
// transform to absolute
if (item[0] === item[0].toLowerCase()) {
isLowerCase = true;
}
// last 2 items in an array of coordinates are the actualy x/y (except H/V);
// collect them
// TODO (kangax): support relative h/v commands
x = isLowerCase
? previousX + getX(item)
: item[0] === 'V'
? previousX
: getX(item);
y = isLowerCase
? previousY + getY(item)
: item[0] === 'H'
? previousY
: getY(item);
var val = parseInt(x, 10);
if (!isNaN(val)) aX.push(val);
val = parseInt(y, 10);
if (!isNaN(val)) aY.push(val);
}, this);
var minX = min(aX),
minY = min(aY),
maxX = max(aX),
maxY = max(aY),
deltaX = maxX - minX,
deltaY = maxY - minY;
var o = {
left: this.left + (minX + deltaX / 2),
top: this.top + (minY + deltaY / 2),
width: deltaX,
height: deltaY
};
return o;
}
});
/**
* Creates an instance of fabric.Path from an object
* @static
* @memberOf fabric.Path
* @param {Object} object
* @param {Function} callback Callback to invoke when an fabric.Path instance is created
*/
fabric.Path.fromObject = function(object, callback) {
if (typeof object.path === 'string') {
fabric.loadSVGFromURL(object.path, function (elements) {
var path = elements[0];
var pathUrl = object.path;
delete object.path;
fabric.util.object.extend(path, object);
path.setSourcePath(pathUrl);
callback(path);
});
}
else {
callback(new fabric.Path(object.path, object));
}
};
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`)
* @static
* @memberOf fabric.Path
* @see http://www.w3.org/TR/SVG/paths.html#PathElement
*/
fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']);
/**
* Creates an instance of fabric.Path from an SVG <path> element
* @static
* @memberOf fabric.Path
* @param {SVGElement} element to parse
* @param {Function} callback Callback to invoke when an fabric.Path instance is created
* @param {Object} [options] Options object
*/
fabric.Path.fromElement = function(element, callback, options) {
var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES);
callback && callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options)));
};
/* _FROM_SVG_END_ */
/**
* Indicates that instances of this type are async
* @static
* @memberOf fabric.Path
* @type Boolean
* @default
*/
fabric.Path.async = true;
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
invoke = fabric.util.array.invoke,
parentToObject = fabric.Object.prototype.toObject;
if (fabric.PathGroup) {
fabric.warn('fabric.PathGroup is already defined');
return;
}
/**
* Path group class
* @class fabric.PathGroup
* @extends fabric.Path
*/
fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @lends fabric.PathGroup.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'path-group',
/**
* Fill value
* @type String
* @default
*/
fill: '',
/**
* Constructor
* @param {Array} paths
* @param {Object} [options] Options object
* @return {fabric.PathGroup} thisArg
*/
initialize: function(paths, options) {
options = options || { };
this.paths = paths || [ ];
for (var i = this.paths.length; i--; ) {
this.paths[i].group = this;
}
this.setOptions(options);
this.setCoords();
if (options.sourcePath) {
this.setSourcePath(options.sourcePath);
}
},
/**
* Renders this group on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render this instance on
*/
render: function(ctx) {
// do not render if object is not visible
if (!this.visible) return;
ctx.save();
var m = this.transformMatrix;
if (m) {
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
this.transform(ctx);
this._setShadow(ctx);
this.clipTo && fabric.util.clipContext(this, ctx);
for (var i = 0, l = this.paths.length; i < l; ++i) {
this.paths[i].render(ctx, true);
}
this.clipTo && ctx.restore();
this._removeShadow(ctx);
if (this.active) {
this.drawBorders(ctx);
this.drawControls(ctx);
}
ctx.restore();
},
/**
* Sets certain property to a certain value
* @param {String} prop
* @param {Any} value
* @return {fabric.PathGroup} thisArg
*/
_set: function(prop, value) {
if ((prop === 'fill' || prop === 'overlayFill') && value && this.isSameColor()) {
var i = this.paths.length;
while (i--) {
this.paths[i]._set(prop, value);
}
}
return this.callSuper('_set', prop, value);
},
/**
* Returns object representation of this path group
* @param {Array} [propertiesToInclude]
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(parentToObject.call(this, propertiesToInclude), {
paths: invoke(this.getObjects(), 'toObject', propertiesToInclude),
sourcePath: this.sourcePath
});
},
/**
* Returns dataless object representation of this path group
* @param {Array} [propertiesToInclude]
* @return {Object} dataless object representation of an instance
*/
toDatalessObject: function(propertiesToInclude) {
var o = this.toObject(propertiesToInclude);
if (this.sourcePath) {
o.paths = this.sourcePath;
}
return o;
},
/**
* Returns a string representation of this path group
* @return {String} string representation of an object
*/
toString: function() {
return '#<fabric.PathGroup (' + this.complexity() +
'): { top: ' + this.top + ', left: ' + this.left + ' }>';
},
/**
* Returns true if all paths in this group are of same color
* @return {Boolean} true if all paths are of the same color (`fill`)
*/
isSameColor: function() {
var firstPathFill = this.getObjects()[0].get('fill');
return this.getObjects().every(function(path) {
return path.get('fill') === firstPathFill;
});
},
/**
* Returns number representation of object's complexity
* @return {Number} complexity
*/
complexity: function() {
return this.paths.reduce(function(total, path) {
return total + ((path && path.complexity) ? path.complexity() : 0);
}, 0);
},
/**
* Makes path group grayscale
* @return {fabric.PathGroup} thisArg
*/
toGrayscale: function() {
var i = this.paths.length;
while (i--) {
this.paths[i].toGrayscale();
}
return this;
},
/**
* Returns all paths in this path group
* @return {Array} array of path objects included in this path group
*/
getObjects: function() {
return this.paths;
}
});
/**
* Creates fabric.PathGroup instance from an object representation
* @static
* @memberOf fabric.PathGroup
* @param {Object} object
* @param {Function} callback Callback to invoke when an fabric.PathGroup instance is created
*/
fabric.PathGroup.fromObject = function(object, callback) {
if (typeof object.paths === 'string') {
fabric.loadSVGFromURL(object.paths, function (elements) {
var pathUrl = object.paths;
delete object.paths;
var pathGroup = fabric.util.groupSVGElements(elements, object, pathUrl);
callback(pathGroup);
});
}
else {
fabric.util.enlivenObjects(object.paths, function(enlivenedObjects) {
delete object.paths;
callback(new fabric.PathGroup(enlivenedObjects, object));
});
}
};
/**
* Indicates that instances of this type are async
* @static
* @memberOf fabric.PathGroup
* @type Boolean
* @default
*/
fabric.PathGroup.async = true;
})(typeof exports !== 'undefined' ? exports : this);
(function(global){
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
min = fabric.util.array.min,
max = fabric.util.array.max,
invoke = fabric.util.array.invoke;
if (fabric.Group) {
return;
}
// lock-related properties, for use in fabric.Group#get
// to enable locking behavior on group
// when one of its objects has lock-related properties set
var _lockProperties = {
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
lockUniScaling: true
};
/**
* Group class
* @class fabric.Group
* @extends fabric.Object
* @mixes fabric.Collection
*/
fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'group',
/**
* Constructor
* @param {Object} objects Group objects
* @param {Object} [options] Options object
* @return {Object} thisArg
*/
initialize: function(objects, options) {
options = options || { };
this._objects = objects || [];
for (var i = this._objects.length; i--; ) {
this._objects[i].group = this;
}
this.originalState = { };
this.callSuper('initialize');
this._calcBounds();
this._updateObjectsCoords();
if (options) {
extend(this, options);
}
this._setOpacityIfSame();
this.setCoords(true);
this.saveCoords();
},
/**
* @private
*/
_updateObjectsCoords: function() {
var groupDeltaX = this.left,
groupDeltaY = this.top;
this.forEachObject(function(object) {
var objectLeft = object.get('left'),
objectTop = object.get('top');
object.set('originalLeft', objectLeft);
object.set('originalTop', objectTop);
object.set('left', objectLeft - groupDeltaX);
object.set('top', objectTop - groupDeltaY);
object.setCoords();
// do not display corners of objects enclosed in a group
object.__origHasControls = object.hasControls;
object.hasControls = false;
}, this);
},
/**
* Returns string represenation of a group
* @return {String}
*/
toString: function() {
return '#<fabric.Group: (' + this.complexity() + ')>';
},
/**
* Returns an array of all objects in this group
* @return {Array} group objects
*/
getObjects: function() {
return this._objects;
},
/**
* Adds an object to a group; Then recalculates group's dimension, position.
* @param {Object} object
* @return {fabric.Group} thisArg
* @chainable
*/
addWithUpdate: function(object) {
this._restoreObjectsState();
this._objects.push(object);
object.group = this;
// since _restoreObjectsState set objects inactive
this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this);
this._calcBounds();
this._updateObjectsCoords();
return this;
},
/**
* Removes an object from a group; Then recalculates group's dimension, position.
* @param {Object} object
* @return {fabric.Group} thisArg
* @chainable
*/
removeWithUpdate: function(object) {
this._restoreObjectsState();
// since _restoreObjectsState set objects inactive
this.forEachObject(function(o){ o.set('active', true); o.group = this; }, this);
this.remove(object);
this._calcBounds();
this._updateObjectsCoords();
return this;
},
/**
* @private
*/
_onObjectAdded: function(object) {
object.group = this;
},
/**
* @private
*/
_onObjectRemoved: function(object) {
delete object.group;
object.set('active', false);
},
/**
* Properties that are delegated to group objects when reading/writing
* @param {Object} delegatedProperties
*/
delegatedProperties: {
fill: true,
opacity: true,
fontFamily: true,
fontWeight: true,
fontSize: true,
fontStyle: true,
lineHeight: true,
textDecoration: true,
textAlign: true,
backgroundColor: true
},
/**
* @private
*/
_set: function(key, value) {
if (key in this.delegatedProperties) {
var i = this._objects.length;
this[key] = value;
while (i--) {
this._objects[i].set(key, value);
}
}
else {
this[key] = value;
}
},
/**
* Returns object representation of an instance
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
objects: invoke(this._objects, 'toObject', propertiesToInclude)
});
},
/**
* Renders instance on a given context
* @param {CanvasRenderingContext2D} ctx context to render instance on
* @param {Boolean} [noTransform] When true, context is not transformed
*/
render: function(ctx, noTransform) {
// do not render if object is not visible
if (!this.visible) return;
ctx.save();
this.transform(ctx);
var groupScaleFactor = Math.max(this.scaleX, this.scaleY);
this.clipTo && fabric.util.clipContext(this, ctx);
//The array is now sorted in order of highest first, so start from end.
for (var i = 0, len = this._objects.length; i < len; i++) {
var object = this._objects[i],
originalScaleFactor = object.borderScaleFactor,
originalHasRotatingPoint = object.hasRotatingPoint;
// do not render if object is not visible
if (!object.visible) continue;
object.borderScaleFactor = groupScaleFactor;
object.hasRotatingPoint = false;
object.render(ctx);
object.borderScaleFactor = originalScaleFactor;
object.hasRotatingPoint = originalHasRotatingPoint;
}
this.clipTo && ctx.restore();
if (!noTransform && this.active) {
this.drawBorders(ctx);
this.drawControls(ctx);
}
ctx.restore();
this.setCoords();
},
/**
* Retores original state of each of group objects (original state is that which was before group was created).
* @private
* @return {fabric.Group} thisArg
* @chainable
*/
_restoreObjectsState: function() {
this._objects.forEach(this._restoreObjectState, this);
return this;
},
/**
* Restores original state of a specified object in group
* @private
* @param {fabric.Object} object
* @return {fabric.Group} thisArg
*/
_restoreObjectState: function(object) {
var groupLeft = this.get('left'),
groupTop = this.get('top'),
groupAngle = this.getAngle() * (Math.PI / 180),
rotatedTop = Math.cos(groupAngle) * object.get('top') * this.get('scaleY') + Math.sin(groupAngle) * object.get('left') * this.get('scaleX'),
rotatedLeft = -Math.sin(groupAngle) * object.get('top') * this.get('scaleY') + Math.cos(groupAngle) * object.get('left') * this.get('scaleX');
object.setAngle(object.getAngle() + this.getAngle());
object.set('left', groupLeft + rotatedLeft);
object.set('top', groupTop + rotatedTop);
object.set('scaleX', object.get('scaleX') * this.get('scaleX'));
object.set('scaleY', object.get('scaleY') * this.get('scaleY'));
object.setCoords();
object.hasControls = object.__origHasControls;
delete object.__origHasControls;
object.set('active', false);
object.setCoords();
delete object.group;
return this;
},
/**
* Destroys a group (restoring state of its objects)
* @return {fabric.Group} thisArg
* @chainable
*/
destroy: function() {
return this._restoreObjectsState();
},
/**
* Saves coordinates of this instance (to be used together with `hasMoved`)
* @saveCoords
* @return {fabric.Group} thisArg
* @chainable
*/
saveCoords: function() {
this._originalLeft = this.get('left');
this._originalTop = this.get('top');
return this;
},
/**
* Checks whether this group was moved (since `saveCoords` was called last)
* @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called)
*/
hasMoved: function() {
return this._originalLeft !== this.get('left') ||
this._originalTop !== this.get('top');
},
/**
* Sets coordinates of all group objects
* @return {fabric.Group} thisArg
* @chainable
*/
setObjectsCoords: function() {
this.forEachObject(function(object) {
object.setCoords();
});
return this;
},
/**
* @private
*/
_setOpacityIfSame: function() {
var objects = this.getObjects(),
firstValue = objects[0] ? objects[0].get('opacity') : 1;
var isSameOpacity = objects.every(function(o) {
return o.get('opacity') === firstValue;
});
if (isSameOpacity) {
this.opacity = firstValue;
}
},
/**
* @private
*/
_calcBounds: function() {
var aX = [],
aY = [],
minX, minY, maxX, maxY, o, width, height,
i = 0,
len = this._objects.length;
for (; i < len; ++i) {
o = this._objects[i];
o.setCoords();
for (var prop in o.oCoords) {
aX.push(o.oCoords[prop].x);
aY.push(o.oCoords[prop].y);
}
}
minX = min(aX);
maxX = max(aX);
minY = min(aY);
maxY = max(aY);
width = (maxX - minX) || 0;
height = (maxY - minY) || 0;
this.width = width;
this.height = height;
this.left = (minX + width / 2) || 0;
this.top = (minY + height / 2) || 0;
},
/**
* Returns requested property
* @param {String} prop Property to get
* @return {Any}
*/
get: function(prop) {
if (prop in _lockProperties) {
if (this[prop]) {
return this[prop];
}
else {
for (var i = 0, len = this._objects.length; i < len; i++) {
if (this._objects[i][prop]) {
return true;
}
}
return false;
}
}
else {
if (prop in this.delegatedProperties) {
return this._objects[0] && this._objects[0].get(prop);
}
return this[prop];
}
}
});
/**
* Returns {@link fabric.Group} instance from an object representation
* @static
* @memberOf fabric.Group
* @param {Object} object Object to create a group from
* @param {Object} [options] Options object
* @return {fabric.Group} An instance of fabric.Group
*/
fabric.Group.fromObject = function(object, callback) {
fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) {
delete object.objects;
callback && callback(new fabric.Group(enlivenedObjects, object));
});
};
/**
* Indicates that instances of this type are async
* @static
* @memberOf fabric.Group
* @type Boolean
* @default
*/
fabric.Group.async = true;
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var extend = fabric.util.object.extend;
if (!global.fabric) {
global.fabric = { };
}
if (global.fabric.Image) {
fabric.warn('fabric.Image is already defined.');
return;
}
/**
* Image class
* @class fabric.Image
* @extends fabric.Object
*/
fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ {
/**
* Type of an object
* @type String
* @default
*/
type: 'image',
/**
* Constructor
* @param {HTMLImageElement | String} element Image element
* @param {Object} [options] Options object
* @return {fabric.Image} thisArg
*/
initialize: function(element, options) {
options || (options = { });
this.filters = [ ];
this.callSuper('initialize', options);
this._initElement(element);
this._initConfig(options);
if (options.filters) {
this.filters = options.filters;
this.applyFilters();
}
},
/**
* Returns image element which this instance if based on
* @return {HTMLImageElement} Image element
*/
getElement: function() {
return this._element;
},
/**
* Sets image element for this instance to a specified one.
* If filters defined they are applied to new image.
* You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area.
* @param {HTMLImageElement} element
* @param {Function} [callback] Callback is invoked when all filters have been applied and new image is generated
* @return {fabric.Image} thisArg
* @chainable
*/
setElement: function(element, callback) {
this._element = element;
this._originalElement = element;
this._initConfig();
if (this.filters.length !== 0) {
this.applyFilters(callback);
}
return this;
},
/**
* Returns original size of an image
* @return {Object} Object with "width" and "height" properties
*/
getOriginalSize: function() {
var element = this.getElement();
return {
width: element.width,
height: element.height
};
},
/**
* Renders image on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Boolean} [noTransform] When true, context is not transformed
*/
render: function(ctx, noTransform) {
// do not render if object is not visible
if (!this.visible) return;
ctx.save();
var m = this.transformMatrix;
var isInPathGroup = this.group && this.group.type !== 'group';
// this._resetWidthHeight();
if (isInPathGroup) {
ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2);
}
if (m) {
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
if (!noTransform) {
this.transform(ctx);
}
ctx.save();
this._setShadow(ctx);
this.clipTo && fabric.util.clipContext(this, ctx);
this._render(ctx);
if (this.shadow && !this.shadow.affectStroke) {
this._removeShadow(ctx);
}
this._renderStroke(ctx);
this.clipTo && ctx.restore();
ctx.restore();
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.drawControls(ctx);
}
ctx.restore();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_stroke: function(ctx) {
ctx.save();
ctx.lineWidth = this.strokeWidth;
ctx.lineCap = this.strokeLineCap;
ctx.lineJoin = this.strokeLineJoin;
ctx.miterLimit = this.strokeMiterLimit;
ctx.strokeStyle = this.stroke.toLive
? this.stroke.toLive(ctx)
: this.stroke;
ctx.beginPath();
ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height);
ctx.closePath();
ctx.restore();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderDashedStroke: function(ctx) {
var x = -this.width/2,
y = -this.height/2,
w = this.width,
h = this.height;
ctx.save();
ctx.lineWidth = this.strokeWidth;
ctx.lineCap = this.strokeLineCap;
ctx.lineJoin = this.strokeLineJoin;
ctx.miterLimit = this.strokeMiterLimit;
ctx.strokeStyle = this.stroke.toLive
? this.stroke.toLive(ctx)
: this.stroke;
ctx.beginPath();
fabric.util.drawDashedLine(ctx, x, y, x+w, y, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, x+w, y, x+w, y+h, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, x+w, y+h, x, y+h, this.strokeDashArray);
fabric.util.drawDashedLine(ctx, x, y+h, x, y, this.strokeDashArray);
ctx.closePath();
ctx.restore();
},
/**
* Returns object representation of an instance
* @param {Array} propertiesToInclude Any properties that you might want to additionally include in the output
* @return {Object} Object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
src: this._originalElement.src || this._originalElement._src,
filters: this.filters.map(function(filterObj) {
return filterObj && filterObj.toObject();
})
});
},
/**
* Returns source of an image
* @return {String} Source of an image
*/
getSrc: function() {
return this.getElement().src || this.getElement()._src;
},
/**
* Returns string representation of an instance
* @return {String} String representation of an instance
*/
toString: function() {
return '#<fabric.Image: { src: "' + this.getSrc() + '" }>';
},
/**
* Returns a clone of an instance
* @param {Function} callback Callback is invoked with a clone as a first argument
* @param {Array} propertiesToInclude Any properties that you might want to additionally include in the output
*/
clone: function(callback, propertiesToInclude) {
this.constructor.fromObject(this.toObject(propertiesToInclude), callback);
},
/**
* Applies filters assigned to this image (from "filters" array)
* @mthod applyFilters
* @param {Function} callback Callback is invoked when all filters have been applied and new image is generated
* @return {fabric.Image} thisArg
* @chainable
*/
applyFilters: function(callback) {
if (this.filters.length === 0) {
this._element = this._originalElement;
callback && callback();
return;
}
var imgEl = this._originalElement,
canvasEl = fabric.util.createCanvasElement(),
replacement = fabric.util.createImage(),
_this = this;
canvasEl.width = imgEl.width;
canvasEl.height = imgEl.height;
canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height);
this.filters.forEach(function(filter) {
filter && filter.applyTo(canvasEl);
});
/** @ignore */
replacement.width = imgEl.width;
replacement.height = imgEl.height;
if (fabric.isLikelyNode) {
replacement.src = canvasEl.toBuffer(undefined, fabric.Image.pngCompression);
// onload doesn't fire in some node versions, so we invoke callback manually
_this._element = replacement;
callback && callback();
}
else {
replacement.onload = function() {
_this._element = replacement;
callback && callback();
replacement.onload = canvasEl = imgEl = null;
};
replacement.src = canvasEl.toDataURL('image/png');
}
return this;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
ctx.drawImage(
this._element,
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
},
/**
* @private
*/
_resetWidthHeight: function() {
var element = this.getElement();
this.set('width', element.width);
this.set('height', element.height);
},
/**
* The Image class's initialization method. This method is automatically
* called by the constructor.
* @private
* @param {HTMLImageElement|String} element The element representing the image
*/
_initElement: function(element) {
this.setElement(fabric.util.getById(element));
fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS);
},
/**
* @private
* @param {Object} [options] Options object
*/
_initConfig: function(options) {
options || (options = { });
this.setOptions(options);
this._setWidthHeight(options);
},
/**
* @private
* @param {Object} object Object with filters property
* @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created
*/
_initFilters: function(object, callback) {
if (object.filters && object.filters.length) {
fabric.util.enlivenObjects(object.filters, function(enlivenedObjects) {
callback && callback(enlivenedObjects);
}, 'fabric.Image.filters');
}
else {
callback && callback();
}
},
/**
* @private
* @param {Object} [options] Object with width/height properties
*/
_setWidthHeight: function(options) {
this.width = 'width' in options
? options.width
: (this.getElement().width || 0);
this.height = 'height' in options
? options.height
: (this.getElement().height || 0);
},
/**
* Returns complexity of an instance
* @return {Number} complexity of this instance
*/
complexity: function() {
return 1;
}
});
/**
* Default CSS class name for canvas
* @static
* @type String
* @default
*/
fabric.Image.CSS_CANVAS = "canvas-img";
/**
* Alias for getSrc
* @static
*/
fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc;
/**
* Creates an instance of fabric.Image from its object representation
* @static
* @param {Object} object Object to create an instance from
* @param {Function} [callback] Callback to invoke when an image instance is created
*/
fabric.Image.fromObject = function(object, callback) {
var img = fabric.document.createElement('img'),
src = object.src;
/** @ignore */
img.onload = function() {
fabric.Image.prototype._initFilters.call(object, object, function(filters) {
object.filters = filters || [ ];
var instance = new fabric.Image(img, object);
callback && callback(instance);
img = img.onload = img.onerror = null;
});
};
/** @ignore */
img.onerror = function() {
fabric.log('Error loading ' + img.src);
callback && callback(null, true);
img = img.onload = img.onerror = null;
};
img.src = src;
};
/**
* Creates an instance of fabric.Image from an URL string
* @static
* @param {String} url URL to create an image from
* @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument)
* @param {Object} [imgOptions] Options object
*/
fabric.Image.fromURL = function(url, callback, imgOptions) {
fabric.util.loadImage(url, function(img) {
callback(new fabric.Image(img, imgOptions));
});
};
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement})
* @static
* @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement}
*/
fabric.Image.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y width height xlink:href'.split(' '));
/**
* Returns {@link fabric.Image} instance from an SVG element
* @static
* @param {SVGElement} element Element to parse
* @param {Function} callback Callback to execute when fabric.Image object is created
* @param {Object} [options] Options object
* @return {fabric.Image} Instance of fabric.Image
*/
fabric.Image.fromElement = function(element, callback, options) {
var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES);
fabric.Image.fromURL(parsedAttributes['xlink:href'], callback,
extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes));
};
/* _FROM_SVG_END_ */
/**
* Indicates that instances of this type are async
* @static
* @type Boolean
* @default
*/
fabric.Image.async = true;
/**
* Indicates compression level used when generating PNG under Node (in applyFilters). Any of 0-9
* @static
* @type Number
* @default
*/
fabric.Image.pngCompression = 1;
})(typeof exports !== 'undefined' ? exports : this);
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
/**
* @private
* @return {Number} angle value
*/
_getAngleValueForStraighten: function() {
var angle = this.getAngle() % 360;
if (angle > 0) {
return Math.round((angle-1)/90) * 90;
}
return Math.round(angle/90) * 90;
},
/**
* Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer)
* @return {fabric.Object} thisArg
* @chainable
*/
straighten: function() {
this.setAngle(this._getAngleValueForStraighten());
return this;
},
/**
* Same as {@link fabric.Object.prototype.straighten} but with animation
* @param {Object} callbacks Object with callback functions
* @param {Function} [callbacks.onComplete] Invoked on completion
* @param {Function} [callbacks.onChange] Invoked on every step of animation
* @return {fabric.Object} thisArg
* @chainable
*/
fxStraighten: function(callbacks) {
callbacks = callbacks || { };
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: this.get('angle'),
endValue: this._getAngleValueForStraighten(),
duration: this.FX_DURATION,
onChange: function(value) {
_this.setAngle(value);
onChange();
},
onComplete: function() {
_this.setCoords();
onComplete();
},
onStart: function() {
_this.set('active', false);
}
});
return this;
}
});
fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ {
/**
* Straightens object, then rerenders canvas
* @param {fabric.Object} object Object to straighten
* @return {fabric.Canvas} thisArg
* @chainable
*/
straightenObject: function (object) {
object.straighten();
this.renderAll();
return this;
},
/**
* Same as {@link fabric.Canvas.prototype.straightenObject}, but animated
* @param {fabric.Object} object Object to straighten
* @return {fabric.Canvas} thisArg
* @chainable
*/
fxStraightenObject: function (object) {
object.fxStraighten({
onChange: this.renderAll.bind(this)
});
return this;
}
});
/**
* @namespace fabric.Image.filters
* @memberOf fabric.Image
*/
fabric.Image.filters = fabric.Image.filters || { };
/**
* Root filter class from which all filter classes inherit from
* @class fabric.Image.filters.BaseFilter
* @memberOf fabric.Image.filters
*/
fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Image.filters.BaseFilter.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'BaseFilter',
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return { type: this.type };
},
/**
* Returns a JSON representation of an instance
* @return {Object} JSON
*/
toJSON: function() {
// delegate, not alias
return this.toObject();
}
});
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* Brightness filter class
* @class fabric.Image.filters.Brightness
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Brightness = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Brightness',
/**
* Constructor
* @memberOf fabric.Image.filters.Brightness.prototype
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.brightness = options.brightness || 100;
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
brightness = this.brightness;
for (var i = 0, len = data.length; i < len; i += 4) {
data[i] += brightness;
data[i + 1] += brightness;
data[i + 2] += brightness;
}
context.putImageData(imageData, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
brightness: this.brightness
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness
*/
fabric.Image.filters.Brightness.fromObject = function(object) {
return new fabric.Image.filters.Brightness(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* Adapted from <a href="http://www.html5rocks.com/en/tutorials/canvas/imagefilters/">html5rocks article</a>
* @class fabric.Image.filters.Convolute
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Convolute = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Convolute',
/**
* Constructor
* @memberOf fabric.Image.filters.Convolute.prototype
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.opaque = options.opaque;
this.matrix = options.matrix || [ 0, 0, 0,
0, 1, 0,
0, 0, 0 ];
var canvasEl = fabric.util.createCanvasElement();
this.tmpCtx = canvasEl.getContext('2d');
},
/**
* @private
*/
_createImageData: function(w, h) {
return this.tmpCtx.createImageData(w, h);
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var weights = this.matrix;
var context = canvasEl.getContext('2d');
var pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height);
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = this._createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = this.opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
var sy = y;
var sx = x;
var dstOff = (y*w+x)*4;
// calculate the weighed sum of the source image pixels that
// fall under the convolution matrix
var r=0, g=0, b=0, a=0;
for (var cy=0; cy<side; cy++) {
for (var cx=0; cx<side; cx++) {
var scy = sy + cy - halfSide;
var scx = sx + cx - halfSide;
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
var srcOff = (scy*sw+scx)*4;
var wt = weights[cy*side+cx];
r += src[srcOff] * wt;
g += src[srcOff+1] * wt;
b += src[srcOff+2] * wt;
a += src[srcOff+3] * wt;
}
}
}
dst[dstOff] = r;
dst[dstOff+1] = g;
dst[dstOff+2] = b;
dst[dstOff+3] = a + alphaFac*(255-a);
}
}
context.putImageData(output, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
opaque: this.opaque,
matrix: this.matrix
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute
*/
fabric.Image.filters.Convolute.fromObject = function(object) {
return new fabric.Image.filters.Convolute(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* GradientTransparency filter class
* @class fabric.Image.filters.GradientTransparency
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.GradientTransparency = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'GradientTransparency',
/**
* Constructor
* @memberOf fabric.Image.filters.GradientTransparency
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.threshold = options.threshold || 100;
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
threshold = this.threshold,
total = data.length;
for (var i = 0, len = data.length; i < len; i += 4) {
data[i + 3] = threshold + 255 * (total - i) / total;
}
context.putImageData(imageData, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
threshold: this.threshold
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @return {fabric.Image.filters.GradientTransparency} Instance of fabric.Image.filters.GradientTransparency
*/
fabric.Image.filters.GradientTransparency.fromObject = function(object) {
return new fabric.Image.filters.GradientTransparency(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { });
/**
* Grayscale image filter class
* @class fabric.Image.filters.Grayscale
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Grayscale = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Grayscale',
/**
* Applies filter to canvas element
* @memberOf fabric.Image.filters.Grayscale.prototype
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
len = imageData.width * imageData.height * 4,
index = 0,
average;
while (index < len) {
average = (data[index] + data[index + 1] + data[index + 2]) / 3;
data[index] = average;
data[index + 1] = average;
data[index + 2] = average;
index += 4;
}
context.putImageData(imageData, 0, 0);
}
});
/**
* Returns filter instance from an object representation
* @static
* @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale
*/
fabric.Image.filters.Grayscale.fromObject = function() {
return new fabric.Image.filters.Grayscale();
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { });
/**
* Invert filter class
* @class fabric.Image.filters.Invert
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Invert = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Invert',
/**
* Applies filter to canvas element
* @memberOf fabric.Image.filters.Invert.prototype
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
iLen = data.length, i;
for (i = 0; i < iLen; i+=4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
context.putImageData(imageData, 0, 0);
}
});
/**
* Returns filter instance from an object representation
* @static
* @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert
*/
fabric.Image.filters.Invert.fromObject = function() {
return new fabric.Image.filters.Invert();
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* Mask filter class
* @class fabric.Image.filters.Mask
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Mask = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Mask',
/**
* Constructor
* @memberOf fabric.Image.filters.Mask.prototype
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.mask = options.mask;
this.channel = [ 0, 1, 2, 3 ].indexOf(options.channel) > -1 ? options.channel : 0;
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
if (!this.mask) return;
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
maskEl = this.mask.getElement(),
maskCanvasEl = fabric.util.createCanvasElement(),
channel = this.channel,
i,
iLen = imageData.width * imageData.height * 4;
maskCanvasEl.width = maskEl.width;
maskCanvasEl.height = maskEl.height;
maskCanvasEl.getContext('2d').drawImage(maskEl, 0, 0, maskEl.width, maskEl.height);
var maskImageData = maskCanvasEl.getContext('2d').getImageData(0, 0, maskEl.width, maskEl.height),
maskData = maskImageData.data;
for (i = 0; i < iLen; i += 4) {
data[i + 3] = maskData[i + channel];
}
context.putImageData(imageData, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
mask: this.mask.toObject(),
channel: this.channel
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @param {Function} [callback] Callback to invoke when a mask filter instance is created
*/
fabric.Image.filters.Mask.fromObject = function(object, callback) {
var img = fabric.document.createElement('img'),
src = object.mask.src;
/** @ignore */
img.onload = function() {
object.mask = new fabric.Image(img, object.mask);
callback && callback(new fabric.Image.filters.Mask(object));
img = img.onload = img.onerror = null;
};
/** @ignore */
img.onerror = function() {
fabric.log('Error loading ' + img.src);
callback && callback(null, true);
img = img.onload = img.onerror = null;
};
img.src = src;
};
/**
* Indicates that instances of this type are async
* @static
* @type Boolean
* @default
*/
fabric.Image.filters.Mask.async = true;
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* Noise filter class
* @class fabric.Image.filters.Noise
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Noise = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Noise',
/**
* Constructor
* @memberOf fabric.Image.filters.Noise.prototype
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.noise = options.noise || 100;
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
noise = this.noise, rand;
for (var i = 0, len = data.length; i < len; i += 4) {
rand = (0.5 - Math.random()) * noise;
data[i] += rand;
data[i + 1] += rand;
data[i + 2] += rand;
}
context.putImageData(imageData, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
noise: this.noise
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise
*/
fabric.Image.filters.Noise.fromObject = function(object) {
return new fabric.Image.filters.Noise(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* Pixelate filter class
* @class fabric.Image.filters.Pixelate
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Pixelate = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Pixelate',
/**
* Constructor
* @memberOf fabric.Image.filters.Pixelate.prototype
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.blocksize = options.blocksize || 4;
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
iLen = imageData.height,
jLen = imageData.width,
index, i, j, r, g, b, a;
for (i = 0; i < iLen; i += this.blocksize) {
for (j = 0; j < jLen; j += this.blocksize) {
index = (i * 4) * jLen + (j * 4);
r = data[index];
g = data[index+1];
b = data[index+2];
a = data[index+3];
/*
blocksize: 4
[1,x,x,x,1]
[x,x,x,x,1]
[x,x,x,x,1]
[x,x,x,x,1]
[1,1,1,1,1]
*/
for (var _i = i, _ilen = i + this.blocksize; _i < _ilen; _i++) {
for (var _j = j, _jlen = j + this.blocksize; _j < _jlen; _j++) {
index = (_i * 4) * jLen + (_j * 4);
data[index] = r;
data[index + 1] = g;
data[index + 2] = b;
data[index + 3] = a;
}
}
}
}
context.putImageData(imageData, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
blocksize: this.blocksize
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate
*/
fabric.Image.filters.Pixelate.fromObject = function(object) {
return new fabric.Image.filters.Pixelate(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* Remove white filter class
* @class fabric.Image.filters.RemoveWhite
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.RemoveWhite = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.RemoveWhite.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'RemoveWhite',
/**
* Constructor
* @memberOf fabric.Image.filters.RemoveWhite.prototype
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.threshold = options.threshold || 30;
this.distance = options.distance || 20;
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
threshold = this.threshold,
distance = this.distance,
limit = 255 - threshold,
abs = Math.abs,
r, g, b;
for (var i = 0, len = data.length; i < len; i += 4) {
r = data[i];
g = data[i+1];
b = data[i+2];
if (r > limit &&
g > limit &&
b > limit &&
abs(r-g) < distance &&
abs(r-b) < distance &&
abs(g-b) < distance
) {
data[i+3] = 1;
}
}
context.putImageData(imageData, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
threshold: this.threshold,
distance: this.distance
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @return {fabric.Image.filters.RemoveWhite} Instance of fabric.Image.filters.RemoveWhite
*/
fabric.Image.filters.RemoveWhite.fromObject = function(object) {
return new fabric.Image.filters.RemoveWhite(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { });
/**
* Sepia filter class
* @class fabric.Image.filters.Sepia
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Sepia = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Sepia',
/**
* Applies filter to canvas element
* @memberOf fabric.Image.filters.Sepia.prototype
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
iLen = data.length, i, avg;
for (i = 0; i < iLen; i+=4) {
avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2];
data[i] = avg + 100;
data[i + 1] = avg + 50;
data[i + 2] = avg + 255;
}
context.putImageData(imageData, 0, 0);
}
});
/**
* Returns filter instance from an object representation
* @static
* @return {fabric.Image.filters.Sepia} Instance of fabric.Image.filters.Sepia
*/
fabric.Image.filters.Sepia.fromObject = function() {
return new fabric.Image.filters.Sepia();
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { });
/**
* Sepia2 filter class
* @class fabric.Image.filters.Sepia2
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Sepia2 = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Sepia2',
/**
* Applies filter to canvas element
* @memberOf fabric.Image.filters.Sepia.prototype
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
iLen = data.length, i, r, g, b;
for (i = 0; i < iLen; i+=4) {
r = data[i];
g = data[i + 1];
b = data[i + 2];
data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351;
data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203;
data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140;
}
context.putImageData(imageData, 0, 0);
}
});
/**
* Returns filter instance from an object representation
* @static
* @return {fabric.Image.filters.Sepia2} Instance of fabric.Image.filters.Sepia2
*/
fabric.Image.filters.Sepia2.fromObject = function() {
return new fabric.Image.filters.Sepia2();
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend;
/**
* Tint filter class
* @class fabric.Image.filters.Tint
* @memberOf fabric.Image.filters
* @extends fabric.Image.filters.BaseFilter
*/
fabric.Image.filters.Tint = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'Tint',
/**
* Constructor
* @memberOf fabric.Image.filters.Tint.prototype
* @param {Object} [options] Options object
*/
initialize: function(options) {
options = options || { };
this.color = options.color || 0;
},
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo: function(canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
iLen = data.length, i, a;
var rgb = parseInt(this.color, 10).toString(16);
// Pad with leading zeros that may have been stripped off in conversion.
while (rgb.length < 6) rgb = '0' + rgb;
var cr = parseInt('0x' + rgb.substr(0, 2), 16);
var cg = parseInt('0x' + rgb.substr(2, 2), 16);
var cb = parseInt('0x' + rgb.substr(4, 2), 16);
for (i = 0; i < iLen; i+=4) {
a = data[i+3];
if (a > 0){
data[i] = cr;
data[i+1] = cg;
data[i+2] = cb;
}
}
context.putImageData(imageData, 0, 0);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
color: this.color
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @return {fabric.Image.filters.Tint} Instance of fabric.Image.filters.Tint
*/
fabric.Image.filters.Tint.fromObject = function(object) {
return new fabric.Image.filters.Tint(object);
};
})(typeof exports !== 'undefined' ? exports : this);
(function(global) {
var fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
clone = fabric.util.object.clone,
toFixed = fabric.util.toFixed,
supportsLineDash = fabric.StaticCanvas.supports('setLineDash');
if (fabric.Text) {
fabric.warn('fabric.Text is already defined');
return;
}
var stateProperties = fabric.Object.prototype.stateProperties.concat();
stateProperties.push(
'fontFamily',
'fontWeight',
'fontSize',
'text',
'textDecoration',
'textAlign',
'fontStyle',
'lineHeight',
'backgroundColor',
'textBackgroundColor',
'useNative',
'path'
);
/**
* Text class
* @class fabric.Text
* @extends fabric.Object
* @return {fabric.Text} thisArg
*/
fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ {
/**
* Properties which when set cause object to change dimensions
* @type Object
* @private
*/
_dimensionAffectingProps: {
fontSize: true,
fontWeight: true,
fontFamily: true,
textDecoration: true,
fontStyle: true,
lineHeight: true,
stroke: true,
strokeWidth: true,
text: true
},
/**
* Retrieves object's fontSize
* @method getFontSize
* @memberOf fabric.Text.prototype
* @return {String} Font size (in pixels)
*/
/**
* Sets object's fontSize
* @method setFontSize
* @memberOf fabric.Text.prototype
* @param {Number} fontSize Font size (in pixels)
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's fontWeight
* @method getFontWeight
* @memberOf fabric.Text.prototype
* @return {(String|Number)} Font weight
*/
/**
* Sets object's fontWeight
* @method setFontWeight
* @memberOf fabric.Text.prototype
* @param {(Number|String)} fontWeight Font weight
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's fontFamily
* @method getFontFamily
* @memberOf fabric.Text.prototype
* @return {String} Font family
*/
/**
* Sets object's fontFamily
* @method setFontFamily
* @memberOf fabric.Text.prototype
* @param {String} fontFamily Font family
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's text
* @method getText
* @memberOf fabric.Text.prototype
* @return {String} text
*/
/**
* Sets object's text
* @method setText
* @memberOf fabric.Text.prototype
* @param {String} text Text
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's textDecoration
* @method getTextDecoration
* @memberOf fabric.Text.prototype
* @return {String} Text decoration
*/
/**
* Sets object's textDecoration
* @method setTextDecoration
* @memberOf fabric.Text.prototype
* @param {String} textDecoration Text decoration
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's fontStyle
* @method getFontStyle
* @memberOf fabric.Text.prototype
* @return {String} Font style
*/
/**
* Sets object's fontStyle
* @method setFontStyle
* @memberOf fabric.Text.prototype
* @param {String} fontStyle Font style
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's lineHeight
* @method getLineHeight
* @memberOf fabric.Text.prototype
* @return {Number} Line height
*/
/**
* Sets object's lineHeight
* @method setLineHeight
* @memberOf fabric.Text.prototype
* @param {Number} lineHeight Line height
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's textAlign
* @method getTextAlign
* @memberOf fabric.Text.prototype
* @return {String} Text alignment
*/
/**
* Sets object's textAlign
* @method setTextAlign
* @memberOf fabric.Text.prototype
* @param {String} textAlign Text alignment
* @return {fabric.Text}
* @chainable
*/
/**
* Retrieves object's textBackgroundColor
* @method getTextBackgroundColor
* @memberOf fabric.Text.prototype
* @return {String} Text background color
*/
/**
* Sets object's textBackgroundColor
* @method setTextBackgroundColor
* @memberOf fabric.Text.prototype
* @param {String} textBackgroundColor Text background color
* @return {fabric.Text}
* @chainable
*/
/**
* Type of an object
* @type String
* @default
*/
type: 'text',
/**
* Font size (in pixels)
* @type Number
* @default
*/
fontSize: 40,
/**
* Font weight (e.g. bold, normal, 400, 600, 800)
* @type {(Number|String)}
* @default
*/
fontWeight: 'normal',
/**
* Font family
* @type String
* @default
*/
fontFamily: 'Times New Roman',
/**
* Text decoration Possible values: "", "underline", "overline" or "line-through".
* @type String
* @default
*/
textDecoration: '',
/**
* Text alignment. Possible values: "left", "center", or "right".
* @type String
* @default
*/
textAlign: 'left',
/**
* Font style . Possible values: "", "normal", "italic" or "oblique".
* @type String
* @default
*/
fontStyle: '',
/**
* Line height
* @type Number
* @default
*/
lineHeight: 1.3,
/**
* Background color of an entire text box
* @type String
* @default
*/
backgroundColor: '',
/**
* Background color of text lines
* @type String
* @default
*/
textBackgroundColor: '',
/**
* URL of a font file, when using Cufon
* @type String | null
* @default
*/
path: null,
/**
* Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used)
* @type Boolean
* @default
*/
useNative: true,
/**
* List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged})
* as well as for history (undo/redo) purposes
* @type Array
*/
stateProperties: stateProperties,
/**
* When defined, an object is rendered via stroke and this property specifies its color.
* <b>Backwards incompatibility note:</b> This property was named "strokeStyle" until v1.1.6
* @type String
* @default
*/
stroke: null,
/**
* Shadow object representing shadow of this shape.
* <b>Backwards incompatibility note:</b> This property was named "textShadow" (String) until v1.2.11
* @type fabric.Shadow
* @default
*/
shadow: null,
/**
* Constructor
* @param {String} text Text string
* @param {Object} [options] Options object
* @return {fabric.Text} thisArg
*/
initialize: function(text, options) {
options = options || { };
this.text = text;
this.__skipDimension = true;
this.setOptions(options);
this.__skipDimension = false;
this._initDimensions();
this.setCoords();
},
/**
* Renders text object on offscreen canvas, so that it would get dimensions
* @private
*/
_initDimensions: function() {
if (this.__skipDimension) return;
var canvasEl = fabric.util.createCanvasElement();
this._render(canvasEl.getContext('2d'));
},
/**
* Returns string representation of an instance
* @return {String} String representation of text object
*/
toString: function() {
return '#<fabric.Text (' + this.complexity() +
'): { "text": "' + this.text + '", "fontFamily": "' + this.fontFamily + '" }>';
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
var isInPathGroup = this.group && this.group.type !== 'group';
if (isInPathGroup && !this.transformMatrix) {
ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top);
}
else if (isInPathGroup && this.transformMatrix) {
ctx.translate(-this.group.width/2, -this.group.height/2);
}
if (typeof Cufon === 'undefined' || this.useNative === true) {
this._renderViaNative(ctx);
}
else {
this._renderViaCufon(ctx);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderViaNative: function(ctx) {
this.transform(ctx, fabric.isLikelyNode);
this._setTextStyles(ctx);
var textLines = this.text.split(/\r?\n/);
this.width = this._getTextWidth(ctx, textLines);
this.height = this._getTextHeight(ctx, textLines);
this.clipTo && fabric.util.clipContext(this, ctx);
this._renderTextBackground(ctx, textLines);
if (this.textAlign !== 'left' && this.textAlign !== 'justify') {
ctx.save();
ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0);
}
ctx.save();
this._setShadow(ctx);
this._renderTextFill(ctx, textLines);
this._renderTextStroke(ctx, textLines);
this._removeShadow(ctx);
ctx.restore();
if (this.textAlign !== 'left' && this.textAlign !== 'justify') {
ctx.restore();
}
this._renderTextDecoration(ctx, textLines);
this.clipTo && ctx.restore();
this._setBoundaries(ctx, textLines);
this._totalLineHeight = 0;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
*/
_setBoundaries: function(ctx, textLines) {
this._boundaries = [ ];
for (var i = 0, len = textLines.length; i < len; i++) {
var lineWidth = this._getLineWidth(ctx, textLines[i]);
var lineLeftOffset = this._getLineLeftOffset(lineWidth);
this._boundaries.push({
height: this.fontSize * this.lineHeight,
width: lineWidth,
left: lineLeftOffset
});
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_setTextStyles: function(ctx) {
if (this.fill) {
ctx.fillStyle = this.fill.toLive
? this.fill.toLive(ctx)
: this.fill;
}
if (this.stroke) {
ctx.lineWidth = this.strokeWidth;
ctx.lineCap = this.strokeLineCap;
ctx.lineJoin = this.strokeLineJoin;
ctx.miterLimit = this.strokeMiterLimit;
ctx.strokeStyle = this.stroke.toLive
? this.stroke.toLive(ctx)
: this.stroke;
}
ctx.textBaseline = 'alphabetic';
ctx.textAlign = this.textAlign;
ctx.font = this._getFontDeclaration();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
* @return {Number} Height of fabric.Text object
*/
_getTextHeight: function(ctx, textLines) {
return this.fontSize * textLines.length * this.lineHeight;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
* @return {Number} Maximum width of fabric.Text object
*/
_getTextWidth: function(ctx, textLines) {
var maxWidth = ctx.measureText(textLines[0]).width;
for (var i = 1, len = textLines.length; i < len; i++) {
var currentLineWidth = ctx.measureText(textLines[i]).width;
if (currentLineWidth > maxWidth) {
maxWidth = currentLineWidth;
}
}
return maxWidth;
},
/**
* @private
* @param {String} method Method name ("fillText" or "strokeText")
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line Chars to render
* @param {Number} left Left position of text
* @param {Number} top Top position of text
*/
_drawChars: function(method, ctx, chars, left, top) {
ctx[method](chars, left, top);
},
/**
* @private
* @param {String} method Method name ("fillText" or "strokeText")
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line Text to render
* @param {Number} left Left position of text
* @param {Number} top Top position of text
* @param {Number} lineIndex Index of a line in a text
*/
_drawTextLine: function(method, ctx, line, left, top, lineIndex) {
// lift the line by quarter of fontSize
top -= this.fontSize / 4;
// short-circuit
if (this.textAlign !== 'justify') {
this._drawChars(method, ctx, line, left, top, lineIndex);
return;
}
var lineWidth = ctx.measureText(line).width;
var totalWidth = this.width;
if (totalWidth > lineWidth) {
// stretch the line
var words = line.split(/\s+/);
var wordsWidth = ctx.measureText(line.replace(/\s+/g, '')).width;
var widthDiff = totalWidth - wordsWidth;
var numSpaces = words.length - 1;
var spaceWidth = widthDiff / numSpaces;
var leftOffset = 0;
for (var i = 0, len = words.length; i < len; i++) {
this._drawChars(method, ctx, words[i], left + leftOffset, top, lineIndex);
leftOffset += ctx.measureText(words[i]).width + spaceWidth;
}
}
else {
this._drawChars(method, ctx, line, left, top, lineIndex);
}
},
/**
* @private
* @return {Number} Left offset
*/
_getLeftOffset: function() {
if (fabric.isLikelyNode && (this.originX === 'left' || this.originX === 'center')) {
return 0;
}
return -this.width / 2;
},
/**
* @private
* @return {Number} Top offset
*/
_getTopOffset: function() {
if (fabric.isLikelyNode) {
if (this.originY === 'center') {
return -this.height / 2;
}
else if (this.originY === 'bottom') {
return -this.height;
}
return 0;
}
// in browser, text drawing always starts at vertical center
return -this.height / 2;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
*/
_renderTextFill: function(ctx, textLines) {
if (!this.fill && !this.skipFillStrokeCheck) return;
this._boundaries = [ ];
var lineHeights = 0;
for (var i = 0, len = textLines.length; i < len; i++) {
var heightOfLine = this._getHeightOfLine(ctx, i, textLines);
lineHeights += heightOfLine;
this._drawTextLine(
'fillText',
ctx,
textLines[i],
this._getLeftOffset(),
this._getTopOffset() + lineHeights,
i
);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
*/
_renderTextStroke: function(ctx, textLines) {
if (!this.stroke && !this.skipFillStrokeCheck) return;
var lineHeights = 0;
ctx.save();
if (this.strokeDashArray) {
// Spec requires the concatenation of two copies the dash list when the number of elements is odd
if (1 & this.strokeDashArray.length) {
this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray);
}
supportsLineDash && ctx.setLineDash(this.strokeDashArray);
}
ctx.beginPath();
for (var i = 0, len = textLines.length; i < len; i++) {
var heightOfLine = this._getHeightOfLine(ctx, i, textLines);
lineHeights += heightOfLine;
this._drawTextLine(
'strokeText',
ctx,
textLines[i],
this._getLeftOffset(),
this._getTopOffset() + lineHeights,
i
);
}
ctx.closePath();
ctx.restore();
},
_getHeightOfLine: function() {
return this.fontSize * this.lineHeight;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
*/
_renderTextBackground: function(ctx, textLines) {
this._renderTextBoxBackground(ctx);
this._renderTextLinesBackground(ctx, textLines);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderTextBoxBackground: function(ctx) {
if (!this.backgroundColor) return;
ctx.save();
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(
this._getLeftOffset(),
this._getTopOffset(),
this.width,
this.height
);
ctx.restore();
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
*/
_renderTextLinesBackground: function(ctx, textLines) {
if (!this.textBackgroundColor) return;
ctx.save();
ctx.fillStyle = this.textBackgroundColor;
for (var i = 0, len = textLines.length; i < len; i++) {
if (textLines[i] !== '') {
var lineWidth = this._getLineWidth(ctx, textLines[i]);
var lineLeftOffset = this._getLineLeftOffset(lineWidth);
ctx.fillRect(
this._getLeftOffset() + lineLeftOffset,
this._getTopOffset() + (i * this.fontSize * this.lineHeight),
lineWidth,
this.fontSize * this.lineHeight
);
}
}
ctx.restore();
},
/**
* @private
* @param {Number} lineWidth Width of text line
* @return {Number} Line left offset
*/
_getLineLeftOffset: function(lineWidth) {
if (this.textAlign === 'center') {
return (this.width - lineWidth) / 2;
}
if (this.textAlign === 'right') {
return this.width - lineWidth;
}
return 0;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line Text line
* @return {Number} Line width
*/
_getLineWidth: function(ctx, line) {
return this.textAlign === 'justify'
? this.width
: ctx.measureText(line).width;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
*/
_renderTextDecoration: function(ctx, textLines) {
if (!this.textDecoration) return;
// var halfOfVerticalBox = this.originY === 'top' ? 0 : this._getTextHeight(ctx, textLines) / 2;
var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2;
var _this = this;
/** @ignore */
function renderLinesAtOffset(offset) {
for (var i = 0, len = textLines.length; i < len; i++) {
var lineWidth = _this._getLineWidth(ctx, textLines[i]);
var lineLeftOffset = _this._getLineLeftOffset(lineWidth);
ctx.fillRect(
_this._getLeftOffset() + lineLeftOffset,
(offset + (i * _this._getHeightOfLine(ctx, i, textLines))) - halfOfVerticalBox,
lineWidth,
1);
}
}
var fractionOfFontSize = this.fontSize / 4;
if (this.textDecoration.indexOf('underline') > -1) {
renderLinesAtOffset(this.fontSize * this.lineHeight);
}
if (this.textDecoration.indexOf('line-through') > -1) {
renderLinesAtOffset(this.fontSize * this.lineHeight - fractionOfFontSize);
}
if (this.textDecoration.indexOf('overline') > -1) {
renderLinesAtOffset(fractionOfFontSize);
}
},
/**
* @private
*/
_getFontDeclaration: function() {
return [
// node-canvas needs "weight style", while browsers need "style weight"
(fabric.isLikelyNode ? this.fontWeight : this.fontStyle),
(fabric.isLikelyNode ? this.fontStyle : this.fontWeight),
this.fontSize + 'px',
(fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily)
].join(' ');
},
/**
* Renders text instance on a specified context
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Boolean} [noTransform] When true, context is not transformed
*/
render: function(ctx, noTransform) {
// do not render if object is not visible
if (!this.visible) return;
ctx.save();
this._render(ctx);
if (!noTransform && this.active) {
this.drawBorders(ctx);
this.drawControls(ctx);
}
ctx.restore();
},
/**
* Returns object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} Object representation of an instance
*/
toObject: function(propertiesToInclude) {
return extend(this.callSuper('toObject', propertiesToInclude), {
text: this.text,
fontSize: this.fontSize,
fontWeight: this.fontWeight,
fontFamily: this.fontFamily,
fontStyle: this.fontStyle,
lineHeight: this.lineHeight,
textDecoration: this.textDecoration,
textAlign: this.textAlign,
path: this.path,
backgroundColor: this.backgroundColor,
textBackgroundColor: this.textBackgroundColor,
useNative: this.useNative
});
},
/**
* Sets specified property to a specified value
* @param {String} key
* @param {Any} value
* @return {fabric.Text} thisArg
* @chainable
*/
_set: function(key, value) {
if (key === 'fontFamily' && this.path) {
this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3');
}
this.callSuper('_set', key, value);
if (key in this._dimensionAffectingProps) {
this._initDimensions();
this.setCoords();
}
},
/**
* Returns complexity of an instance
* @return {Number} complexity
*/
complexity: function() {
return 1;
}
});
/* _FROM_SVG_START_ */
/**
* List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement})
* @static
* @memberOf fabric.Text
* @see: http://www.w3.org/TR/SVG/text.html#TextElement
*/
fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(
'x y font-family font-style font-weight font-size text-decoration'.split(' '));
/**
* Returns fabric.Text instance from an SVG element (<b>not yet implemented</b>)
* @static
* @memberOf fabric.Text
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @return {fabric.Text} Instance of fabric.Text
*/
fabric.Text.fromElement = function(element, options) {
if (!element) {
return null;
}
var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES);
options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes);
var text = new fabric.Text(element.textContent, options);
/*
Adjust positioning:
x/y attributes in SVG correspond to the bottom-left corner of text bounding box
top/left properties in Fabric correspond to center point of text bounding box
*/
text.set({
left: text.getLeft() + text.getWidth() / 2,
top: text.getTop() - text.getHeight() / 2
});
return text;
};
/* _FROM_SVG_END_ */
/**
* Returns fabric.Text instance from an object representation
* @static
* @memberOf fabric.Text
* @param object {Object} object Object to create an instance from
* @return {fabric.Text} Instance of fabric.Text
*/
fabric.Text.fromObject = function(object) {
return new fabric.Text(object.text, clone(object));
};
fabric.util.createAccessors(fabric.Text);
})(typeof exports !== 'undefined' ? exports : this);
/* Footer for requirejs AMD support */
window.fabric = fabric;
// make sure exports.fabric is always defined when used as 'global' later scopes
var exports = exports || {};
exports.fabric = fabric;
if (typeof define === "function" && define.amd) {
define("fabric", [], function() { return fabric });
}