/* 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(""); 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 dotted stroke in canvas * * @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 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>> 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 String#trim on MDN */ String.prototype.trim = function () { // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); }; } /* _ES5_COMPAT_END_ */ /** * Camelizes a string * @memberOf fabric.util.string * @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, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } /** * 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 Function#bind on MDN * @param {Object} thisArg Object to bind function to * @param {Any[]} [...] Values to pass to a bound function * @return {Function} */ Function.prototype.bind = function(thisArg) { var 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 <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 Easing Equations by Robert Penner * @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(//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(//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 = [ '' ].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 = [ '', '' ].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 https://github.com/mjijackson * @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 https://github.com/mjijackson * @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: * * * * * * * OR * * * * * * * OR * * * * * * * * OR * * * * * * * */ 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); } // 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 <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 <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 <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 '#'; } }); 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 <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. * Backwards incompatibility note: 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 <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 <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 "#"; }, /** * 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 * Backwards incompatibility note: This method was named "setGradientFill" until v1.1.0 * @param {String} property Property name 'stroke' or 'fill' * @param {Object} [options] Options object * @param {String} [options.type] Type of gradient 'radial' or 'linear' * @param {Number} [options.x1=0] x-coordinate of start point * @param {Number} [options.y1=0] y-coordinate of start point * @param {Number} [options.x2=0] x-coordinate of end point * @param {Number} [options.y2=0] y-coordinate of end point * @param {Number} [options.r1=0] Radius of start point (only for radial gradients) * @param {Number} [options.r2=0] Radius of end point (only for radial gradients) * @param {Object} [options.colorStops] Color stops object eg. {0: 'ff0000', 1: '000000'} * @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', …)`) * @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 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 '#'; }, /** * 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 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 '#'; }, /** * 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 '#'; }, /** * 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 '#'; }, /** * 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 html5rocks article * @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= 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. * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 * @type String * @default */ stroke: null, /** * Shadow object representing shadow of this shape. * Backwards incompatibility note: 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 '#'; }, /** * @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 (not yet implemented) * @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 }); }