From 521b10c3ae1a18b2fc8e072b02d4f765757084fb Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 5 Aug 2017 01:33:35 +0200 Subject: [PATCH] add unit testing --- gulpfile.js | 6 + package.json | 4 +- test/fixtures/dog.jpg | Bin 0 -> 155272 bytes test/index.html | 20 + test/unit/test.js | 11 + test/vendor/fabric.js | 26834 ++++++++++++++++++++++++++++++++++++++++ test/vendor/jquery.js | 4 + test/vendor/qunit.css | 436 + test/vendor/qunit.js | 5048 ++++++++ 9 files changed, 32362 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/dog.jpg create mode 100644 test/index.html create mode 100644 test/unit/test.js create mode 100644 test/vendor/fabric.js create mode 100644 test/vendor/jquery.js create mode 100644 test/vendor/qunit.css create mode 100644 test/vendor/qunit.js diff --git a/gulpfile.js b/gulpfile.js index eda0bf9..db20c5e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,6 +3,7 @@ var browserify = require('browserify') var rename = require('gulp-rename'); var source = require('vinyl-source-stream'); var jshint = require('gulp-jshint'); +var qunit = require('gulp-qunit'); gulp.task('build', function() { var app = browserify('./src/main.js'); @@ -12,6 +13,11 @@ gulp.task('build', function() { .pipe(gulp.dest('./dist')) }); +gulp.task('test', ['build'], function() { + return gulp.src('./test/index.html') + .pipe(qunit()); +}); + gulp.task('lint', function() { return gulp.src('./src/**/*.js') .pipe(jshint()) diff --git a/package.json b/package.json index 6e81191..db13499 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "browserify": "^13.1.1", "gulp": "^3.9.0", "jshint": "^2.9.5", + "qunit": "^1.0.0", "gulp-jshint": "^2.0.4", "gulp-rename": "^1.2.2", - "vinyl-source-stream": "^1.1.0" + "vinyl-source-stream": "^1.1.0", + "gulp-qunit": "^1.5.0" } } diff --git a/test/fixtures/dog.jpg b/test/fixtures/dog.jpg new file mode 100644 index 0000000000000000000000000000000000000000..282c52c735c118c327715c6dee04e8809513fa2f GIT binary patch literal 155272 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 + + + + + Image Editor Test Suite + + + +
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/test/unit/test.js b/test/unit/test.js new file mode 100644 index 0000000..fa541b0 --- /dev/null +++ b/test/unit/test.js @@ -0,0 +1,11 @@ +QUnit.module('ImageEditor'); + +QUnit.test('should load image', function( assert ) { + var done = assert.async(); + var editor = new ImageEditor('canvas'); + editor.loadImage('fixtures/dog.jpg', function() { + assert.ok(editor.canvas.getImage()); + done(); + }); +}); + diff --git a/test/vendor/fabric.js b/test/vendor/fabric.js new file mode 100644 index 0000000..b6d2cee --- /dev/null +++ b/test/vendor/fabric.js @@ -0,0 +1,26834 @@ +/* build: `node build.js modules=ALL exclude=json,gestures minifier=uglifyjs` */ + /*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: "1.7.11" }; +if (typeof exports !== 'undefined') { + exports.fabric = fabric; +} + +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + fabric.document = document; + fabric.window = window; + // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) + window.fabric = fabric; +} +else { + // assume we're running under node.js when document/window are not present + fabric.document = require("jsdom") + .jsdom( + decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E") + ); + + if (fabric.document.createWindow) { + fabric.window = fabric.document.createWindow(); + } else { + fabric.window = fabric.document.parentWindow; + } +} + +/** + * 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'; + +/* _FROM_SVG_START_ */ +/** + * Attributes parsed from all SVG elements + * @type array + */ +fabric.SHARED_ATTRIBUTES = [ + "display", + "transform", + "fill", "fill-opacity", "fill-rule", + "opacity", + "stroke", "stroke-dasharray", "stroke-linecap", + "stroke-linejoin", "stroke-miterlimit", + "stroke-opacity", "stroke-width", + "id" +]; +/* _FROM_SVG_END_ */ + +/** + * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion. + */ +fabric.DPI = 96; +fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:e[-+]?\\d+)?)'; +fabric.fontPaths = { }; +fabric.iMatrix = [1, 0, 0, 1, 0, 0]; + +/** + * Cache Object for widths of chars in text rendering. + */ +fabric.charWidthsCache = { }; + +/** + * Device Pixel Ratio + * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html + */ +fabric.devicePixelRatio = fabric.window.devicePixelRatio || + fabric.window.webkitDevicePixelRatio || + fabric.window.mozDevicePixelRatio || + 1; + + +(function() { + + /** + * @private + * @param {String} eventName + * @param {Function} handler + */ + function _removeEventListener(eventName, handler) { + if (!this.__eventListeners[eventName]) { + return; + } + var eventListener = this.__eventListeners[eventName]; + if (handler) { + eventListener[eventListener.indexOf(handler)] = false; + } + else { + fabric.util.array.fill(eventListener, false); + } + } + + /** + * 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. Calling this method + * without arguments removes all handlers for all events + * @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead) + * @memberOf fabric.Observable + * @alias off + * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) + * @param {Function} handler Function to be deleted from EventListeners + * @return {Self} thisArg + * @chainable + */ + function stopObserving(eventName, handler) { + if (!this.__eventListeners) { + return; + } + + // remove all key/value pairs (event name -> event handler) + if (arguments.length === 0) { + for (eventName in this.__eventListeners) { + _removeEventListener.call(this, eventName); + } + } + // one object with key/value pairs was passed + else 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++) { + listenersForEvent[i] && listenersForEvent[i].call(this, options || { }); + } + this.__eventListeners[eventName] = listenersForEvent.filter(function(value) { + return value !== false; + }); + return this; + } + + /** + * @namespace fabric.Observable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#events} + * @see {@link http://fabricjs.com/events|Events demo} + */ + fabric.Observable = { + observe: observe, + stopObserving: stopObserving, + fire: fire, + + on: observe, + off: stopObserving, + trigger: fire + }; +})(); + + +/** + * @namespace fabric.Collection + */ +fabric.Collection = { + + _objects: [], + + /** + * Adds objects to collection, Canvas or Group, then renders canvas + * (if `renderOnAddRemove` is not `false`). + * in case of Group no changes to bounding box are made. + * Objects should be instances of (or inherit from) fabric.Object + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + if (this._onObjectAdded) { + for (var i = 0, length = arguments.length; i < length; i++) { + this._onObjectAdded(arguments[i]); + } + } + 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 + * @chainable + */ + insertAt: function (object, index, nonSplicing) { + var objects = this.getObjects(); + if (nonSplicing) { + objects[index] = object; + } + else { + objects.splice(index, 0, object); + } + this._onObjectAdded && this._onObjectAdded(object); + this.renderOnAddRemove && this.renderAll(); + return this; + }, + + /** + * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) + * @param {...fabric.Object} object Zero or more fabric instances + * @return {Self} thisArg + * @chainable + */ + remove: function() { + var objects = this.getObjects(), + index, somethingRemoved = false; + + for (var i = 0, length = arguments.length; i < length; i++) { + index = objects.indexOf(arguments[i]); + + // only call onObjectRemoved if an object was actually removed + if (index !== -1) { + somethingRemoved = true; + objects.splice(index, 1); + this._onObjectRemoved && this._onObjectRemoved(arguments[i]); + } + } + + this.renderOnAddRemove && somethingRemoved && this.renderAll(); + return this; + }, + + /** + * 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. + * 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 + * @chainable + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(); + for (var i = 0, len = objects.length; i < len; i++) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Returns an array of children objects of this instance + * Type parameter introduced in 1.3.10 + * @param {String} [type] When specified, only objects of this type are returned + * @return {Array} + */ + getObjects: function(type) { + if (typeof type === 'undefined') { + return this._objects; + } + return this._objects.filter(function(o) { + return o.type === type; + }); + }, + + /** + * 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); + } +}; + + +/** + * @namespace fabric.CommonMethods + */ +fabric.CommonMethods = { + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + _setOptions: function(options) { + for (var prop in options) { + this.set(prop, options[prop]); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Gradient to + */ + _initGradient: function(filler, property) { + if (filler && filler.colorStops && !(filler instanceof fabric.Gradient)) { + this.set(property, new fabric.Gradient(filler)); + } + }, + + /** + * @private + * @param {Object} [filler] Options object + * @param {String} [property] property to set the Pattern to + * @param {Function} [callback] callback to invoke after pattern load + */ + _initPattern: function(filler, property, callback) { + if (filler && filler.source && !(filler instanceof fabric.Pattern)) { + this.set(property, new fabric.Pattern(filler, callback)); + } + else { + callback && callback(); + } + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initClipping: function(options) { + if (!options.clipTo || typeof options.clipTo !== 'string') { + return; + } + + var functionBody = fabric.util.getFunctionBody(options.clipTo); + if (typeof functionBody !== 'undefined') { + this.clipTo = new Function('ctx', functionBody); + } + }, + + /** + * @private + */ + _setObject: function(obj) { + for (var prop in obj) { + this._set(prop, obj[prop]); + } + }, + + /** + * 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') { + this._setObject(key); + } + else { + if (typeof value === 'function' && key !== 'clipTo') { + this._set(key, value(this.get(key))); + } + else { + this._set(key, value); + } + } + return this; + }, + + _set: function(key, value) { + this[key] = value; + }, + + /** + * 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; + }, + + /** + * Basic getter + * @param {String} property Property name + * @return {*} value of a property + */ + get: function(property) { + return this[property]; + } +}; + + +(function(global) { + + var sqrt = Math.sqrt, + atan2 = Math.atan2, + pow = Math.pow, + abs = Math.abs, + PiBy180 = Math.PI / 180; + + /** + * @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 {*} value + * @return {Array} original array + */ + removeFromArray: function(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) + */ + getRandomInt: function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }, + + /** + * Transforms degrees to radians. + * @static + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + degreesToRadians: function(degrees) { + return degrees * PiBy180; + }, + + /** + * Transforms radians to degrees. + * @static + * @memberOf fabric.util + * @param {Number} radians value in radians + * @return {Number} value in degrees + */ + radiansToDegrees: function(radians) { + return radians / PiBy180; + }, + + /** + * Rotates `point` around `origin` with `radians` + * @static + * @memberOf fabric.util + * @param {fabric.Point} point The point to rotate + * @param {fabric.Point} origin The origin of the rotation + * @param {Number} radians The radians of the angle for the rotation + * @return {fabric.Point} The new rotated point + */ + rotatePoint: function(point, origin, radians) { + point.subtractEquals(origin); + var v = fabric.util.rotateVector(point, radians); + return new fabric.Point(v.x, v.y).addEquals(origin); + }, + + /** + * Rotates `vector` with `radians` + * @static + * @memberOf fabric.util + * @param {Object} vector The vector to rotate (x and y) + * @param {Number} radians The radians of the angle for the rotation + * @return {Object} The new rotated point + */ + rotateVector: function(vector, radians) { + var sin = Math.sin(radians), + cos = Math.cos(radians), + rx = vector.x * cos - vector.y * sin, + ry = vector.x * sin + vector.y * cos; + return { + x: rx, + y: ry + }; + }, + + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + transformPoint: function(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[2] * p.y, + t[1] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[2] * p.y + t[4], + t[1] * p.x + t[3] * p.y + t[5] + ); + }, + + /** + * Returns coordinates of points's bounding rectangle (left, top, width, height) + * @param {Array} points 4 points array + * @return {Object} Object with left, top, width, height properties + */ + makeBoundingBoxFromPoints: function(points) { + var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x], + minX = fabric.util.array.min(xPoints), + maxX = fabric.util.array.max(xPoints), + width = Math.abs(minX - maxX), + yPoints = [points[0].y, points[1].y, points[2].y, points[3].y], + minY = fabric.util.array.min(yPoints), + maxY = fabric.util.array.max(yPoints), + height = Math.abs(minY - maxY); + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + invertTransform: function(t) { + var a = 1 / (t[0] * t[3] - t[1] * t[2]), + r = [a * t[3], -a * t[1], -a * t[2], a * t[0]], + o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r, true); + r[4] = -o.x; + r[5] = -o.y; + return r; + }, + + /** + * 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} + */ + toFixed: function(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + }, + + /** + * Converts from attribute value to pixel value if applicable. + * Returns converted pixels or original value not converted. + * @param {Number|String} value number to operate on + * @param {Number} fontSize + * @return {Number|String} + */ + parseUnit: function(value, fontSize) { + var unit = /\D{0,2}$/.exec(value), + number = parseFloat(value); + if (!fontSize) { + fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + switch (unit[0]) { + case 'mm': + return number * fabric.DPI / 25.4; + + case 'cm': + return number * fabric.DPI / 2.54; + + case 'in': + return number * fabric.DPI; + + case 'pt': + return number * fabric.DPI / 72; // or * 4 / 3 + + case 'pc': + return number * fabric.DPI / 72 * 12; // or * 16 + + case 'em': + return number * fontSize; + + default: + return number; + } + }, + + /** + * Function which always returns `false`. + * @static + * @memberOf fabric.util + * @return {Boolean} + */ + falseFunction: function() { + 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" + */ + getKlass: function(type, namespace) { + // capitalize first letter only + type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); + return fabric.util.resolveNamespace(namespace)[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) + */ + resolveNamespace: function(namespace) { + if (!namespace) { + return fabric; + } + + var parts = namespace.split('.'), + len = parts.length, i, + obj = global || fabric.window; + + for (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 {*} [context] Context to invoke callback in + * @param {Object} [crossOrigin] crossOrigin value to set image element to + */ + loadImage: function(url, callback, context, crossOrigin) { + if (!url) { + callback && callback.call(context, url); + return; + } + + var img = fabric.util.createImage(); + + /** @ignore */ + img.onload = function () { + callback && callback.call(context, img); + img = img.onload = img.onerror = null; + }; + + /** @ignore */ + img.onerror = function() { + fabric.log('Error loading ' + img.src); + callback && callback.call(context, null, true); + img = img.onload = img.onerror = null; + }; + + // data-urls appear to be buggy with crossOrigin + // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 + // see https://code.google.com/p/chromium/issues/detail?id=315152 + // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 + if (url.indexOf('data') !== 0 && crossOrigin) { + img.crossOrigin = crossOrigin; + } + + img.src = url; + }, + + /** + * Creates corresponding fabric instances from their object representations + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {String} namespace Namespace to get klass "Class" object from + * @param {Function} reviver Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenObjects: function(objects, callback, namespace, reviver) { + objects = objects || []; + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + callback && callback(enlivenedObjects); + } + } + + var enlivenedObjects = [], + numLoadedObjects = 0, + numTotalObjects = objects.length, + forceAsync = true; + + if (!numTotalObjects) { + callback && callback(enlivenedObjects); + return; + } + + objects.forEach(function (o, index) { + // if sparse array + if (!o || !o.type) { + onLoaded(); + return; + } + var klass = fabric.util.getKlass(o.type, namespace); + klass.fromObject(o, function (obj, error) { + error || (enlivenedObjects[index] = obj); + reviver && reviver(o, obj, error); + onLoaded(); + }, forceAsync); + }); + }, + + /** + * Create and wait for loading of patterns + * @static + * @memberOf fabric.util + * @param {Array} objects Objects to enliven + * @param {Function} callback Callback to invoke when all objects are created + * @param {String} namespace Namespace to get klass "Class" object from + * @param {Function} reviver Method for further parsing of object elements, + * called after each fabric object created. + */ + enlivenPatterns: function(patterns, callback) { + patterns = patterns || []; + + function onLoaded() { + if (++numLoadedPatterns === numPatterns) { + callback && callback(enlivenedPatterns); + } + } + + var enlivenedPatterns = [], + numLoadedPatterns = 0, + numPatterns = patterns.length; + + if (!numPatterns) { + callback && callback(enlivenedPatterns); + return; + } + + patterns.forEach(function (p, index) { + if (p && p.source) { + new fabric.Pattern(p, function(pattern) { + enlivenedPatterns[index] = pattern; + onLoaded(); + }); + } + else { + enlivenedPatterns[index] = p; + 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 + * @param {String} path Value to set sourcePath to + * @return {fabric.Object|fabric.PathGroup} + */ + groupSVGElements: function(elements, options, path) { + var object; + + object = new fabric.PathGroup(elements, options); + + if (typeof path !== 'undefined') { + object.sourcePath = 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 + */ + populateWithProperties: function(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 {CanvasRenderingContext2D} ctx context + * @param {Number} x start x coordinate + * @param {Number} y start y coordinate + * @param {Number} x2 end x coordinate + * @param {Number} y2 end y coordinate + * @param {Array} da dash array pattern + */ + drawDashedLine: function(ctx, x, y, x2, y2, da) { + var dx = x2 - x, + dy = y2 - y, + len = sqrt(dx * dx + dy * dy), + rot = atan2(dy, dx), + dc = da.length, + di = 0, + draw = true; + + ctx.save(); + ctx.translate(x, y); + ctx.moveTo(0, 0); + ctx.rotate(rot); + + x = 0; + while (len > x) { + x += da[di++ % dc]; + if (x > len) { + x = len; + } + ctx[draw ? 'lineTo' : 'moveTo'](x, 0); + draw = !draw; + } + + ctx.restore(); + }, + + /** + * 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 + */ + createCanvasElement: function(canvasEl) { + canvasEl || (canvasEl = fabric.document.createElement('canvas')); + /* eslint-disable camelcase */ + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + /* eslint-enable camelcase */ + return canvasEl; + }, + + /** + * Creates image element (works on client and node) + * @static + * @memberOf fabric.util + * @return {HTMLImageElement} HTML image element + */ + createImage: function() { + return fabric.isLikelyNode + ? new (require('canvas').Image)() + : fabric.document.createElement('img'); + }, + + /** + * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array + * @static + * @memberOf fabric.util + * @param {Object} klass "Class" to create accessors for + */ + createAccessors: function(klass) { + var proto = klass.prototype, i, propName, + capitalizedPropName, setterName, getterName; + + for (i = proto.stateProperties.length; i--; ) { + + propName = proto.stateProperties[i]; + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1); + setterName = 'set' + capitalizedPropName; + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + }, + + /** + * @static + * @memberOf fabric.util + * @param {fabric.Object} receiver Object implementing `clipTo` method + * @param {CanvasRenderingContext2D} ctx Context to clip + */ + clipContext: function(receiver, ctx) { + ctx.save(); + ctx.beginPath(); + receiver.clipTo(ctx); + ctx.clip(); + }, + + /** + * Multiply matrix A by matrix B to nest transformations + * @static + * @memberOf fabric.util + * @param {Array} a First transformMatrix + * @param {Array} b Second transformMatrix + * @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices + * @return {Array} The product of the two transform matrices + */ + multiplyTransformMatrices: function(a, b, is2x2) { + // Matrix multiply a * b + return [ + a[0] * b[0] + a[2] * b[1], + a[1] * b[0] + a[3] * b[1], + a[0] * b[2] + a[2] * b[3], + a[1] * b[2] + a[3] * b[3], + is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4], + is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5] + ]; + }, + + /** + * Decomposes standard 2x2 matrix into transform componentes + * @static + * @memberOf fabric.util + * @param {Array} a transformMatrix + * @return {Object} Components of transform + */ + qrDecompose: function(a) { + var angle = atan2(a[1], a[0]), + denom = pow(a[0], 2) + pow(a[1], 2), + scaleX = sqrt(denom), + scaleY = (a[0] * a[3] - a[2] * a [1]) / scaleX, + skewX = atan2(a[0] * a[2] + a[1] * a [3], denom); + return { + angle: angle / PiBy180, + scaleX: scaleX, + scaleY: scaleY, + skewX: skewX / PiBy180, + skewY: 0, + translateX: a[4], + translateY: a[5] + }; + }, + + customTransformMatrix: function(scaleX, scaleY, skewX) { + var skewMatrixX = [1, 0, abs(Math.tan(skewX * PiBy180)), 1], + scaleMatrix = [abs(scaleX), 0, 0, abs(scaleY)]; + return fabric.util.multiplyTransformMatrices(scaleMatrix, skewMatrixX, true); + }, + + resetObjectTransform: function (target) { + target.scaleX = 1; + target.scaleY = 1; + target.skewX = 0; + target.skewY = 0; + target.flipX = false; + target.flipY = false; + target.setAngle(0); + }, + + /** + * Returns string representation of function body + * @param {Function} fn Function to get body of + * @return {String} Function body + */ + getFunctionBody: function(fn) { + return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; + }, + + /** + * Returns true if context has transparent pixel + * at specified location (taking tolerance into account) + * @param {CanvasRenderingContext2D} ctx context + * @param {Number} x x coordinate + * @param {Number} y y coordinate + * @param {Number} tolerance Tolerance + */ + isTransparent: function(ctx, x, y, tolerance) { + + // If tolerance is > 0 adjust start coords to take into account. + // If moves off Canvas fix to 0 + if (tolerance > 0) { + if (x > tolerance) { + x -= tolerance; + } + else { + x = 0; + } + if (y > tolerance) { + y -= tolerance; + } + else { + y = 0; + } + } + + var _isTransparent = true, i, temp, + imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1), + l = imageData.data.length; + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (i = 3; i < l; i += 4) { + temp = imageData.data[i]; + _isTransparent = temp <= 0; + if (_isTransparent === false) { + break; // Stop if colour found + } + } + + imageData = null; + + return _isTransparent; + }, + + /** + * Parse preserveAspectRatio attribute from element + * @param {string} attribute to be parsed + * @return {Object} an object containing align and meetOrSlice attribute + */ + parsePreserveAspectRatioAttribute: function(attribute) { + var meetOrSlice = 'meet', alignX = 'Mid', alignY = 'Mid', + aspectRatioAttrs = attribute.split(' '), align; + + if (aspectRatioAttrs && aspectRatioAttrs.length) { + meetOrSlice = aspectRatioAttrs.pop(); + if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') { + align = meetOrSlice; + meetOrSlice = 'meet'; + } + else if (aspectRatioAttrs.length) { + align = aspectRatioAttrs.pop(); + } + } + //divide align in alignX and alignY + alignX = align !== 'none' ? align.slice(1, 4) : 'none'; + alignY = align !== 'none' ? align.slice(5, 8) : 'none'; + return { + meetOrSlice: meetOrSlice, + alignX: alignX, + alignY: alignY + }; + }, + + /** + * Clear char widths cache for a font family. + * @memberOf fabric.util + * @param {String} [fontFamily] font family to clear + */ + clearFabricFontCache: function(fontFamily) { + if (!fontFamily) { + fabric.charWidthsCache = { }; + } + else if (fabric.charWidthsCache[fontFamily]) { + delete fabric.charWidthsCache[fontFamily]; + } + } + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var arcToSegmentsCache = { }, + segmentToBezierCache = { }, + boundsOfCurveCache = { }, + _join = Array.prototype.join; + + /* Adapted from http://dxr.mozilla.org/mozilla-central/source/content/svg/content/src/nsSVGPathDataParser.cpp + * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here + * http://mozilla.org/MPL/2.0/ + */ + function arcToSegments(toX, toY, rx, ry, large, sweep, rotateX) { + var argsString = _join.call(arguments); + if (arcToSegmentsCache[argsString]) { + return arcToSegmentsCache[argsString]; + } + + var PI = Math.PI, th = rotateX * PI / 180, + sinTh = Math.sin(th), + cosTh = Math.cos(th), + fromX = 0, fromY = 0; + + rx = Math.abs(rx); + ry = Math.abs(ry); + + var px = -cosTh * toX * 0.5 - sinTh * toY * 0.5, + py = -cosTh * toY * 0.5 + sinTh * toX * 0.5, + rx2 = rx * rx, ry2 = ry * ry, py2 = py * py, px2 = px * px, + pl = rx2 * ry2 - rx2 * py2 - ry2 * px2, + root = 0; + + if (pl < 0) { + var s = Math.sqrt(1 - pl / (rx2 * ry2)); + rx *= s; + ry *= s; + } + else { + root = (large === sweep ? -1.0 : 1.0) * + Math.sqrt( pl / (rx2 * py2 + ry2 * px2)); + } + + var cx = root * rx * py / ry, + cy = -root * ry * px / rx, + cx1 = cosTh * cx - sinTh * cy + toX * 0.5, + cy1 = sinTh * cx + cosTh * cy + toY * 0.5, + mTheta = calcVectorAngle(1, 0, (px - cx) / rx, (py - cy) / ry), + dtheta = calcVectorAngle((px - cx) / rx, (py - cy) / ry, (-px - cx) / rx, (-py - cy) / ry); + + if (sweep === 0 && dtheta > 0) { + dtheta -= 2 * PI; + } + else if (sweep === 1 && dtheta < 0) { + dtheta += 2 * PI; + } + + // Convert into cubic bezier segments <= 90deg + var segments = Math.ceil(Math.abs(dtheta / PI * 2)), + result = [], mDelta = dtheta / segments, + mT = 8 / 3 * Math.sin(mDelta / 4) * Math.sin(mDelta / 4) / Math.sin(mDelta / 2), + th3 = mTheta + mDelta; + + for (var i = 0; i < segments; i++) { + result[i] = segmentToBezier(mTheta, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY); + fromX = result[i][4]; + fromY = result[i][5]; + mTheta = th3; + th3 += mDelta; + } + arcToSegmentsCache[argsString] = result; + return result; + } + + function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) { + var argsString2 = _join.call(arguments); + if (segmentToBezierCache[argsString2]) { + return segmentToBezierCache[argsString2]; + } + + var costh2 = Math.cos(th2), + sinth2 = Math.sin(th2), + costh3 = Math.cos(th3), + sinth3 = Math.sin(th3), + toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1, + toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1, + cp1X = fromX + mT * ( -cosTh * rx * sinth2 - sinTh * ry * costh2), + cp1Y = fromY + mT * ( -sinTh * rx * sinth2 + cosTh * ry * costh2), + cp2X = toX + mT * ( cosTh * rx * sinth3 + sinTh * ry * costh3), + cp2Y = toY + mT * ( sinTh * rx * sinth3 - cosTh * ry * costh3); + + segmentToBezierCache[argsString2] = [ + cp1X, cp1Y, + cp2X, cp2Y, + toX, toY + ]; + return segmentToBezierCache[argsString2]; + } + + /* + * Private + */ + function calcVectorAngle(ux, uy, vx, vy) { + var ta = Math.atan2(uy, ux), + tb = Math.atan2(vy, vx); + if (tb >= ta) { + return tb - ta; + } + else { + return 2 * Math.PI - (ta - tb); + } + } + + /** + * Draws arc + * @param {CanvasRenderingContext2D} ctx + * @param {Number} fx + * @param {Number} fy + * @param {Array} coords + */ + fabric.util.drawArc = function(ctx, fx, fy, coords) { + var rx = coords[0], + ry = coords[1], + rot = coords[2], + large = coords[3], + sweep = coords[4], + tx = coords[5], + ty = coords[6], + segs = [[], [], [], []], + segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segsNorm.length; i < len; i++) { + segs[i][0] = segsNorm[i][0] + fx; + segs[i][1] = segsNorm[i][1] + fy; + segs[i][2] = segsNorm[i][2] + fx; + segs[i][3] = segsNorm[i][3] + fy; + segs[i][4] = segsNorm[i][4] + fx; + segs[i][5] = segsNorm[i][5] + fy; + ctx.bezierCurveTo.apply(ctx, segs[i]); + } + }; + + /** + * Calculate bounding box of a elliptic-arc + * @param {Number} fx start point of arc + * @param {Number} fy + * @param {Number} rx horizontal radius + * @param {Number} ry vertical radius + * @param {Number} rot angle of horizontal axe + * @param {Number} large 1 or 0, whatever the arc is the big or the small on the 2 points + * @param {Number} sweep 1 or 0, 1 clockwise or counterclockwise direction + * @param {Number} tx end point of arc + * @param {Number} ty + */ + fabric.util.getBoundsOfArc = function(fx, fy, rx, ry, rot, large, sweep, tx, ty) { + + var fromX = 0, fromY = 0, bound, bounds = [], + segs = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); + + for (var i = 0, len = segs.length; i < len; i++) { + bound = getBoundsOfCurve(fromX, fromY, segs[i][0], segs[i][1], segs[i][2], segs[i][3], segs[i][4], segs[i][5]); + bounds.push({ x: bound[0].x + fx, y: bound[0].y + fy }); + bounds.push({ x: bound[1].x + fx, y: bound[1].y + fy }); + fromX = segs[i][4]; + fromY = segs[i][5]; + } + return bounds; + }; + + /** + * Calculate bounding box of a beziercurve + * @param {Number} x0 starting point + * @param {Number} y0 + * @param {Number} x1 first control point + * @param {Number} y1 + * @param {Number} x2 secondo control point + * @param {Number} y2 + * @param {Number} x3 end of beizer + * @param {Number} y3 + */ + // taken from http://jsbin.com/ivomiq/56/edit no credits available for that. + function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) { + var argsString = _join.call(arguments); + if (boundsOfCurveCache[argsString]) { + return boundsOfCurveCache[argsString]; + } + + var sqrt = Math.sqrt, + min = Math.min, max = Math.max, + abs = Math.abs, tvalues = [], + bounds = [[], []], + a, b, c, t, t1, t2, b2ac, sqrtb2ac; + + b = 6 * x0 - 12 * x1 + 6 * x2; + a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; + c = 3 * x1 - 3 * x0; + + for (var i = 0; i < 2; ++i) { + if (i > 0) { + b = 6 * y0 - 12 * y1 + 6 * y2; + a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; + c = 3 * y1 - 3 * y0; + } + + if (abs(a) < 1e-12) { + if (abs(b) < 1e-12) { + continue; + } + t = -c / b; + if (0 < t && t < 1) { + tvalues.push(t); + } + continue; + } + b2ac = b * b - 4 * c * a; + if (b2ac < 0) { + continue; + } + sqrtb2ac = sqrt(b2ac); + t1 = (-b + sqrtb2ac) / (2 * a); + if (0 < t1 && t1 < 1) { + tvalues.push(t1); + } + t2 = (-b - sqrtb2ac) / (2 * a); + if (0 < t2 && t2 < 1) { + tvalues.push(t2); + } + } + + var x, y, j = tvalues.length, jlen = j, mt; + while (j--) { + t = tvalues[j]; + mt = 1 - t; + x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); + bounds[0][j] = x; + + y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); + bounds[1][j] = y; + } + + bounds[0][jlen] = x0; + bounds[1][jlen] = y0; + bounds[0][jlen + 1] = x3; + bounds[1][jlen + 1] = y3; + var result = [ + { + x: min.apply(null, bounds[0]), + y: min.apply(null, bounds[1]) + }, + { + x: max.apply(null, bounds[0]), + y: max.apply(null, bounds[1]) + } + ]; + boundsOfCurveCache[argsString] = result; + return result; + } + + fabric.util.getBoundsOfCurve = getBoundsOfCurve; + +})(); + + +(function() { + + var slice = Array.prototype.slice; + + /* _ES5_COMPAT_START_ */ + + if (!Array.prototype.indexOf) { + /** + * Finds index of an element in an array + * @param {*} searchElement + * @return {Number} + */ + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + if (this === void 0 || this === null) { + throw new TypeError(); + } + var t = Object(this), len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } + else if (n !== 0 && n !== Number.POSITIVE_INFINITY && n !== Number.NEGATIVE_INFINITY) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; + } + + if (!Array.prototype.forEach) { + /** + * Iterates an array, invoking callback for each element + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.forEach = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + fn.call(context, this[i], i, this); + } + } + }; + } + + if (!Array.prototype.map) { + /** + * Returns a result of iterating over an array, invoking callback for each element + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.map = function(fn, context) { + var result = []; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + result[i] = fn.call(context, this[i], i, this); + } + } + return result; + }; + } + + if (!Array.prototype.every) { + /** + * Returns true if a callback returns truthy value for all elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Boolean} + */ + Array.prototype.every = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && !fn.call(context, this[i], i, this)) { + return false; + } + } + return true; + }; + } + + if (!Array.prototype.some) { + /** + * Returns true if a callback returns truthy value for at least one element in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Boolean} + */ + Array.prototype.some = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && fn.call(context, this[i], i, this)) { + return true; + } + } + return false; + }; + } + + if (!Array.prototype.filter) { + /** + * Returns the result of iterating over elements in an array + * @param {Function} fn Callback to invoke for each element + * @param {Object} [context] Context to invoke callback in + * @return {Array} + */ + Array.prototype.filter = function(fn, context) { + var result = [], val; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + val = this[i]; // in case fn mutates this + if (fn.call(context, val, i, this)) { + result.push(val); + } + } + } + return result; + }; + } + + if (!Array.prototype.reduce) { + /** + * Returns "folded" (reduced) result of iterating over elements in an array + * @param {Function} fn Callback to invoke for each element + * @return {*} + */ + Array.prototype.reduce = function(fn /*, initial*/) { + var len = this.length >>> 0, + i = 0, + rv; + + if (arguments.length > 1) { + rv = arguments[1]; + } + else { + do { + if (i in this) { + rv = this[i++]; + break; + } + // if array contains no values, no initial value to return + if (++i >= len) { + throw new TypeError(); + } + } + while (true); + } + for (; i < len; i++) { + if (i in this) { + rv = fn.call(null, rv, this[i], i, this); + } + } + return rv; + }; + } + + /* _ES5_COMPAT_END_ */ + + /** + * Invokes method on all items in a given array + * @memberOf fabric.util.array + * @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 {*} + */ + function max(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 >= value2; + }); + } + + /** + * Finds minimum value in array (not necessarily "first" one) + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + * @return {*} + */ + function min(array, byProperty) { + return find(array, byProperty, function(value1, value2) { + return value1 < value2; + }); + } + + /** + * @private + */ + function fill(array, value) { + var k = array.length; + while (k--) { + array[k] = value; + } + return array; + } + + /** + * @private + */ + function find(array, byProperty, condition) { + if (!array || array.length === 0) { + return; + } + + var i = array.length - 1, + result = byProperty ? array[i][byProperty] : array[i]; + if (byProperty) { + while (i--) { + if (condition(array[i][byProperty], result)) { + result = array[i][byProperty]; + } + } + } + else { + while (i--) { + if (condition(array[i], result)) { + result = array[i]; + } + } + } + return result; + } + + /** + * @namespace fabric.util.array + */ + fabric.util.array = { + fill: fill, + invoke: invoke, + min: min, + max: max + }; + +})(); + + +(function() { + /** + * Copies all enumerable properties of one js object to another + * Does not clone or extend fabric.Object subclasses. + * @memberOf fabric.util.object + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + * @return {Object} + */ + + function extend(destination, source, deep) { + // JScript DontEnum bug is not taken care of + // the deep clone is for internal use, is not meant to avoid + // javascript traps or cloning html element or self referenced objects. + if (deep) { + if (!fabric.isLikelyNode && source instanceof Element) { + // avoid cloning deep images, canvases, + destination = source; + } + else if (source instanceof Array) { + destination = []; + for (var i = 0, len = source.length; i < len; i++) { + destination[i] = extend({ }, source[i], deep); + } + } + else if (source && typeof source === 'object') { + for (var property in source) { + if (source.hasOwnProperty(property)) { + destination[property] = extend({ }, source[property], deep); + } + } + } + else { + // this sounds odd for an extend but is ok for recursive use + destination = source; + } + } + else { + 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, deep) { + return extend({ }, object, deep); + } + + /** @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 + * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized + * and other letters stay untouched, if false first letter is capitalized + * and other letters are converted to lowercase. + * @return {String} Capitalized version of a string + */ + function capitalize(string, firstLetterOnly) { + return string.charAt(0).toUpperCase() + + (firstLetterOnly ? string.slice(1) : 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 _this = this, args = slice.call(arguments, 1), bound; + if (args.length) { + bound = function() { + return apply.call(_this, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); + }; + } + else { + /** @ignore */ + bound = function() { + return apply.call(_this, this instanceof Dummy ? this : thisArg, arguments); + }; + } + Dummy.prototype = this.prototype; + bound.prototype = new Dummy(); + + return bound; + }; + } + +})(); +/* _ES5_COMPAT_END_ */ + + +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }, + + IS_DONTENUM_BUGGY = (function() { + for (var p in { toString: 1 }) { + if (p === 'toString') { + return false; + } + } + return true; + })(), + + /** @ignore */ + 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 parentMethod = null, + _this = this; + + // climb prototype chain to find method not equal to callee's method + while (_this.constructor.superclass) { + var superClassMethod = _this.constructor.superclass.prototype[methodName]; + if (_this[methodName] !== superClassMethod) { + parentMethod = superClassMethod; + break; + } + // eslint-disable-next-line + _this = _this.constructor.superclass.prototype; + } + + if (!parentMethod) { + return console.log('tried to callSuper ' + methodName + ', method not found in prototype chain', this); + } + + return (arguments.length > 1) + ? parentMethod.apply(this, slice.call(arguments, 1)) + : parentMethod.call(this); + } + + /** + * Helper for creation of "classes". + * @memberOf fabric.util + * @param {Function} [parent] optional "Class" to inherit from + * @param {Object} [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 () { + + var unknown = 'unknown'; + + /* EVENT HANDLING */ + + function areHostMethods(object) { + var methodNames = Array.prototype.slice.call(arguments, 1), + t, i, len = methodNames.length; + for (i = 0; i < len; i++) { + t = typeof object[methodNames[i]]; + if (!(/^(?:function|object|unknown)$/).test(t)) { + return false; + } + } + return true; + } + + /** @ignore */ + var getElement, + setElement, + getUniqueId = (function () { + var uid = 0; + return function (element) { + return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); + }; + })(); + + (function () { + var elements = { }; + /** @ignore */ + getElement = function (uid) { + return elements[uid]; + }; + /** @ignore */ + setElement = function (uid, element) { + elements[uid] = element; + }; + })(); + + function createListener(uid, handler) { + return { + handler: handler, + wrappedHandler: createWrappedHandler(uid, handler) + }; + } + + function createWrappedHandler(uid, handler) { + return function (e) { + handler.call(getElement(uid), e || fabric.window.event); + }; + } + + function createDispatcher(uid, eventName) { + return function (e) { + if (handlers[uid] && handlers[uid][eventName]) { + var handlersForEvent = handlers[uid][eventName]; + for (var i = 0, len = handlersForEvent.length; i < len; i++) { + handlersForEvent[i].call(this, e || fabric.window.event); + } + } + }; + } + + var shouldUseAddListenerRemoveListener = ( + areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && + areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), + + shouldUseAttachEventDetachEvent = ( + areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && + areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), + + // IE branch + listeners = { }, + + // DOM L0 branch + handlers = { }, + + addListener, removeListener; + + if (shouldUseAddListenerRemoveListener) { + /** @ignore */ + addListener = function (element, eventName, handler, options) { + // since ie10 or ie9 can use addEventListener but they do not support options, i need to check + element.addEventListener(eventName, handler, shouldUseAttachEventDetachEvent ? false : options); + }; + /** @ignore */ + removeListener = function (element, eventName, handler, options) { + element.removeEventListener(eventName, handler, shouldUseAttachEventDetachEvent ? false : options); + }; + } + + else if (shouldUseAttachEventDetachEvent) { + /** @ignore */ + addListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + setElement(uid, element); + if (!listeners[uid]) { + listeners[uid] = { }; + } + if (!listeners[uid][eventName]) { + listeners[uid][eventName] = []; + + } + var listener = createListener(uid, handler); + listeners[uid][eventName].push(listener); + element.attachEvent('on' + eventName, listener.wrappedHandler); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + var uid = getUniqueId(element), listener; + if (listeners[uid] && listeners[uid][eventName]) { + for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) { + listener = listeners[uid][eventName][i]; + if (listener && listener.handler === handler) { + element.detachEvent('on' + eventName, listener.wrappedHandler); + listeners[uid][eventName][i] = null; + } + } + } + }; + } + else { + /** @ignore */ + addListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + if (!handlers[uid]) { + handlers[uid] = { }; + } + if (!handlers[uid][eventName]) { + handlers[uid][eventName] = []; + var existingHandler = element['on' + eventName]; + if (existingHandler) { + handlers[uid][eventName].push(existingHandler); + } + element['on' + eventName] = createDispatcher(uid, eventName); + } + handlers[uid][eventName].push(handler); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + if (handlers[uid] && handlers[uid][eventName]) { + var handlersForEvent = handlers[uid][eventName]; + for (var i = 0, len = handlersForEvent.length; i < len; i++) { + if (handlersForEvent[i] === handler) { + handlersForEvent.splice(i, 1); + } + } + } + }; + } + + /** + * 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 Event object + */ + function getPointer(event) { + event || (event = fabric.window.event); + + var element = event.target || + (typeof event.srcElement !== unknown ? event.srcElement : null), + + scroll = fabric.util.getScrollLeftTop(element); + + return { + x: pointerX(event) + scroll.left, + y: pointerY(event) + scroll.top + }; + } + + var pointerX = function(event) { + // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) + // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] + // need to investigate later + return (typeof event.clientX !== unknown ? event.clientX : 0); + }, + + pointerY = function(event) { + return (typeof event.clientY !== unknown ? event.clientY : 0); + }; + + function _getPointer(event, pageProp, clientProp) { + var touchProp = event.type === 'touchend' ? 'changedTouches' : 'touches'; + + return (event[touchProp] && event[touchProp][0] + ? (event[touchProp][0][pageProp] - (event[touchProp][0][pageProp] - event[touchProp][0][clientProp])) + || event[clientProp] + : event[clientProp]); + } + + if (fabric.isTouchSupported) { + pointerX = function(event) { + return _getPointer(event, 'pageX', 'clientX'); + }; + pointerY = function(event) { + return _getPointer(event, 'pageY', 'clientY'); + }; + } + + fabric.util.getPointer = getPointer; + + fabric.util.object.extend(fabric.util, fabric.Observable); + +})(); + + +(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; + } + + var sliceCanConvertNodelists, + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + 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 && (' ' + 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 element scroll offsets + * @memberOf fabric.util + * @param {HTMLElement} element Element to operate on + * @return {Object} Object with left/top values + */ + function getScrollLeftTop(element) { + + var left = 0, + top = 0, + docElement = fabric.document.documentElement, + body = fabric.document.body || { + scrollLeft: 0, scrollTop: 0 + }; + + // While loop checks (and then sets element to) .parentNode OR .host + // to account for ShadowDOM. We still want to traverse up out of ShadowDOM, + // but the .parentNode of a root ShadowDOM node will always be null, instead + // it should be accessed through .host. See http://stackoverflow.com/a/24765528/4383938 + while (element && (element.parentNode || element.host)) { + + // Set element to element parent, or 'host' in case of ShadowDOM + element = element.parentNode || element.host; + + if (element === fabric.document) { + left = body.scrollLeft || docElement.scrollLeft || 0; + top = body.scrollTop || docElement.scrollTop || 0; + } + else { + left += element.scrollLeft || 0; + top += element.scrollTop || 0; + } + + if (element.nodeType === 1 && + fabric.util.getElementStyle(element, 'position') === 'fixed') { + break; + } + } + + return { left: left, top: top }; + } + + /** + * 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, + doc = element && element.ownerDocument, + box = { left: 0, top: 0 }, + offset = { left: 0, top: 0 }, + scrollLeftTop, + offsetAttributes = { + borderLeftWidth: 'left', + borderTopWidth: 'top', + paddingLeft: 'left', + paddingTop: 'top' + }; + + if (!doc) { + return offset; + } + + 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(); + } + + scrollLeftTop = getScrollLeftTop(element); + + return { + left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, + top: box.top + scrollLeftTop.top - (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. + */ + var getElementStyle; + if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { + getElementStyle = function(element, attr) { + var style = fabric.document.defaultView.getComputedStyle(element, null); + return style ? style[attr] : undefined; + }; + } + else { + getElementStyle = function(element, attr) { + var value = element.style[attr]; + if (!value && element.currentStyle) { + value = element.currentStyle[attr]; + } + return value; + }; + } + + (function () { + var style = fabric.document.documentElement.style, + 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.getScrollLeftTop = getScrollLeftTop; + 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 {String} [options.parameters] parameters to append to url in GET or in body + * @param {String} [options.body] body to send with POST or PUT request + * @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 = options.body || options.parameters; + + /** @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; +})(); + + +/** + * Wrapper around `console.log` (when available) + * @param {*} [values] Values to log + */ +fabric.log = function() { }; + +/** + * Wrapper around `console.warn` (when available) + * @param {*} [values] Values to log as a warning + */ +fabric.warn = function() { }; + +/* eslint-disable */ +if (typeof console !== 'undefined') { + + ['log', 'warn'].forEach(function(methodName) { + + if (typeof console[methodName] !== 'undefined' && + typeof console[methodName].apply === 'function') { + + fabric[methodName] = function() { + return console[methodName].apply(console, arguments); + }; + } + }); +} +/* eslint-enable */ + + +(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 (in ms) + */ + function animate(options) { + + requestAnimFrame(function(timestamp) { + options || (options = { }); + + var start = timestamp || +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;}, + 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(ticktime) { + time = ticktime || +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); + })(start); + }); + + } + + 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/ + * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + function requestAnimFrame() { + return _requestAnimFrame.apply(fabric.window, arguments); + } + + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + +})(); + + +(function() { + // Calculate an in-between color. Returns a "rgba()" string. + // Credit: Edwin Martin + // http://www.bitstorm.org/jquery/color-animation/jquery.animate-colors.js + function calculateColor(begin, end, pos) { + var color = 'rgba(' + + parseInt((begin[0] + pos * (end[0] - begin[0])), 10) + ',' + + parseInt((begin[1] + pos * (end[1] - begin[1])), 10) + ',' + + parseInt((begin[2] + pos * (end[2] - begin[2])), 10); + + color += ',' + (begin && end ? parseFloat(begin[3] + pos * (end[3] - begin[3])) : 1); + color += ')'; + return color; + } + + /** + * Changes the color from one to another within certain period of time, invoking callbacks as value is being changed. + * @memberOf fabric.util + * @param {String} fromColor The starting color in hex or rgb(a) format. + * @param {String} toColor The starting color in hex or rgb(a) format. + * @param {Number} [duration] Duration of change (in ms). + * @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 {Function} [options.colorEasing] Easing function. Note that this function only take two arguments (currentTime, duration). Thus the regular animation easing functions cannot be used. + */ + function animateColor(fromColor, toColor, duration, options) { + var startColor = new fabric.Color(fromColor).getSource(), + endColor = new fabric.Color(toColor).getSource(); + + options = options || {}; + + fabric.util.animate(fabric.util.object.extend(options, { + duration: duration || 500, + startValue: startColor, + endValue: endColor, + byValue: endColor, + easing: function (currentTime, startValue, byValue, duration) { + var posValue = options.colorEasing + ? options.colorEasing(currentTime, duration) + : 1 - Math.cos(currentTime / duration * (Math.PI / 2)); + return calculateColor(startValue, byValue, posValue); + } + })); + } + + fabric.util.animateColor = animateColor; + +})(); + + +(function() { + + function normalize(a, c, p, s) { + if (a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + //handle the 0/0 case: + if (c === 0 && a === 0) { + s = p / (2 * Math.PI) * Math.asin(1); + } + else { + s = p / (2 * Math.PI) * Math.asin(c / a); + } + } + return { a: a, c: c, p: p, s: s }; + } + + function elastic(opts, t, d) { + return opts.a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); + } + + /** + * 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, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return -elastic(opts, t, d) + b; + } + + /** + * Elastic easing out + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d; + if (t === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + var opts = normalize(a, c, p, s); + return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) + opts.c + b; + } + + /** + * Elastic easing in and out + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s = 1.70158, p = 0, a = c; + if (t === 0) { + return b; + } + t /= d / 2; + if (t === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + var opts = normalize(a, c, p, s); + if (t < 1) { + return -0.5 * elastic(opts, t, d) + b; + } + return opts.a * Math.pow(2, -10 * (t -= 1)) * + Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) * 0.5 + opts.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 = { + + /** + * Quadratic easing in + * @memberOf fabric.util.ease + */ + easeInQuad: function(t, b, c, d) { + return c * (t /= d) * t + b; + }, + + /** + * Quadratic easing out + * @memberOf fabric.util.ease + */ + easeOutQuad: function(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + + /** + * Quadratic easing in and out + * @memberOf fabric.util.ease + */ + easeInOutQuad: function(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 + */ + easeInCubic: function(t, b, c, d) { + return c * (t /= d) * t * t + b; + }, + + 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) { + + 'use strict'; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + parseUnit = fabric.util.parseUnit, + multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, + + reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/i, + reViewBoxTagNames = /^(symbol|image|marker|pattern|view|svg)$/i, + reNotAllowedAncestors = /^(?:pattern|defs|symbol|metadata|clipPath|mask)$/i, + reAllowedParents = /^(symbol|g|a|svg)$/i, + + attributesMap = { + cx: 'left', + x: 'left', + r: 'radius', + cy: 'top', + y: 'top', + display: 'visible', + visibility: 'visible', + transform: 'transformMatrix', + 'fill-opacity': 'fillOpacity', + 'fill-rule': 'fillRule', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'font-style': 'fontStyle', + 'font-weight': 'fontWeight', + 'stroke-dasharray': 'strokeDashArray', + 'stroke-linecap': 'strokeLineCap', + 'stroke-linejoin': 'strokeLineJoin', + 'stroke-miterlimit': 'strokeMiterLimit', + 'stroke-opacity': 'strokeOpacity', + 'stroke-width': 'strokeWidth', + 'text-decoration': 'textDecoration', + 'text-anchor': 'originX', + opacity: 'opacity' + }, + + colorAttributes = { + stroke: 'strokeOpacity', + fill: 'fillOpacity' + }; + + fabric.cssRules = { }; + fabric.gradientDefs = { }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + function normalizeValue(attr, value, parentAttributes, fontSize) { + var isArray = Object.prototype.toString.call(value) === '[object Array]', + parsed; + + if ((attr === 'fill' || attr === 'stroke') && value === 'none') { + value = ''; + } + else if (attr === 'strokeDashArray') { + if (value === 'none') { + value = null; + } + else { + value = value.replace(/,/g, ' ').split(/\s+/).map(function(n) { + return parseFloat(n); + }); + } + } + else if (attr === 'transformMatrix') { + if (parentAttributes && parentAttributes.transformMatrix) { + value = multiplyTransformMatrices( + parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); + } + else { + value = fabric.parseTransformAttribute(value); + } + } + else if (attr === 'visible') { + value = (value === 'none' || value === 'hidden') ? false : true; + // display=none on parent element always takes precedence over child element + if (parentAttributes && parentAttributes.visible === false) { + value = false; + } + } + else if (attr === 'opacity') { + value = parseFloat(value); + if (parentAttributes && typeof parentAttributes.opacity !== 'undefined') { + value *= parentAttributes.opacity; + } + } + else if (attr === 'originX' /* text-anchor */) { + value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; + } + else { + parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); + } + + return (!isArray && isNaN(parsed) ? value : parsed); + } + + /** + * @private + * @param {Object} attributes Array of attributes to parse + */ + function _setStrokeFillOpacity(attributes) { + for (var attr in colorAttributes) { + + if (typeof attributes[colorAttributes[attr]] === 'undefined' || attributes[attr] === '') { + continue; + } + + if (typeof attributes[attr] === 'undefined') { + if (!fabric.Object.prototype[attr]) { + continue; + } + attributes[attr] = fabric.Object.prototype[attr]; + } + + 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(); + } + return attributes; + } + + /** + * @private + */ + function _getMultipleNodes(doc, nodeNames) { + var nodeName, nodeArray = [], nodeList; + for (var i = 0; i < nodeNames.length; i++) { + nodeName = nodeNames[i]; + nodeList = doc.getElementsByTagName(nodeName); + nodeArray = nodeArray.concat(Array.prototype.slice.call(nodeList)); + } + return nodeArray; + } + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @param {String} attributeValue String containing attribute value + * @return {Array} Array of 6 elements representing transformation matrix + */ + fabric.parseTransformAttribute = (function() { + function rotateMatrix(matrix, args) { + var cos = Math.cos(args[0]), sin = Math.sin(args[0]), + x = 0, y = 0; + if (args.length === 3) { + x = args[1]; + y = args[2]; + } + + matrix[0] = cos; + matrix[1] = sin; + matrix[2] = -sin; + matrix[3] = cos; + matrix[4] = x - (cos * x - sin * y); + matrix[5] = y - (sin * x + cos * y); + } + + function scaleMatrix(matrix, args) { + var multiplierX = args[0], + multiplierY = (args.length === 2) ? args[1] : args[0]; + + matrix[0] = multiplierX; + matrix[3] = multiplierY; + } + + function skewMatrix(matrix, args, pos) { + matrix[pos] = Math.tan(fabric.util.degreesToRadians(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 = fabric.reNum, + + commaWsp = '(?:\\s+,?\\s*|,\\s*)', + + skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', + + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + ')' + + commaWsp + '(' + number + '))?\\s*\\))', + + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + + commaWsp + '(' + number + '))?\\s*\\))', + + matrix = '(?:(matrix)\\s*\\(\\s*' + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + commaWsp + + '(' + number + ')' + + '\\s*\\))', + + transform = '(?:' + + matrix + '|' + + translate + '|' + + scale + '|' + + rotate + '|' + + skewX + '|' + + skewY + + ')', + + transforms = '(?:' + transform + '(?:' + commaWsp + '*' + transform + ')*' + ')', + + transformList = '^\\s*(?:' + transforms + '?)\\s*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transformList), + // == end transform regexp + + reTransform = new RegExp(transform, 'g'); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(), + 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) { + // match !== '' && match != null + return (!!match); + }), + operation = m[1], + args = m.slice(2).map(parseFloat); + + switch (operation) { + case 'translate': + translateMatrix(matrix, args); + break; + case 'rotate': + args[0] = fabric.util.degreesToRadians(args[0]); + rotateMatrix(matrix, args); + break; + case 'scale': + scaleMatrix(matrix, args); + break; + case 'skewX': + skewMatrix(matrix, args, 2); + break; + case 'skewY': + skewMatrix(matrix, args, 1); + 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; + }; + })(); + + /** + * @private + */ + function parseStyleString(style, oStyle) { + var attr, value; + style.replace(/;\s*$/, '').split(';').forEach(function (chunk) { + var pair = chunk.split(':'); + + attr = pair[0].trim().toLowerCase(); + value = pair[1].trim(); + + oStyle[attr] = value; + }); + } + + /** + * @private + */ + function parseStyleObject(style, oStyle) { + var attr, value; + for (var prop in style) { + if (typeof style[prop] === 'undefined') { + continue; + } + + attr = prop.toLowerCase(); + value = style[prop]; + + oStyle[attr] = value; + } + } + + /** + * @private + */ + function getGlobalStylesForElement(element, svgUid) { + var styles = { }; + for (var rule in fabric.cssRules[svgUid]) { + if (elementMatchesRule(element, rule.split(' '))) { + for (var property in fabric.cssRules[svgUid][rule]) { + styles[property] = fabric.cssRules[svgUid][rule][property]; + } + } + } + return styles; + } + + /** + * @private + */ + function elementMatchesRule(element, selectors) { + var firstMatching, parentMatching = true; + //start from rightmost selector. + firstMatching = selectorMatches(element, selectors.pop()); + if (firstMatching && selectors.length) { + parentMatching = doesSomeParentMatch(element, selectors); + } + return firstMatching && parentMatching && (selectors.length === 0); + } + + function doesSomeParentMatch(element, selectors) { + var selector, parentMatching = true; + while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { + if (parentMatching) { + selector = selectors.pop(); + } + element = element.parentNode; + parentMatching = selectorMatches(element, selector); + } + return selectors.length === 0; + } + + /** + * @private + */ + function selectorMatches(element, selector) { + var nodeName = element.nodeName, + classNames = element.getAttribute('class'), + id = element.getAttribute('id'), matcher; + // i check if a selector matches slicing away part from it. + // if i get empty string i should match + matcher = new RegExp('^' + nodeName, 'i'); + selector = selector.replace(matcher, ''); + if (id && selector.length) { + matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + if (classNames && selector.length) { + classNames = classNames.split(' '); + for (var i = classNames.length; i--;) { + matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); + selector = selector.replace(matcher, ''); + } + } + return selector.length === 0; + } + + /** + * @private + * to support IE8 missing getElementById on SVGdocument + */ + function elementById(doc, id) { + var el; + doc.getElementById && (el = doc.getElementById(id)); + if (el) { + return el; + } + var node, i, nodelist = doc.getElementsByTagName('*'); + for (i = 0; i < nodelist.length; i++) { + node = nodelist[i]; + if (id === node.getAttribute('id')) { + return node; + } + } + } + + /** + * @private + */ + function parseUseDirectives(doc) { + var nodelist = _getMultipleNodes(doc, ['use', 'svg:use']), i = 0; + + while (nodelist.length && i < nodelist.length) { + var el = nodelist[i], + xlink = el.getAttribute('xlink:href').substr(1), + x = el.getAttribute('x') || 0, + y = el.getAttribute('y') || 0, + el2 = elementById(doc, xlink).cloneNode(true), + currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', + parentNode, oldLength = nodelist.length, attr, j, attrs, l; + + applyViewboxTransform(el2); + if (/^svg$/i.test(el2.nodeName)) { + var el3 = el2.ownerDocument.createElement('g'); + for (j = 0, attrs = el2.attributes, l = attrs.length; j < l; j++) { + attr = attrs.item(j); + el3.setAttribute(attr.nodeName, attr.nodeValue); + } + // el2.firstChild != null + while (el2.firstChild) { + el3.appendChild(el2.firstChild); + } + el2 = el3; + } + + for (j = 0, attrs = el.attributes, l = attrs.length; j < l; j++) { + attr = attrs.item(j); + if (attr.nodeName === 'x' || attr.nodeName === 'y' || attr.nodeName === 'xlink:href') { + continue; + } + + if (attr.nodeName === 'transform') { + currentTrans = attr.nodeValue + ' ' + currentTrans; + } + else { + el2.setAttribute(attr.nodeName, attr.nodeValue); + } + } + + el2.setAttribute('transform', currentTrans); + el2.setAttribute('instantiated_by_use', '1'); + el2.removeAttribute('id'); + parentNode = el.parentNode; + parentNode.replaceChild(el2, el); + // some browsers do not shorten nodelist after replaceChild (IE8) + if (nodelist.length === oldLength) { + i++; + } + } + } + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // matches, e.g.: +14.56e-12, etc. + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*,?' + + '\\s*(' + fabric.reNum + '+)\\s*' + + '$' + ); + + /** + * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements + */ + function applyViewboxTransform(element) { + + var viewBoxAttr = element.getAttribute('viewBox'), + scaleX = 1, + scaleY = 1, + minX = 0, + minY = 0, + viewBoxWidth, viewBoxHeight, matrix, el, + widthAttr = element.getAttribute('width'), + heightAttr = element.getAttribute('height'), + x = element.getAttribute('x') || 0, + y = element.getAttribute('y') || 0, + preserveAspectRatio = element.getAttribute('preserveAspectRatio') || '', + missingViewBox = (!viewBoxAttr || !reViewBoxTagNames.test(element.nodeName) + || !(viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))), + missingDimAttr = (!widthAttr || !heightAttr || widthAttr === '100%' || heightAttr === '100%'), + toBeParsed = missingViewBox && missingDimAttr, + parsedDim = { }, translateMatrix = ''; + + parsedDim.width = 0; + parsedDim.height = 0; + parsedDim.toBeParsed = toBeParsed; + + if (toBeParsed) { + return parsedDim; + } + + if (missingViewBox) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + return parsedDim; + } + + minX = -parseFloat(viewBoxAttr[1]); + minY = -parseFloat(viewBoxAttr[2]); + viewBoxWidth = parseFloat(viewBoxAttr[3]); + viewBoxHeight = parseFloat(viewBoxAttr[4]); + + if (!missingDimAttr) { + parsedDim.width = parseUnit(widthAttr); + parsedDim.height = parseUnit(heightAttr); + scaleX = parsedDim.width / viewBoxWidth; + scaleY = parsedDim.height / viewBoxHeight; + } + else { + parsedDim.width = viewBoxWidth; + parsedDim.height = viewBoxHeight; + } + + // default is to preserve aspect ratio + preserveAspectRatio = fabric.util.parsePreserveAspectRatioAttribute(preserveAspectRatio); + if (preserveAspectRatio.alignX !== 'none') { + //translate all container for the effect of Mid, Min, Max + scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); + } + + if (scaleX === 1 && scaleY === 1 && minX === 0 && minY === 0 && x === 0 && y === 0) { + return parsedDim; + } + + if (x || y) { + translateMatrix = ' translate(' + parseUnit(x) + ' ' + parseUnit(y) + ') '; + } + + matrix = translateMatrix + ' matrix(' + scaleX + + ' 0' + + ' 0 ' + + scaleY + ' ' + + (minX * scaleX) + ' ' + + (minY * scaleY) + ') '; + + if (element.nodeName === 'svg') { + el = element.ownerDocument.createElement('g'); + // element.firstChild != null + while (element.firstChild) { + el.appendChild(element.firstChild); + } + element.appendChild(el); + } + else { + el = element; + matrix = el.getAttribute('transform') + matrix; + } + + el.setAttribute('transform', matrix); + return parsedDim; + } + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (element.nodeName && nodeName.test(element.nodeName.replace('svg:', '')) + && !element.getAttribute('instantiated_by_use')) { + return true; + } + } + return false; + } + + /** + * 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. + * @param {Object} [parsingOptions] options for parsing document + * @param {String} [parsingOptions.crossOrigin] crossOrigin settings + */ + fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) { + if (!doc) { + return; + } + + parseUseDirectives(doc); + + var svgUid = fabric.Object.__uid++, + options = applyViewboxTransform(doc), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + options.crossOrigin = parsingOptions && parsingOptions.crossOrigin; + options.svgUid = svgUid; + + if (descendants.length === 0 && fabric.isLikelyNode) { + // 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) { + applyViewboxTransform(el); + return reAllowedSVGTagNames.test(el.nodeName.replace('svg:', '')) && + !hasAncestorWithNodeName(el, reNotAllowedAncestors); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + + if (!elements || (elements && !elements.length)) { + callback && callback([], {}); + return; + } + + fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); + fabric.cssRules[svgUid] = fabric.getCSSRules(doc); + // Precedence of rules: style > class > attribute + fabric.parseElements(elements, function(instances) { + if (callback) { + callback(instances, options); + } + }, clone(options), reviver, parsingOptions); + }; + + var reFontDeclaration = new RegExp( + '(normal|italic)?\\s*(normal|small-caps)?\\s*' + + '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + + fabric.reNum + + '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)'); + + extend(fabric, { + /** + * Parses a short font declaration, building adding its properties to a style object + * @static + * @function + * @memberOf fabric + * @param {String} value font declaration + * @param {Object} oStyle definition + */ + parseFontDeclaration: function(value, oStyle) { + var match = value.match(reFontDeclaration); + + if (!match) { + return; + } + var fontStyle = match[1], + // font variant is not used + // fontVariant = match[2], + fontWeight = match[3], + fontSize = match[4], + lineHeight = match[5], + fontFamily = match[6]; + + if (fontStyle) { + oStyle.fontStyle = fontStyle; + } + if (fontWeight) { + oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); + } + if (fontSize) { + oStyle.fontSize = parseUnit(fontSize); + } + if (fontFamily) { + oStyle.fontFamily = fontFamily; + } + if (lineHeight) { + oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; + } + }, + + /** + * 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 + */ + getGradientDefs: function(doc) { + var tagArray = [ + 'linearGradient', + 'radialGradient', + 'svg:linearGradient', + 'svg:radialGradient'], + elList = _getMultipleNodes(doc, tagArray), + el, j = 0, id, xlink, + gradientDefs = { }, idsToXlinkMap = { }; + + j = elList.length; + + while (j--) { + el = elList[j]; + xlink = el.getAttribute('xlink:href'); + id = el.getAttribute('id'); + if (xlink) { + idsToXlinkMap[id] = xlink.substr(1); + } + gradientDefs[id] = el; + } + + for (id in idsToXlinkMap) { + var el2 = gradientDefs[idsToXlinkMap[id]].cloneNode(true); + el = gradientDefs[id]; + while (el2.firstChild) { + el.appendChild(el2.firstChild); + } + } + return gradientDefs; + }, + + /** + * 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 + */ + parseAttributes: function(element, attributes, svgUid) { + + if (!element) { + return; + } + + var value, + parentAttributes = { }, + fontSize; + + if (typeof svgUid === 'undefined') { + svgUid = element.getAttribute('svgUid'); + } + // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards + if (element.parentNode && reAllowedParents.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); + } + fontSize = (parentAttributes && parentAttributes.fontSize ) || + element.getAttribute('font-size') || fabric.Text.DEFAULT_SVG_FONT_SIZE; + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + if (value) { // eslint-disable-line + 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, svgUid), fabric.parseStyleAttribute(element))); + + var normalizedAttr, normalizedValue, normalizedStyle = {}; + for (var attr in ownAttributes) { + normalizedAttr = normalizeAttr(attr); + normalizedValue = normalizeValue(normalizedAttr, ownAttributes[attr], parentAttributes, fontSize); + normalizedStyle[normalizedAttr] = normalizedValue; + } + if (normalizedStyle && normalizedStyle.font) { + fabric.parseFontDeclaration(normalizedStyle.font, normalizedStyle); + } + var mergedAttrs = extend(parentAttributes, normalizedStyle); + return reAllowedParents.test(element.nodeName) ? mergedAttrs : _setStrokeFillOpacity(mergedAttrs); + }, + + /** + * 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. + */ + parseElements: function(elements, callback, options, reviver, parsingOptions) { + new fabric.ElementsParser(elements, callback, options, reviver, parsingOptions).parse(); + }, + + /** + * 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 + */ + parseStyleAttribute: function(element) { + var oStyle = { }, + style = element.getAttribute('style'); + + if (!style) { + return oStyle; + } + + if (typeof style === 'string') { + parseStyleString(style, oStyle); + } + else { + parseStyleObject(style, oStyle); + } + + return oStyle; + }, + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @param {String} points points attribute string + * @return {Array} array of points + */ + parsePointsAttribute: function(points) { + + // points attribute is required and must not be empty + if (!points) { + return null; + } + + // replace commas with whitespace and remove bookending whitespace + points = points.replace(/,/g, ' ').trim(); + + points = points.split(/\s+/); + var parsedPoints = [], i, len; + + 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; + }, + + /** + * 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 + */ + getCSSRules: function(doc) { + var styles = doc.getElementsByTagName('style'), + allRules = { }, rules; + + // very crude parsing of style contents + for (var i = 0, len = styles.length; i < len; i++) { + // IE9 doesn't support textContent, but provides text instead. + var styleContents = styles[i].textContent || styles[i].text; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + if (styleContents.trim() === '') { + continue; + } + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + rules.forEach(function(rule) { + + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), + ruleObj = { }, declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + ruleObj[property] = value; + } + rule = match[1]; + rule.split(',').forEach(function(_rule) { + _rule = _rule.replace(/^svg/i, '').trim(); + if (_rule === '') { + return; + } + if (allRules[_rule]) { + fabric.util.object.extend(allRules[_rule], ruleObj); + } + else { + allRules[_rule] = fabric.util.object.clone(ruleObj); + } + }); + }); + } + return allRules; + }, + + /** + * 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. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromURL: function(url, callback, reviver, options) { + + url = url.replace(/^\n\s*/, '').trim(); + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (xml && !xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(//i, '')); + } + if (!xml || !xml.documentElement) { + callback && callback(null); + } + + fabric.parseSVGDocument(xml.documentElement, function (results, _options) { + callback && callback(results, _options); + }, reviver, 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. + * @param {Object} [options] Object containing options for parsing + * @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources + */ + loadSVGFromString: function(string, callback, reviver, options) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + // IE chokes on DOCTYPE + doc.loadXML(string.replace(//i, '')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, _options) { + callback(results, _options); + }, reviver, options); + } + }); + +})(typeof exports !== 'undefined' ? exports : this); + + +fabric.ElementsParser = function(elements, callback, options, reviver, parsingOptions) { + this.elements = elements; + this.callback = callback; + this.options = options; + this.reviver = reviver; + this.svgUid = (options && options.svgUid) || 0; + this.parsingOptions = parsingOptions; +}; + +fabric.ElementsParser.prototype.parse = function() { + this.instances = new Array(this.elements.length); + this.numElements = this.elements.length; + + this.createObjects(); +}; + +fabric.ElementsParser.prototype.createObjects = function() { + for (var i = 0, len = this.elements.length; i < len; i++) { + this.elements[i].setAttribute('svgUid', this.svgUid); + (function(_obj, i) { + setTimeout(function() { + _obj.createObject(_obj.elements[i], i); + }, 0); + })(this, i); + } +}; + +fabric.ElementsParser.prototype.createObject = function(el, index) { + var klass = fabric[fabric.util.string.capitalize(el.tagName.replace('svg:', ''))]; + if (klass && klass.fromElement) { + try { + this._createObject(klass, el, index); + } + catch (err) { + fabric.log(err); + } + } + else { + this.checkIfDone(); + } +}; + +fabric.ElementsParser.prototype._createObject = function(klass, el, index) { + if (klass.async) { + klass.fromElement(el, this.createCallback(index, el), this.options); + } + else { + var obj = klass.fromElement(el, this.options); + this.resolveGradient(obj, 'fill'); + this.resolveGradient(obj, 'stroke'); + this.reviver && this.reviver(el, obj); + this.instances[index] = obj; + this.checkIfDone(); + } +}; + +fabric.ElementsParser.prototype.createCallback = function(index, el) { + var _this = this; + return function(obj) { + _this.resolveGradient(obj, 'fill'); + _this.resolveGradient(obj, 'stroke'); + _this.reviver && _this.reviver(el, obj); + _this.instances[index] = obj; + _this.checkIfDone(); + }; +}; + +fabric.ElementsParser.prototype.resolveGradient = function(obj, property) { + + var instanceFillValue = obj.get(property); + if (!(/^url\(/).test(instanceFillValue)) { + return; + } + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); + if (fabric.gradientDefs[this.svgUid][gradientId]) { + obj.set(property, + fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][gradientId], obj)); + } +}; + +fabric.ElementsParser.prototype.checkIfDone = function() { + if (--this.numElements === 0) { + this.instances = this.instances.filter(function(el) { + // eslint-disable-next-line no-eq-null, eqeqeq + return el != null; + }); + this.callback(this.instances); + } +}; + + +(function(global) { + + 'use strict'; + + /* 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 */ { + + type: 'point', + + 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 + * @chainable + */ + 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 + * @return {fabric.Point} thisArg + * @chainable + */ + 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 + * @chainable + */ + 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 + * @chainable + */ + scalarSubtractEquals: function (scalar) { + this.x -= scalar; + this.y -= scalar; + return this; + }, + + /** + * Miltiplies this point by a value and returns a new one + * TODO: rename in scalarMultiply in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + multiply: function (scalar) { + return new Point(this.x * scalar, this.y * scalar); + }, + + /** + * Miltiplies this point by a value + * TODO: rename in scalarMultiplyEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + multiplyEquals: function (scalar) { + this.x *= scalar; + this.y *= scalar; + return this; + }, + + /** + * Divides this point by a value and returns a new one + * TODO: rename in scalarDivide in 2.0 + * @param {Number} scalar + * @return {fabric.Point} + */ + divide: function (scalar) { + return new Point(this.x / scalar, this.y / scalar); + }, + + /** + * Divides this point by a value + * TODO: rename in scalarDivideEquals in 2.0 + * @param {Number} scalar + * @return {fabric.Point} thisArg + * @chainable + */ + 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 , position of interpolation, between 0 and 1 default 0.5 + * @return {fabric.Point} + */ + lerp: function (that, t) { + if (typeof t === 'undefined') { + t = 0.5; + } + t = Math.max(Math.min(1, t), 0); + 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 this.lerp(that); + }, + + /** + * 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 + * @param {Number} y + * @chainable + */ + setXY: function (x, y) { + this.x = x; + this.y = y; + return this; + }, + + /** + * Sets x of this point + * @param {Number} x + * @chainable + */ + setX: function (x) { + this.x = x; + return this; + }, + + /** + * Sets y of this point + * @param {Number} y + * @chainable + */ + setY: function (y) { + this.y = y; + return this; + }, + + /** + * Sets x/y of this point from another point + * @param {fabric.Point} that + * @chainable + */ + setFromPoint: function (that) { + this.x = that.x; + this.y = that.y; + return this; + }, + + /** + * 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; + }, + + /** + * return a cloned instance of the point + * @return {fabric.Point} + */ + clone: function () { + return new Point(this.x, this.y); + } + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + /* 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 */ { + + constructor: Intersection, + + /** + * Appends a point to intersection + * @param {fabric.Point} point + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoint: function (point) { + this.points.push(point); + return this; + }, + + /** + * Appends points to intersection + * @param {Array} points + * @return {fabric.Intersection} thisArg + * @chainable + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + return this; + } + }; + + /** + * Checks if one line intersects another + * TODO: rename in intersectSegmentSegment + * @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, + uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (uB !== 0) { + var ua = uaT / uB, + ub = ubT / uB; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection('Intersection'); + result.appendPoint(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection(); + } + } + else { + if (uaT === 0 || ubT === 0) { + result = new Intersection('Coincident'); + } + else { + result = new Intersection('Parallel'); + } + } + return result; + }; + + /** + * Checks if line intersects polygon + * TODO: rename in intersectSegmentPolygon + * fix detection of coincident + * @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, + b1, b2, inter; + + for (var i = 0; i < length; i++) { + 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 {fabric.Point} r1 + * @param {fabric.Point} 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) { + + 'use strict'; + + 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) or hsl format or from known color list + * @return {fabric.Color} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} + */ + 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]; + } + + if (color === 'transparent') { + source = [255, 255, 255, 0]; + } + + if (!source) { + source = Color.sourceFromHex(color); + } + if (!source) { + source = Color.sourceFromRgb(color); + } + if (!source) { + source = Color.sourceFromHsl(color); + } + if (!source) { + //if color is not recognize let's make black as canvas does + source = [0, 0, 0, 1]; + } + 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 representation 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 representation 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 representation 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 representation 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 representation in HEX format + * @return {String} ex: FF5555 + */ + toHex: function() { + var source = this.getSource(), r, g, b; + + r = source[0].toString(16); + r = (r.length === 1) ? ('0' + r) : r; + + g = source[1].toString(16); + g = (g.length === 1) ? ('0' + g) : g; + + b = source[2].toString(16); + b = (b.length === 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + }, + + /** + * Returns color representation in HEXA format + * @return {String} ex: FF5555CC + */ + toHexa: function() { + var source = this.getSource(), a; + + a = source[3] * 255; + a = a.toString(16); + a = (a.length === 1) ? ('0' + a) : a; + + return this.toHex() + a.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 + */ + // eslint-disable-next-line max-len + fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/; + + /** + * 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: #FF5544CC, #FF5555, 010155, aff) + * @static + * @field + * @memberOf fabric.Color + */ + fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; + + /** + * Map of the 17 basic color names with HEX code + * @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', + grey: '#808080', + green: '#008000', + lime: '#00FF00', + maroon: '#800000', + navy: '#000080', + olive: '#808000', + orange: '#FFA500', + purple: '#800080', + red: '#FF0000', + silver: '#C0C0C0', + teal: '#008080', + white: '#FFFFFF', + yellow: '#FFFF00' + }; + + /** + * @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 representation (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 representation (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, + 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 representation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @memberOf fabric.Color + * @param {String} color ex: FF5555 or FF5544CC (RGBa) + * @return {Array} source + */ + fabric.Color.sourceFromHex = function(color) { + if (color.match(Color.reHex)) { + var value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3 || value.length === 4), + isRGBa = (value.length === 8 || value.length === 4), + 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), + a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; + + return [ + parseInt(r, 16), + parseInt(g, 16), + parseInt(b, 16), + parseFloat((parseInt(a, 16) / 255).toFixed(2)) + ]; + } + }; + + /** + * 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') || 0, + color, colorAlpha, opacity; + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; + 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'); + } + + color = new fabric.Color(color); + colorAlpha = color.getAlpha(); + opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); + opacity *= colorAlpha; + + return { + offset: offset, + color: color.toRgb(), + opacity: opacity + }; + } + + function getLinearCoords(el) { + return { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + } + + function getRadialCoords(el) { + return { + 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%' + }; + } + /* _FROM_SVG_END_ */ + + var clone = fabric.util.object.clone; + + /** + * Gradient class + * @class fabric.Gradient + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} + * @see {@link fabric.Gradient#initialize} for constructor definition + */ + fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { + + /** + * Horizontal offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetX: 0, + + /** + * Vertical offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetY: 0, + + /** + * 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.colorStops = options.colorStops.slice(); + if (options.gradientTransform) { + this.gradientTransform = options.gradientTransform; + } + this.offsetX = options.offsetX || this.offsetX; + this.offsetY = options.offsetY || this.offsetY; + }, + + /** + * Adds another colorStop + * @param {Object} colorStop Object with offset and color + * @return {fabric.Gradient} thisArg + */ + addColorStop: function(colorStops) { + for (var position in colorStops) { + var color = new fabric.Color(colorStops[position]); + this.colorStops.push({ + offset: parseFloat(position), + color: color.toRgb(), + opacity: color.getAlpha() + }); + } + return this; + }, + + /** + * Returns object representation of a gradient + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} + */ + toObject: function(propertiesToInclude) { + var object = { + type: this.type, + coords: this.coords, + colorStops: this.colorStops, + offsetX: this.offsetX, + offsetY: this.offsetY, + gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an gradient + * @param {Object} object Object to create a gradient for + * @return {String} SVG representation of an gradient (linear/radial) + */ + toSVG: function(object) { + var coords = clone(this.coords, true), + markup, commonAttributes, colorStops = clone(this.colorStops, true), + needsSwap = coords.r1 > coords.r2; + // colorStops must be sorted ascending + colorStops.sort(function(a, b) { + return a.offset - b.offset; + }); + + if (!(object.group && object.group.type === 'path-group')) { + for (var prop in coords) { + if (prop === 'x1' || prop === 'x2') { + coords[prop] += this.offsetX - object.width / 2; + } + else if (prop === 'y1' || prop === 'y2') { + coords[prop] += this.offsetY - object.height / 2; + } + } + } + + commonAttributes = 'id="SVGID_' + this.id + + '" gradientUnits="userSpaceOnUse"'; + if (this.gradientTransform) { + commonAttributes += ' gradientTransform="matrix(' + this.gradientTransform.join(' ') + ')" '; + } + if (this.type === 'linear') { + markup = [ + '\n' + ]; + } + else if (this.type === 'radial') { + // svg radial gradient has just 1 radius. the biggest. + markup = [ + '\n' + ]; + } + + if (this.type === 'radial') { + if (needsSwap) { + // svg goes from internal to external radius. if radius are inverted, swap color stops. + colorStops = colorStops.concat(); + colorStops.reverse(); + for (var i = 0; i < colorStops.length; i++) { + colorStops[i].offset = 1 - colorStops[i].offset; + } + } + var minRadius = Math.min(coords.r1, coords.r2); + if (minRadius > 0) { + // i have to shift all colorStops and add new one in 0. + var maxRadius = Math.max(coords.r1, coords.r2), + percentageShift = minRadius / maxRadius; + for (var i = 0; i < colorStops.length; i++) { + colorStops[i].offset += percentageShift * (1 - colorStops[i].offset); + } + } + } + + for (var i = 0; i < colorStops.length; i++) { + var colorStop = colorStops[i]; + markup.push( + '\n' + ); + } + + markup.push((this.type === 'linear' ? '\n' : '\n')); + + return markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns an instance of CanvasGradient + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} object + * @return {CanvasGradient} + */ + toLive: function(ctx, object) { + var gradient, prop, coords = fabric.util.object.clone(this.coords); + + if (!this.type) { + return; + } + + if (object.group && object.group.type === 'path-group') { + for (prop in coords) { + if (prop === 'x1' || prop === 'x2') { + coords[prop] += -this.offsetX + object.width / 2; + } + else if (prop === 'y1' || prop === 'y2') { + coords[prop] += -this.offsetY + object.height / 2; + } + } + } + + if (this.type === 'linear') { + gradient = ctx.createLinearGradient( + coords.x1, coords.y1, coords.x2, coords.y2); + } + else if (this.type === 'radial') { + gradient = ctx.createRadialGradient( + coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, 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(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, + gradientUnits = el.getAttribute('gradientUnits') || 'objectBoundingBox', + gradientTransform = el.getAttribute('gradientTransform'), + colorStops = [], + coords, ellipseMatrix; + + if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { + type = 'linear'; + } + else { + type = 'radial'; + } + + if (type === 'linear') { + coords = getLinearCoords(el); + } + else if (type === 'radial') { + coords = getRadialCoords(el); + } + + for (var i = colorStopEls.length; i--; ) { + colorStops.push(getColorStop(colorStopEls[i])); + } + + ellipseMatrix = _convertPercentUnitsToValues(instance, coords, gradientUnits); + + var gradient = new fabric.Gradient({ + type: type, + coords: coords, + colorStops: colorStops, + offsetX: -instance.left, + offsetY: -instance.top + }); + + if (gradientTransform || ellipseMatrix !== '') { + gradient.gradientTransform = fabric.parseTransformAttribute((gradientTransform || '') + ellipseMatrix); + } + return gradient; + }, + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Gradient} instance from its object representation + * @static + * @memberOf fabric.Gradient + * @param {Object} obj + * @param {Object} [options] Options object + */ + forObject: function(obj, options) { + options || (options = { }); + _convertPercentUnitsToValues(obj, options.coords, 'userSpaceOnUse'); + return new fabric.Gradient(options); + } + }); + + /** + * @private + */ + function _convertPercentUnitsToValues(object, options, gradientUnits) { + var propValue, addFactor = 0, multFactor = 1, ellipseMatrix = ''; + for (var prop in options) { + if (options[prop] === 'Infinity') { + options[prop] = 1; + } + else if (options[prop] === '-Infinity') { + options[prop] = 0; + } + propValue = parseFloat(options[prop], 10); + if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { + multFactor = 0.01; + } + else { + multFactor = 1; + } + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + multFactor *= gradientUnits === 'objectBoundingBox' ? object.width : 1; + addFactor = gradientUnits === 'objectBoundingBox' ? object.left || 0 : 0; + } + else if (prop === 'y1' || prop === 'y2') { + multFactor *= gradientUnits === 'objectBoundingBox' ? object.height : 1; + addFactor = gradientUnits === 'objectBoundingBox' ? object.top || 0 : 0; + } + options[prop] = propValue * multFactor + addFactor; + } + if (object.type === 'ellipse' && + options.r2 !== null && + gradientUnits === 'objectBoundingBox' && + object.rx !== object.ry) { + + var scaleFactor = object.ry / object.rx; + ellipseMatrix = ' scale(1, ' + scaleFactor + ')'; + if (options.y1) { + options.y1 /= scaleFactor; + } + if (options.y2) { + options.y2 /= scaleFactor; + } + } + return ellipseMatrix; + } +})(); + + +(function() { + + 'use strict'; + + var toFixed = fabric.util.toFixed; + + /** + * Pattern class + * @class fabric.Pattern + * @see {@link http://fabricjs.com/patterns|Pattern demo} + * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} + * @see {@link fabric.Pattern#initialize} for constructor definition + */ + + + 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 + * @param {Function} [callback] function to invoke after callback init. + * @return {fabric.Pattern} thisArg + */ + initialize: function(options, callback) { + options || (options = { }); + + this.id = fabric.Object.__uid++; + this.setOptions(options); + if (!options.source || (options.source && typeof options.source !== 'string')) { + callback && callback(this); + return; + } + // function string + if (typeof fabric.util.getFunctionBody(options.source) !== 'undefined') { + this.source = new Function(fabric.util.getFunctionBody(options.source)); + callback && callback(this); + } + else { + // img src string + var _this = this; + this.source = fabric.util.createImage(); + fabric.util.loadImage(options.source, function(img) { + _this.source = img; + callback && callback(_this); + }); + } + }, + + /** + * Returns object representation of a pattern + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of a pattern instance + */ + toObject: function(propertiesToInclude) { + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + source, object; + + // callback + if (typeof this.source === 'function') { + source = String(this.source); + } + // element + else if (typeof this.source.src === 'string') { + source = this.source.src; + } + // element + else if (typeof this.source === 'object' && this.source.toDataURL) { + source = this.source.toDataURL(); + } + + object = { + type: 'pattern', + source: source, + repeat: this.repeat, + offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), + }; + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a pattern + * @param {fabric.Object} object + * @return {String} SVG representation of a pattern + */ + toSVG: function(object) { + var patternSource = typeof this.source === 'function' ? this.source() : this.source, + patternWidth = patternSource.width / object.width, + patternHeight = patternSource.height / object.height, + patternOffsetX = this.offsetX / object.width, + patternOffsetY = this.offsetY / object.height, + patternImgSrc = ''; + if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { + patternHeight = 1; + } + if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { + patternWidth = 1; + } + if (patternSource.src) { + patternImgSrc = patternSource.src; + } + else if (patternSource.toDataURL) { + patternImgSrc = patternSource.toDataURL(); + } + + return '\n' + + '\n' + + '\n'; + }, + /* _TO_SVG_END_ */ + + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * 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; + + // if the image failed to load, return, and allow rest to continue loading + if (!source) { + return ''; + } + + // if an image + if (typeof source.src !== 'undefined') { + if (!source.complete) { + return ''; + } + if (source.naturalWidth === 0 || source.naturalHeight === 0) { + return ''; + } + } + return ctx.createPattern(source, this.repeat); + } + }); +})(); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Shadow) { + fabric.warn('fabric.Shadow is already defined.'); + return; + } + + /** + * Shadow class + * @class fabric.Shadow + * @see {@link http://fabricjs.com/shadows|Shadow demo} + * @see {@link fabric.Shadow#initialize} for constructor definition + */ + 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, + + /** + * Indicates whether toObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * 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(), + 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 '); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of a shadow + * @param {fabric.Object} object + * @return {String} SVG representation of a shadow + */ + toSVG: function(object) { + var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + offset = fabric.util.rotateVector( + { x: this.offsetX, y: this.offsetY }, + fabric.util.degreesToRadians(-object.angle)), + BLUR_BOX = 20; + + if (object.width && object.height) { + //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion + // we add some extra space to filter box to contain the blur ( 20 ) + fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX; + } + if (object.flipX) { + offset.x *= -1; + } + if (object.flipY) { + offset.y *= -1; + } + return ( + '\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\n' + + '\n'); + }, + /* _TO_SVG_END_ */ + + /** + * Returns object representation of a shadow + * @return {Object} Object representation of a shadow instance + */ + toObject: function() { + if (this.includeDefaultValues) { + return { + color: this.color, + blur: this.blur, + offsetX: this.offsetX, + offsetY: this.offsetY, + affectStroke: this.affectStroke + }; + } + var obj = { }, proto = fabric.Shadow.prototype; + + ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke'].forEach(function(prop) { + if (this[prop] !== proto[prop]) { + obj[prop] = this[prop]; + } + }, this); + + return obj; + } + }); + + /** + * 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 + */ + // eslint-disable-next-line max-len + fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function () { + + 'use strict'; + + 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, + toFixed = fabric.util.toFixed, + transformPoint = fabric.util.transformPoint, + invertTransform = fabric.util.invertTransform, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * Static canvas class + * @class fabric.StaticCanvas + * @mixes fabric.Collection + * @mixes fabric.Observable + * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} + * @see {@link fabric.StaticCanvas#initialize} for constructor definition + * @fires before:render + * @fires after:render + * @fires canvas:cleared + * @fires object:added + * @fires object:removed + */ + fabric.StaticCanvas = fabric.util.createClass(fabric.CommonMethods, /** @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); + }, + + /** + * Background color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. + * @type {(String|fabric.Pattern)} + * @default + */ + backgroundColor: '', + + /** + * Background image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setBackgroundImage}. + * Backwards incompatibility note: The "backgroundImageOpacity" + * and "backgroundImageStretch" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#opacity}, {@link fabric.Image#width} and {@link fabric.Image#height}. + * @type fabric.Image + * @default + */ + backgroundImage: null, + + /** + * Overlay color of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayColor} + * @since 1.3.9 + * @type {(String|fabric.Pattern)} + * @default + */ + overlayColor: '', + + /** + * Overlay image of canvas instance. + * Should be set via {@link fabric.StaticCanvas#setOverlayImage}. + * Backwards incompatibility note: The "overlayImageLeft" + * and "overlayImageTop" properties are deprecated since 1.3.9. + * Use {@link fabric.Image#left} and {@link fabric.Image#top}. + * @type fabric.Image + * @default + */ + overlayImage: null, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * @type Boolean + * @default + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @type Boolean + * @default + */ + stateful: false, + + /** + * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove} should also re-render canvas. + * Disabling this option could give a great performance boost when adding/removing a lot of objects to/from canvas at once + * (followed by a manual rendering after addition/deletion) + * @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, + + /** + * Indicates whether this canvas will use image smoothing, this is on by default in browsers + * @type Boolean + * @default + */ + imageSmoothingEnabled: true, + + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: fabric.iMatrix.concat(), + + /** + * if set to false background image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + backgroundVpt: true, + + /** + * if set to false overlya image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @default + */ + overlayVpt: true, + + /** + * Callback; invoked right before object is about to be scaled/rotated + */ + onBeforeScaleRotate: function () { + /* NOOP */ + }, + + /** + * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens + */ + enableRetinaScaling: true, + + /** + * Describe canvas element extension over design + * properties are tl,tr,bl,br. + * if canvas is not zoomed/panned those points are the four corner of canvas + * if canvas is viewportTransformed you those points indicate the extension + * of canvas element in plain untrasformed coordinates + * The coordinates get updated with @method calcViewportBoundaries. + * @memberOf fabric.StaticCanvas.prototype + */ + vptCoords: { }, + + /** + * Based on vptCoords and object.aCoords, skip rendering of objects that + * are not included in current viewport. + * May greatly help in applications with crowded canvas and use of zoom/pan + * If One of the corner of the bounding box of the object is on the canvas + * the objects get rendered. + * @memberOf fabric.StaticCanvas.prototype + */ + skipOffscreen: false, + + /** + * @private + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + _initStatic: function(el, options) { + var cb = fabric.StaticCanvas.prototype.renderAll.bind(this); + this._objects = []; + this._createLowerCanvas(el); + this._initOptions(options); + this._setImageSmoothing(); + // only initialize retina scaling once + if (!this.interactive) { + this._initRetinaScaling(); + } + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, cb); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, cb); + } + if (options.backgroundColor) { + this.setBackgroundColor(options.backgroundColor, cb); + } + if (options.overlayColor) { + this.setOverlayColor(options.overlayColor, cb); + } + this.calcOffset(); + }, + + /** + * @private + */ + _isRetinaScaling: function() { + return (fabric.devicePixelRatio !== 1 && this.enableRetinaScaling); + }, + + /** + * @private + * @return {Number} retinaScaling if applied, otherwise 1; + */ + getRetinaScaling: function() { + return this._isRetinaScaling() ? fabric.devicePixelRatio : 1; + }, + + /** + * @private + */ + _initRetinaScaling: function() { + if (!this._isRetinaScaling()) { + return; + } + this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio); + this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio); + + this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio); + }, + + /** + * 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 {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or 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 {@link fabric.Image|overlay image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} + * @example Normal overlayImage with left/top = 0 + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage with different properties + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched overlayImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched overlayImage #2 - width/height correspond to canvas width/height + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position overlayImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example overlayImage loaded from cross-origin + * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setOverlayImage: function (image, callback, options) { + return this.__setBgOverlayImage('overlayImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas + * @param {(fabric.Image|String)} image fabric.Image instance or 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 {@link fabric.Image|background image}. + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/YH9yD/|jsFiddle demo} + * @example Normal backgroundImage with left/top = 0 + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage with different properties + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top' + * }); + * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height + * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img) { + * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); + * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); + * }); + * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * width: canvas.width, + * height: canvas.height, + * // Needed to position backgroundImage at 0/0 + * originX: 'left', + * originY: 'top' + * }); + * @example backgroundImage loaded from cross-origin + * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { + * opacity: 0.5, + * angle: 45, + * left: 400, + * top: 400, + * originX: 'left', + * originY: 'top', + * crossOrigin: 'anonymous' + * }); + */ + setBackgroundImage: function (image, callback, options) { + return this.__setBgOverlayImage('backgroundImage', image, callback, options); + }, + + /** + * Sets {@link fabric.StaticCanvas#overlayColor|background color} for this canvas + * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} + * @example Normal overlayColor - color value + * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as overlayColor with repeat and offset + * canvas.setOverlayColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setOverlayColor: function(overlayColor, callback) { + return this.__setBgOverlayColor('overlayColor', overlayColor, callback); + }, + + /** + * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas + * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to + * @param {Function} callback Callback to invoke when background color is set + * @return {fabric.Canvas} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} + * @example Normal backgroundColor - color value + * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png' + * }, canvas.renderAll.bind(canvas)); + * @example fabric.Pattern used as backgroundColor with repeat and offset + * canvas.setBackgroundColor({ + * source: 'http://fabricjs.com/assets/escheresque_ste.png', + * repeat: 'repeat', + * offsetX: 200, + * offsetY: 100 + * }, canvas.renderAll.bind(canvas)); + */ + setBackgroundColor: function(backgroundColor, callback) { + return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); + }, + + /** + * @private + * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-imagesmoothingenabled|WhatWG Canvas Standard} + */ + _setImageSmoothing: function() { + var ctx = this.getContext(); + + ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled || ctx.webkitImageSmoothingEnabled + || ctx.mozImageSmoothingEnabled || ctx.msImageSmoothingEnabled || ctx.oImageSmoothingEnabled; + ctx.imageSmoothingEnabled = this.imageSmoothingEnabled; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} + * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) + * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to + * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay + * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. + */ + __setBgOverlayImage: function(property, image, callback, options) { + if (typeof image === 'string') { + fabric.util.loadImage(image, function(img) { + img && (this[property] = new fabric.Image(img, options)); + callback && callback(img); + }, this, options && options.crossOrigin); + } + else { + options && image.setOptions(options); + this[property] = image; + callback && callback(image); + } + + return this; + }, + + /** + * @private + * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} + * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) + * @param {(Object|String|null)} color Object with pattern information, color value or null + * @param {Function} [callback] Callback is invoked when color is set + */ + __setBgOverlayColor: function(property, color, callback) { + this[property] = color; + this._initGradient(color, property); + this._initPattern(color, property, callback); + return this; + }, + + /** + * @private + */ + _createCanvasElement: function(canvasEl) { + var element = fabric.util.createCanvasElement(canvasEl); + if (!element.style) { + element.style = { }; + } + if (!element) { + throw CANVAS_INIT_ERROR; + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + return element; + }, + + /** + * @private + * @param {Object} [options] Options object + */ + _initOptions: function (options) { + this._setOptions(options); + + this.width = this.width || parseInt(this.lowerCanvasEl.width, 10) || 0; + this.height = this.height || parseInt(this.lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) { + return; + } + + this.lowerCanvasEl.width = this.width; + this.lowerCanvasEl.height = this.height; + + this.lowerCanvasEl.style.width = this.width + 'px'; + this.lowerCanvasEl.style.height = this.height + 'px'; + + this.viewportTransform = this.viewportTransform.slice(); + }, + + /** + * Creates a bottom canvas + * @private + * @param {HTMLElement} [canvasEl] + */ + _createLowerCanvas: function (canvasEl) { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(canvasEl); + + 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|String} value Value to set width to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value, options) { + return this.setDimensions({ width: value }, options); + }, + + /** + * Sets height of this canvas instance + * @param {Number|String} value Value to set height to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value, options) { + return this.setDimensions({ height: value }, options); + }, + + /** + * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) + * @param {Object} dimensions Object with width/height properties + * @param {Number|String} [dimensions.width] Width of canvas element + * @param {Number|String} [dimensions.height] Height of canvas element + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function (dimensions, options) { + var cssValue; + + options = options || {}; + + for (var prop in dimensions) { + cssValue = dimensions[prop]; + + if (!options.cssOnly) { + this._setBackstoreDimension(prop, dimensions[prop]); + cssValue += 'px'; + } + + if (!options.backstoreOnly) { + this._setCssDimension(prop, cssValue); + } + } + this._initRetinaScaling(); + this._setImageSmoothing(); + this.calcOffset(); + + if (!options.cssOnly) { + this.renderAll(); + } + + 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 + */ + _setBackstoreDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + } + + if (this.cacheCanvasEl) { + this.cacheCanvasEl[prop] = value; + } + + this[prop] = value; + + return this; + }, + + /** + * Helper for setting css width/height + * @private + * @param {String} prop property (width|height) + * @param {String} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setCssDimension: function (prop, value) { + this.lowerCanvasEl.style[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl.style[prop] = value; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value; + } + + return this; + }, + + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return this.viewportTransform[0]; + }, + + /** + * Sets viewport transform of this canvas instance + * @param {Array} vpt the transform in the form of context.transform + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportTransform: function (vpt) { + var activeGroup = this._activeGroup, object, ingoreVpt = false, skipAbsolute = true; + this.viewportTransform = vpt; + for (var i = 0, len = this._objects.length; i < len; i++) { + object = this._objects[i]; + object.group || object.setCoords(ingoreVpt, skipAbsolute); + } + if (activeGroup) { + activeGroup.setCoords(ingoreVpt, skipAbsolute); + } + this.calcViewportBoundaries(); + this.renderAll(); + return this; + }, + + /** + * Sets zoom level of this canvas instance, zoom centered around point + * @param {fabric.Point} point to zoom with respect to + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + zoomToPoint: function (point, value) { + // TODO: just change the scale, preserve other transformations + var before = point, vpt = this.viewportTransform.slice(0); + point = transformPoint(point, invertTransform(this.viewportTransform)); + vpt[0] = value; + vpt[3] = value; + var after = transformPoint(point, vpt); + vpt[4] += before.x - after.x; + vpt[5] += before.y - after.y; + return this.setViewportTransform(vpt); + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + this.zoomToPoint(new fabric.Point(0, 0), value); + return this; + }, + + /** + * Pan viewport so as to place point at top left corner of canvas + * @param {fabric.Point} point to move to + * @return {fabric.Canvas} instance + * @chainable true + */ + absolutePan: function (point) { + var vpt = this.viewportTransform.slice(0); + vpt[4] = -point.x; + vpt[5] = -point.y; + return this.setViewportTransform(vpt); + }, + + /** + * Pans viewpoint relatively + * @param {fabric.Point} point (position vector) to move by + * @return {fabric.Canvas} instance + * @chainable true + */ + relativePan: function (point) { + return this.absolutePan(new fabric.Point( + -point.x - this.viewportTransform[4], + -point.y - this.viewportTransform[5] + )); + }, + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was added + */ + _onObjectAdded: function(obj) { + this.stateful && obj.setupState(); + obj._set('canvas', this); + obj.setCoords(); + 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'); + delete obj.canvas; + }, + + /** + * 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; + this.backgroundImage = null; + this.overlayImage = null; + this.backgroundColor = ''; + this.overlayColor = ''; + if (this._hasITextHandlers) { + this.off('mouse:up', this._mouseUpITextHandler); + this._iTextInstances = null; + this._hasITextHandlers = false; + } + this.clearContext(this.contextContainer); + this.fire('canvas:cleared'); + this.renderAll(); + return this; + }, + + /** + * Renders the canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._objects); + return this; + }, + + /** + * Calculate the position of the 4 corner of canvas with current viewportTransform. + * helps to determinate when an object is in the current rendering viewport using + * object absolute coordinates ( aCoords ) + * @return {Object} points.tl + * @chainable + */ + calcViewportBoundaries: function() { + var points = { }, width = this.getWidth(), height = this.getHeight(), + iVpt = invertTransform(this.viewportTransform); + points.tl = transformPoint({ x: 0, y: 0 }, iVpt); + points.br = transformPoint({ x: width, y: height }, iVpt); + points.tr = new fabric.Point(points.br.x, points.tl.y); + points.bl = new fabric.Point(points.tl.x, points.br.y); + this.vptCoords = points; + return points; + }, + + /** + * Renders background, objects, overlay and controls. + * @param {CanvasRenderingContext2D} ctx + * @param {Array} objects to render + * @return {fabric.Canvas} instance + * @chainable + */ + renderCanvas: function(ctx, objects) { + this.calcViewportBoundaries(); + this.clearContext(ctx); + this.fire('before:render'); + if (this.clipTo) { + fabric.util.clipContext(this, ctx); + } + this._renderBackground(ctx); + + ctx.save(); + //apply viewport transform once for all rendering process + ctx.transform.apply(ctx, this.viewportTransform); + this._renderObjects(ctx, objects); + ctx.restore(); + if (!this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + if (this.clipTo) { + ctx.restore(); + } + this._renderOverlay(ctx); + if (this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + this.fire('after:render'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} objects to render + */ + _renderObjects: function(ctx, objects) { + for (var i = 0, length = objects.length; i < length; ++i) { + objects[i] && objects[i].render(ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {string} property 'background' or 'overlay' + */ + _renderBackgroundOrOverlay: function(ctx, property) { + var object = this[property + 'Color']; + if (object) { + ctx.fillStyle = object.toLive + ? object.toLive(ctx, this) + : object; + + ctx.fillRect( + object.offsetX || 0, + object.offsetY || 0, + this.width, + this.height); + } + object = this[property + 'Image']; + if (object) { + if (this[property + 'Vpt']) { + ctx.save(); + ctx.transform.apply(ctx, this.viewportTransform); + } + object.render(ctx); + this[property + 'Vpt'] && ctx.restore(); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'background'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay: function(ctx) { + this._renderBackgroundOrOverlay(ctx, 'overlay'); + }, + + /** + * 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 in the canvas + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center horizontally + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + return this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); + }, + + /** + * Centers object vertically in the canvas + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center vertically + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); + }, + + /** + * Centers object vertically and horizontally in the canvas + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function(object) { + var center = this.getCenter(); + + return this._centerObject(object, new fabric.Point(center.left, center.top)); + }, + + /** + * Centers object vertically and horizontally in the viewport + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObject: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, vpCenter); + }, + + /** + * Centers object horizontally in the viewport, object.top is unchanged + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectH: function(object) { + var vpCenter = this.getVpCenter(); + this._centerObject(object, new fabric.Point(vpCenter.x, object.getCenterPoint().y)); + return this; + }, + + /** + * Centers object Vertically in the viewport, object.top is unchanged + * You might need to call `setCoords` on an object after centering, to update controls area. + * @param {fabric.Object} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectV: function(object) { + var vpCenter = this.getVpCenter(); + + return this._centerObject(object, new fabric.Point(object.getCenterPoint().x, vpCenter.y)); + }, + + /** + * Calculate the point in canvas that correspond to the center of actual viewport. + * @return {fabric.Point} vpCenter, viewport center + * @chainable + */ + getVpCenter: function() { + var center = this.getCenter(), + iVpt = invertTransform(this.viewportTransform); + return transformPoint({ x: center.left, y: center.top }, iVpt); + }, + + /** + * @private + * @param {fabric.Object} object Object to center + * @param {fabric.Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject: function(object, center) { + object.setPositionByOrigin(center, 'center', 'center'); + this.renderAll(); + return this; + }, + + /** + * 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 data = { + objects: this._toObjects(methodName, propertiesToInclude) + }; + + extend(data, this.__serializeBgOverlay(methodName, propertiesToInclude)); + + fabric.util.populateWithProperties(this, data, propertiesToInclude); + + return data; + }, + + /** + * @private + */ + _toObjects: function(methodName, propertiesToInclude) { + return this.getObjects().filter(function(object) { + return !object.excludeFromExport; + }).map(function(instance) { + return this._toObject(instance, methodName, propertiesToInclude); + }, this); + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + var originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, + + /** + * @private + */ + __serializeBgOverlay: function(methodName, propertiesToInclude) { + var data = { }; + + if (this.backgroundColor) { + data.background = this.backgroundColor.toObject + ? this.backgroundColor.toObject(propertiesToInclude) + : this.backgroundColor; + } + + if (this.overlayColor) { + data.overlay = this.overlayColor.toObject + ? this.overlayColor.toObject(propertiesToInclude) + : this.overlayColor; + } + if (this.backgroundImage) { + data.backgroundImage = this._toObject(this.backgroundImage, methodName, propertiesToInclude); + } + if (this.overlayImage) { + data.overlayImage = this._toObject(this.overlayImage, methodName, propertiesToInclude); + } + + return data; + }, + + /* _TO_SVG_START_ */ + /** + * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, + * a zoomed canvas will then produce zoomed SVG output. + * @type Boolean + * @default + */ + svgViewportTransformation: true, + + /** + * Returns SVG representation of canvas + * @function + * @param {Object} [options] Options object for SVG output + * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included + * @param {Object} [options.viewBox] SVG viewbox object + * @param {Number} [options.viewBox.x] x-cooridnate of viewbox + * @param {Number} [options.viewBox.y] y-coordinate of viewbox + * @param {Number} [options.viewBox.width] Width of viewbox + * @param {Number} [options.viewBox.height] Height of viewbox + * @param {String} [options.encoding=UTF-8] Encoding of SVG output + * @param {String} [options.width] desired width of svg with or without units + * @param {String} [options.height] desired height of svg with or without units + * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. + * @return {String} SVG string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} + * @example Normal SVG output + * var svg = canvas.toSVG(); + * @example SVG output without preamble (without <?xml ../>) + * var svg = canvas.toSVG({suppressPreamble: true}); + * @example SVG output with viewBox attribute + * var svg = canvas.toSVG({ + * viewBox: { + * x: 100, + * y: 100, + * width: 200, + * height: 300 + * } + * }); + * @example SVG output with different encoding (default: UTF-8) + * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); + * @example Modify SVG output with reviver function + * var svg = canvas.toSVG(null, function(svg) { + * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); + * }); + */ + toSVG: function(options, reviver) { + options || (options = { }); + + var markup = []; + + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + + this._setSVGBgOverlayColor(markup, 'backgroundColor'); + this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); + + this._setSVGObjects(markup, reviver); + + this._setSVGBgOverlayColor(markup, 'overlayColor'); + this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); + + markup.push(''); + + return markup.join(''); + }, + + /** + * @private + */ + _setSVGPreamble: function(markup, options) { + if (options.suppressPreamble) { + return; + } + markup.push( + '\n', + '\n' + ); + }, + + /** + * @private + */ + _setSVGHeader: function(markup, options) { + var width = options.width || this.width, + height = options.height || this.height, + vpt, viewBox = 'viewBox="0 0 ' + this.width + ' ' + this.height + '" ', + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + if (options.viewBox) { + viewBox = 'viewBox="' + + options.viewBox.x + ' ' + + options.viewBox.y + ' ' + + options.viewBox.width + ' ' + + options.viewBox.height + '" '; + } + else { + if (this.svgViewportTransformation) { + vpt = this.viewportTransform; + viewBox = 'viewBox="' + + toFixed(-vpt[4] / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.width / vpt[0], NUM_FRACTION_DIGITS) + ' ' + + toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS) + '" '; + } + } + + markup.push( + '\n', + 'Created with Fabric.js ', fabric.version, '\n', + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), + '\n' + ); + }, + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup: function() { + var _this = this, + markup = ['backgroundColor', 'overlayColor'].map(function(prop) { + var fill = _this[prop]; + if (fill && fill.toLive) { + return fill.toSVG(_this, false); + } + }); + return markup.join(''); + }, + + /** + * Creates markup containing SVG font faces, + * font URLs for font faces must be collected by developers + * and are not extracted from the DOM by fabricjs + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup: function() { + var markup = '', fontList = { }, obj, fontFamily, + style, row, rowIndex, _char, charIndex, + fontPaths = fabric.fontPaths, objects = this.getObjects(); + + for (var i = 0, len = objects.length; i < len; i++) { + obj = objects[i]; + fontFamily = obj.fontFamily; + if (obj.type.indexOf('text') === -1 || fontList[fontFamily] || !fontPaths[fontFamily]) { + continue; + } + fontList[fontFamily] = true; + if (!obj.styles) { + continue; + } + style = obj.styles; + for (rowIndex in style) { + row = style[rowIndex]; + for (charIndex in row) { + _char = row[charIndex]; + fontFamily = _char.fontFamily; + if (!fontList[fontFamily] && fontPaths[fontFamily]) { + fontList[fontFamily] = true; + } + } + } + } + + for (var j in fontList) { + markup += [ + '\t\t@font-face {\n', + '\t\t\tfont-family: \'', j, '\';\n', + '\t\t\tsrc: url(\'', fontPaths[j], '\');\n', + '\t\t}\n' + ].join(''); + } + + if (markup) { + markup = [ + '\t\n' + ].join(''); + } + + return markup; + }, + + /** + * @private + */ + _setSVGObjects: function(markup, reviver) { + var instance; + for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { + instance = objects[i]; + if (instance.excludeFromExport) { + continue; + } + this._setSVGObject(markup, instance, reviver); + } + }, + + /** + * push single object svg representation in the markup + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + markup.push(instance.toSVG(reviver)); + }, + + /** + * @private + */ + _setSVGBgOverlayImage: function(markup, property, reviver) { + if (this[property] && this[property].toSVG) { + markup.push(this[property].toSVG(reviver)); + } + }, + + /** + * @private + */ + _setSVGBgOverlayColor: function(markup, property) { + var filler = this[property]; + if (!filler) { + return; + } + if (filler.toLive) { + var repeat = filler.repeat; + markup.push( + '\n' + ); + } + else { + markup.push( + '\n' + ); + } + }, + /* _TO_SVG_END_ */ + + /** + * Moves an object or the objects of a multiple selection + * 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) { + if (!object) { + return this; + } + var activeGroup = this._activeGroup, + i, obj, objs; + if (object === activeGroup) { + objs = activeGroup._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.unshift(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.unshift(object); + } + return this.renderAll && this.renderAll(); + }, + + /** + * Moves an object or the objects of a multiple selection + * to the top of the stack of drawn objects + * @param {fabric.Object} object Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + if (!object) { + return this; + } + var activeGroup = this._activeGroup, + i, obj, objs; + if (object === activeGroup) { + objs = activeGroup._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.push(obj); + } + } + else { + removeFromArray(this._objects, object); + this._objects.push(object); + } + return this.renderAll && this.renderAll(); + }, + + /** + * Moves an object or a selection 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) { + if (!object) { + return this; + } + var activeGroup = this._activeGroup, + i, obj, idx, newIdx, objs; + + if (object === activeGroup) { + objs = activeGroup._objects; + for (i = 0; i < objs.length; i++) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx !== 0) { + newIdx = idx - 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== 0) { + // if object is not on the bottom of stack + newIdx = this._findNewLowerIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderAll && this.renderAll(); + return this; + }, + + /** + * @private + */ + _findNewLowerIndex: function(object, idx, intersecting) { + 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; + } + + return newIdx; + }, + + /** + * Moves an object or a selection 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) { + if (!object) { + return this; + } + var activeGroup = this._activeGroup, + i, obj, idx, newIdx, objs; + + if (object === activeGroup) { + objs = activeGroup._objects; + for (i = objs.length; i--;) { + obj = objs[i]; + idx = this._objects.indexOf(obj); + if (idx !== this._objects.length - 1) { + newIdx = idx + 1; + removeFromArray(this._objects, obj); + this._objects.splice(newIdx, 0, obj); + } + } + } + else { + idx = this._objects.indexOf(object); + if (idx !== this._objects.length - 1) { + // if object is not on top of stack (last item in an array) + newIdx = this._findNewUpperIndex(object, idx, intersecting); + removeFromArray(this._objects, object); + this._objects.splice(newIdx, 0, object); + } + } + this.renderAll && this.renderAll(); + return this; + }, + + /** + * @private + */ + _findNewUpperIndex: function(object, idx, intersecting) { + 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; + } + + return newIdx; + }, + + /** + * 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 listeners + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + this.clear(); + 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"}', + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @param {String} methodName Method to check support for; + * Could be one of "getImageData", "toDataURL", "toDataURLWithQuality" or "setLineDash" + * @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; + } + } + }); + + /** + * Returns JSON representation of canvas + * @function + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {String} JSON string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} + * @example JSON without additional properties + * var json = canvas.toJSON(); + * @example JSON with additional properties included + * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY', 'lockUniScaling']); + * @example JSON without default values + * canvas.includeDefaultValues = false; + * var json = canvas.toJSON(); + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + +})(); + + +/** + * BaseBrush class + * @class fabric.BaseBrush + * @see {@link http://fabricjs.com/freedrawing|Freedrawing demo} + */ +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', + + /** + * Stroke Dash Array. + * @type Array + * @default + */ + strokeDashArray: null, + + /** + * Sets shadow of an object + * @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)") + * @return {fabric.Object} thisArg + * @chainable + */ + setShadow: function(options) { + this.shadow = new fabric.Shadow(options); + return this; + }, + + /** + * 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; + if (this.strokeDashArray && fabric.StaticCanvas.supports('setLineDash')) { + ctx.setLineDash(this.strokeDashArray); + } + }, + + /** + * Sets brush shadow styles + * @private + */ + _setShadow: function() { + if (!this.shadow) { + return; + } + + var ctx = this.canvas.contextTop, + zoom = this.canvas.getZoom(); + + ctx.shadowColor = this.shadow.color; + ctx.shadowBlur = this.shadow.blur * zoom; + ctx.shadowOffsetX = this.shadow.offsetX * zoom; + ctx.shadowOffsetY = this.shadow.offsetY * zoom; + }, + + /** + * Removes brush shadow styles + * @private + */ + _resetShadow: function() { + var ctx = this.canvas.contextTop; + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + } +}); + + +(function() { + + /** + * 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(); + }, + + /** + * @private + * @param {Object} pointer Actual mouse position related to the canvas. + */ + _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 Point to be added to points array + */ + _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 {Object} 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, + v = this.canvas.viewportTransform, + p1 = this._points[0], + p2 = this._points[1]; + + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + ctx.beginPath(); + + //if we only have 2 points in the path and they are the same + //it means that the user only clicked the canvas without moving the mouse + //then we should be drawing a dot. A path isn't drawn between two identical dots + //that's why we set them apart a bit + if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { + p1.x -= 0.5; + p2.x += 0.5; + } + 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(); + ctx.restore(); + }, + + /** + * Converts points to SVG path + * @param {Array} points Array of points + * @return {String} SVG path + */ + convertPointsToSVGPath: function(points) { + var path = [], + p1 = new fabric.Point(points[0].x, points[0].y), + p2 = new fabric.Point(points[1].x, points[1].y); + + path.push('M ', points[0].x, ' ', points[0].y, ' '); + for (var i = 1, len = points.length; i < len; i++) { + var midPoint = p1.midPointFrom(p2); + // p1 is our bezier control point + // midpoint is our endpoint + // start point is p(i-1) value. + path.push('Q ', p1.x, ' ', p1.y, ' ', midPoint.x, ' ', midPoint.y, ' '); + p1 = new fabric.Point(points[i].x, points[i].y); + if ((i + 1) < points.length) { + p2 = new fabric.Point(points[i + 1].x, points[i + 1].y); + } + } + path.push('L ', p1.x, ' ', p1.y, ' '); + return path; + }, + + /** + * 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, { + fill: null, + stroke: this.color, + strokeWidth: this.width, + strokeLineCap: this.strokeLineCap, + strokeLineJoin: this.strokeLineJoin, + strokeDashArray: this.strokeDashArray, + originX: 'center', + originY: 'center' + }); + + 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.convertPointsToSVGPath(this._points).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; + } + + var path = this.createPath(pathData); + + 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), + ctx = this.canvas.contextTop, + v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + + ctx.fillStyle = point.fill; + ctx.beginPath(); + ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fill(); + + ctx.restore(); + }, + + /** + * 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], + circle = new fabric.Circle({ + radius: point.radius, + left: point.x, + top: point.y, + originX: 'center', + originY: 'center', + fill: point.fill + }); + + this.shadow && circle.setShadow(this.shadow); + + circles.push(circle); + } + var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); + group.canvas = this.canvas; + + 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), + + circleRadius = fabric.util.getRandomInt( + Math.max(0, this.width - 20), this.width + 20) / 2, + + 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, + + /** + * Whether overlapping dots (rectangles) should be removed (for performance reasons) + * @type Boolean + * @default + */ + optimizeOverlapping: true, + + /** + * 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, + originX: 'center', + originY: 'center', + fill: this.color + }); + + this.shadow && rect.setShadow(this.shadow); + rects.push(rect); + } + } + + if (this.optimizeOverlapping) { + rects = this._getOptimizedRects(rects); + } + + var group = new fabric.Group(rects, { originX: 'center', originY: 'center' }); + group.canvas = this.canvas; + + 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(); + }, + + /** + * @private + * @param {Array} rects + */ + _getOptimizedRects: function(rects) { + + // avoid creating duplicate rects at the same coordinates + var uniqueRects = { }, key; + + for (var i = 0, len = rects.length; i < len; i++) { + key = rects[i].left + '' + rects[i].top; + if (!uniqueRects[key]) { + uniqueRects[key] = rects[i]; + } + } + var uniqueRectsArray = []; + for (key in uniqueRects) { + uniqueRectsArray.push(uniqueRects[key]); + } + + return uniqueRectsArray; + }, + + /** + * Renders brush + */ + render: function() { + var ctx = this.canvas.contextTop; + ctx.fillStyle = this.color; + + var v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + + 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 = new fabric.Point(x, y); + point.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), + topLeft = path._getLeftTopCoords().scalarAdd(path.strokeWidth / 2); + + path.stroke = new fabric.Pattern({ + source: this.source || this.getPatternSrcFunction(), + offsetX: -topLeft.x, + offsetY: -topLeft.y + }); + return path; + } +}); + + +(function() { + + var getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + radiansToDegrees = fabric.util.radiansToDegrees, + atan2 = Math.atan2, + abs = Math.abs, + supportLineDash = fabric.StaticCanvas.supports('setLineDash'), + + STROKE_OFFSET = 0.5; + + /** + * Canvas class + * @class fabric.Canvas + * @extends fabric.StaticCanvas + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} + * @see {@link fabric.Canvas#initialize} for constructor definition + * + * @fires object:added + * @fires object:modified + * @fires object:rotating + * @fires object:scaling + * @fires object:moving + * @fires object:selected + * + * @fires before:selection:cleared + * @fires selection:cleared + * @fires selection:created + * + * @fires path:created + * @fires mouse:down + * @fires mouse:move + * @fires mouse:up + * @fires mouse:over + * @fires mouse:out + * + */ + 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(); + }, + + /** + * When true, objects can be transformed by one side (unproportionally) + * @type Boolean + * @default + */ + uniScaleTransform: false, + + /** + * Indicates which key enable unproportional scaling + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + uniScaleKey: 'shiftKey', + + /** + * When true, objects use center point as the origin of scale transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, objects use center point as the origin of rotate transformation. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: false, + + /** + * Indicates which key enable centered Transfrom + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + centeredKey: 'altKey', + + /** + * Indicates which key enable alternate action on corner + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + altActionKey: 'shiftKey', + + /** + * 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, + + /** + * Indicates which key enable multiple click selection + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.2 + * @type String + * @default + */ + selectionKey: 'shiftKey', + + /** + * Indicates which key enable alternative selection + * in case of target overlapping with active object + * values: 'altKey', 'shiftKey', 'ctrlKey'. + * If `null` or 'none' or any other string that is not a modifier key + * feature is disabled feature disabled. + * @since 1.6.5 + * @type null|String + * @default + */ + altSelectionKey: null, + + /** + * 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, + + /** + * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. + * After mousedown, mousemove creates a shape, + * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. + * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} + * @type Boolean + * @default + */ + isDrawingMode: false, + + /** + * Indicates whether objects should remain in current stack position when selected. + * When false objects are brought to top and rendered as part of the selection group + * @type Boolean + * @default + */ + preserveObjectStacking: false, + + /** + * Indicates the angle that an object will lock to while rotating. + * @type Number + * @since 1.6.7 + * @default + */ + snapAngle: 0, + + /** + * Indicates the distance from the snapAngle the rotation will lock to the snapAngle. + * When `null`, the snapThreshold will default to the snapAngle. + * @type null|Number + * @since 1.6.7 + * @default + */ + snapThreshold: null, + + /** + * Indicates if the right click on canvas can output the context menu or not + * @type Boolean + * @since 1.6.5 + * @default + */ + stopContextMenu: false, + + /** + * Indicates if the canvas can fire right click events + * @type Boolean + * @since 1.6.5 + * @default + */ + fireRightClick: false, + + /** + * Indicates if the canvas can fire middle click events + * @type Boolean + * @since 1.7.8 + * @default + */ + fireMiddleClick: false, + + /** + * @private + */ + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEventListeners(); + + this._initRetinaScaling(); + + this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); + + this.calcOffset(); + }, + + /** + * Divides objects in two groups, one to render immediately + * and one to render as activeGroup. + * @return {Array} objects to render immediately and pushes the other in the activeGroup. + */ + _chooseObjectsToRender: function() { + var activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(), + object, objsToRender = [], activeGroupObjects = []; + + if ((activeGroup || activeObject) && !this.preserveObjectStacking) { + for (var i = 0, length = this._objects.length; i < length; i++) { + object = this._objects[i]; + if ((!activeGroup || !activeGroup.contains(object)) && object !== activeObject) { + objsToRender.push(object); + } + else { + activeGroupObjects.push(object); + } + } + if (activeGroup) { + activeGroup._set('_objects', activeGroupObjects); + objsToRender.push(activeGroup); + } + activeObject && objsToRender.push(activeObject); + } + else { + objsToRender = this._objects; + } + return objsToRender; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { + this.clearContext(this.contextTop); + this.contextTopDirty = false; + } + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + return this; + }, + + /** + * 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.clearContext(ctx); + + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(ctx); + } + + this.fire('after:render'); + this.contextTopDirty = true; + return this; + }, + + /** + * Resets the current transform to its original values and chooses the type of resizing based on the event + * @private + */ + _resetCurrentTransform: function() { + var t = this._currentTransform; + + t.target.set({ + scaleX: t.original.scaleX, + scaleY: t.original.scaleY, + skewX: t.original.skewX, + skewY: t.original.skewY, + left: t.original.left, + top: t.original.top + }); + + if (this._shouldCenterTransform(t.target)) { + if (t.action === 'rotate') { + this._setOriginToCenter(t.target); + } + else { + if (t.originX !== 'center') { + if (t.originX === 'right') { + t.mouseXSign = -1; + } + else { + t.mouseXSign = 1; + } + } + if (t.originY !== 'center') { + if (t.originY === 'bottom') { + t.mouseYSign = -1; + } + else { + t.mouseYSign = 1; + } + } + + t.originX = 'center'; + t.originY = 'center'; + } + } + else { + t.originX = t.original.originX; + t.originY = t.original.originY; + } + }, + + /** + * Checks if point is contained within an area of given object + * @param {Event} e Event object + * @param {fabric.Object} target Object to test against + * @param {Object} [point] x,y object of point coordinates we want to check. + * @return {Boolean} true if point is contained within an area of given object + */ + containsPoint: function (e, target, point) { + var ignoreZoom = true, + pointer = point || this.getPointer(e, ignoreZoom), + xy; + + if (target.group && target.group === this.getActiveGroup()) { + xy = this._normalizePointer(target.group, pointer); + } + else { + xy = { x: pointer.x, y: pointer.y }; + } + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + return (target.containsPoint(xy) || target._findTargetCorner(pointer)); + }, + + /** + * @private + */ + _normalizePointer: function (object, pointer) { + var m = object.calcTransformMatrix(), + invertedM = fabric.util.invertTransform(m), + vptPointer = this.restorePointerVpt(pointer); + return fabric.util.transformPoint(vptPointer, invertedM); + }, + + /** + * 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 hasBorders = target.hasBorders, + transparentCorners = target.transparentCorners, + ctx = this.contextCache, + originalColor = target.selectionBackgroundColor; + + target.hasBorders = target.transparentCorners = false; + target.selectionBackgroundColor = ''; + + ctx.save(); + ctx.transform.apply(ctx, this.viewportTransform); + target.render(ctx); + ctx.restore(); + + target.active && target._renderControls(ctx); + + target.hasBorders = hasBorders; + target.transparentCorners = transparentCorners; + target.selectionBackgroundColor = originalColor; + + var isTransparent = fabric.util.isTransparent( + ctx, x, y, this.targetFindTolerance); + + this.clearContext(ctx); + + return isTransparent; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _shouldClearSelection: function (e, target) { + var activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(); + + return ( + !target + || + (target && + activeGroup && + !activeGroup.contains(target) && + activeGroup !== target && + !e[this.selectionKey]) + || + (target && !target.evented) + || + (target && + !target.selectable && + activeObject && + activeObject !== target) + ); + }, + + /** + * @private + * @param {fabric.Object} target + */ + _shouldCenterTransform: function (target) { + if (!target) { + return; + } + + var t = this._currentTransform, + centerTransform; + + if (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY') { + centerTransform = this.centeredScaling || target.centeredScaling; + } + else if (t.action === 'rotate') { + centerTransform = this.centeredRotation || target.centeredRotation; + } + + return centerTransform ? !t.altKey : t.altKey; + }, + + /** + * @private + */ + _getOriginFromCorner: function(target, corner) { + var origin = { + x: target.originX, + y: target.originY + }; + + if (corner === 'ml' || corner === 'tl' || corner === 'bl') { + origin.x = 'right'; + } + else if (corner === 'mr' || corner === 'tr' || corner === 'br') { + origin.x = 'left'; + } + + if (corner === 'tl' || corner === 'mt' || corner === 'tr') { + origin.y = 'bottom'; + } + else if (corner === 'bl' || corner === 'mb' || corner === 'br') { + origin.y = 'top'; + } + + return origin; + }, + + /** + * @private + */ + _getActionFromCorner: function(target, corner, e) { + if (!corner) { + return 'drag'; + } + + switch (corner) { + case 'mtr': + return 'rotate'; + case 'ml': + case 'mr': + return e[this.altActionKey] ? 'skewY' : 'scaleX'; + case 'mt': + case 'mb': + return e[this.altActionKey] ? 'skewX' : 'scaleY'; + default: + return 'scale'; + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _setupCurrentTransform: function (e, target) { + if (!target) { + return; + } + + var pointer = this.getPointer(e), + corner = target._findTargetCorner(this.getPointer(e, true)), + action = this._getActionFromCorner(target, corner, e), + origin = this._getOriginFromCorner(target, corner); + + this._currentTransform = { + target: target, + action: action, + corner: corner, + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + originX: origin.x, + originY: origin.y, + ex: pointer.x, + ey: pointer.y, + lastX: pointer.x, + lastY: pointer.y, + left: target.left, + top: target.top, + theta: degreesToRadians(target.angle), + width: target.width * target.scaleX, + mouseXSign: 1, + mouseYSign: 1, + shiftKey: e.shiftKey, + altKey: e[this.centeredKey] + }; + + this._currentTransform.original = { + left: target.left, + top: target.top, + scaleX: target.scaleX, + scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, + originX: origin.x, + originY: origin.y + }; + + this._resetCurrentTransform(); + }, + + /** + * Translates object by "setting" its left/top + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @return {Boolean} true if the translation occurred + */ + _translateObject: function (x, y) { + var transform = this._currentTransform, + target = transform.target, + newLeft = x - transform.offsetX, + newTop = y - transform.offsetY, + moveX = !target.get('lockMovementX') && target.left !== newLeft, + moveY = !target.get('lockMovementY') && target.top !== newTop; + + moveX && target.set('left', newLeft); + moveY && target.set('top', newTop); + return moveX || moveY; + }, + + /** + * Check if we are increasing a positive skew or lower it, + * checking mouse direction and pressed corner. + * @private + */ + _changeSkewTransformOrigin: function(mouseMove, t, by) { + var property = 'originX', origins = { 0: 'center' }, + skew = t.target.skewX, originA = 'left', originB = 'right', + corner = t.corner === 'mt' || t.corner === 'ml' ? 1 : -1, + flipSign = 1; + + mouseMove = mouseMove > 0 ? 1 : -1; + if (by === 'y') { + skew = t.target.skewY; + originA = 'top'; + originB = 'bottom'; + property = 'originY'; + } + origins[-1] = originA; + origins[1] = originB; + + t.target.flipX && (flipSign *= -1); + t.target.flipY && (flipSign *= -1); + + if (skew === 0) { + t.skewSign = -corner * mouseMove * flipSign; + t[property] = origins[-mouseMove]; + } + else { + skew = skew > 0 ? 1 : -1; + t.skewSign = skew; + t[property] = origins[skew * corner * flipSign]; + } + }, + + /** + * Skew object by mouse events + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @param {String} by Either 'x' or 'y' + * @return {Boolean} true if the skewing occurred + */ + _skewObject: function (x, y, by) { + var t = this._currentTransform, + target = t.target, skewed = false, + lockSkewingX = target.get('lockSkewingX'), + lockSkewingY = target.get('lockSkewingY'); + + if ((lockSkewingX && by === 'x') || (lockSkewingY && by === 'y')) { + return false; + } + + // Get the constraint point + var center = target.getCenterPoint(), + actualMouseByCenter = target.toLocalPoint(new fabric.Point(x, y), 'center', 'center')[by], + lastMouseByCenter = target.toLocalPoint(new fabric.Point(t.lastX, t.lastY), 'center', 'center')[by], + actualMouseByOrigin, constraintPosition, dim = target._getTransformedDimensions(); + + this._changeSkewTransformOrigin(actualMouseByCenter - lastMouseByCenter, t, by); + actualMouseByOrigin = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY)[by]; + constraintPosition = target.translateToOriginPoint(center, t.originX, t.originY); + // Actually skew the object + skewed = this._setObjectSkew(actualMouseByOrigin, t, by, dim); + t.lastX = x; + t.lastY = y; + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + return skewed; + }, + + /** + * Set object skew + * @private + * @return {Boolean} true if the skewing occurred + */ + _setObjectSkew: function(localMouse, transform, by, _dim) { + var target = transform.target, newValue, skewed = false, + skewSign = transform.skewSign, newDim, dimNoSkew, + otherBy, _otherBy, _by, newDimMouse, skewX, skewY; + + if (by === 'x') { + otherBy = 'y'; + _otherBy = 'Y'; + _by = 'X'; + skewX = 0; + skewY = target.skewY; + } + else { + otherBy = 'x'; + _otherBy = 'X'; + _by = 'Y'; + skewX = target.skewX; + skewY = 0; + } + + dimNoSkew = target._getTransformedDimensions(skewX, skewY); + newDimMouse = 2 * Math.abs(localMouse) - dimNoSkew[by]; + if (newDimMouse <= 2) { + newValue = 0; + } + else { + newValue = skewSign * Math.atan((newDimMouse / target['scale' + _by]) / + (dimNoSkew[otherBy] / target['scale' + _otherBy])); + newValue = fabric.util.radiansToDegrees(newValue); + } + skewed = target['skew' + _by] !== newValue; + target.set('skew' + _by, newValue); + if (target['skew' + _otherBy] !== 0) { + newDim = target._getTransformedDimensions(); + newValue = (_dim[otherBy] / newDim[otherBy]) * target['scale' + _otherBy]; + target.set('scale' + _otherBy, newValue); + } + return skewed; + }, + + /** + * Scales object by invoking its scaleX/scaleY methods + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @param {String} by Either 'x' or 'y' - specifies dimension constraint by which to scale an object. + * When not provided, an object is scaled by both dimensions equally + * @return {Boolean} true if the scaling occurred + */ + _scaleObject: function (x, y, by) { + var t = this._currentTransform, + target = t.target, + lockScalingX = target.get('lockScalingX'), + lockScalingY = target.get('lockScalingY'), + lockScalingFlip = target.get('lockScalingFlip'); + + if (lockScalingX && lockScalingY) { + return false; + } + + // Get the constraint point + var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY), + localMouse = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY), + dim = target._getTransformedDimensions(), scaled = false; + + this._setLocalMouse(localMouse, t); + + // Actually scale the object + scaled = this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip, dim); + + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + return scaled; + }, + + /** + * @private + * @return {Boolean} true if the scaling occurred + */ + _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { + var target = transform.target, forbidScalingX = false, forbidScalingY = false, scaled = false, + changeX, changeY, scaleX, scaleY; + + scaleX = localMouse.x * target.scaleX / _dim.x; + scaleY = localMouse.y * target.scaleY / _dim.y; + changeX = target.scaleX !== scaleX; + changeY = target.scaleY !== scaleY; + + if (lockScalingFlip && scaleX <= 0 && scaleX < target.scaleX) { + forbidScalingX = true; + } + + if (lockScalingFlip && scaleY <= 0 && scaleY < target.scaleY) { + forbidScalingY = true; + } + + if (by === 'equally' && !lockScalingX && !lockScalingY) { + forbidScalingX || forbidScalingY || (scaled = this._scaleObjectEqually(localMouse, target, transform, _dim)); + } + else if (!by) { + forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = scaled || changeX)); + forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY)); + } + else if (by === 'x' && !target.get('lockUniScaling')) { + forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = scaled || changeX)); + } + else if (by === 'y' && !target.get('lockUniScaling')) { + forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY)); + } + transform.newScaleX = scaleX; + transform.newScaleY = scaleY; + forbidScalingX || forbidScalingY || this._flipObject(transform, by); + return scaled; + }, + + /** + * @private + * @return {Boolean} true if the scaling occurred + */ + _scaleObjectEqually: function(localMouse, target, transform, _dim) { + + var dist = localMouse.y + localMouse.x, + lastDist = _dim.y * transform.original.scaleY / target.scaleY + + _dim.x * transform.original.scaleX / target.scaleX, + scaled; + + // We use transform.scaleX/Y instead of target.scaleX/Y + // because the object may have a min scale and we'll loose the proportions + transform.newScaleX = transform.original.scaleX * dist / lastDist; + transform.newScaleY = transform.original.scaleY * dist / lastDist; + scaled = transform.newScaleX !== target.scaleX || transform.newScaleY !== target.scaleY; + target.set('scaleX', transform.newScaleX); + target.set('scaleY', transform.newScaleY); + return scaled; + }, + + /** + * @private + */ + _flipObject: function(transform, by) { + if (transform.newScaleX < 0 && by !== 'y') { + if (transform.originX === 'left') { + transform.originX = 'right'; + } + else if (transform.originX === 'right') { + transform.originX = 'left'; + } + } + + if (transform.newScaleY < 0 && by !== 'x') { + if (transform.originY === 'top') { + transform.originY = 'bottom'; + } + else if (transform.originY === 'bottom') { + transform.originY = 'top'; + } + } + }, + + /** + * @private + */ + _setLocalMouse: function(localMouse, t) { + var target = t.target, zoom = this.getZoom(), + padding = target.padding / zoom; + + if (t.originX === 'right') { + localMouse.x *= -1; + } + else if (t.originX === 'center') { + localMouse.x *= t.mouseXSign * 2; + if (localMouse.x < 0) { + t.mouseXSign = -t.mouseXSign; + } + } + + if (t.originY === 'bottom') { + localMouse.y *= -1; + } + else if (t.originY === 'center') { + localMouse.y *= t.mouseYSign * 2; + if (localMouse.y < 0) { + t.mouseYSign = -t.mouseYSign; + } + } + + // adjust the mouse coordinates when dealing with padding + if (abs(localMouse.x) > padding) { + if (localMouse.x < 0) { + localMouse.x += padding; + } + else { + localMouse.x -= padding; + } + } + else { // mouse is within the padding, set to 0 + localMouse.x = 0; + } + + if (abs(localMouse.y) > padding) { + if (localMouse.y < 0) { + localMouse.y += padding; + } + else { + localMouse.y -= padding; + } + } + else { + localMouse.y = 0; + } + }, + + /** + * Rotates object by invoking its rotate method + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @return {Boolean} true if the rotation occurred + */ + _rotateObject: function (x, y) { + + var t = this._currentTransform; + + if (t.target.get('lockRotation')) { + return false; + } + + var lastAngle = atan2(t.ey - t.top, t.ex - t.left), + curAngle = atan2(y - t.top, x - t.left), + angle = radiansToDegrees(curAngle - lastAngle + t.theta), + hasRoated = true; + + if (t.target.snapAngle > 0) { + var snapAngle = t.target.snapAngle, + snapThreshold = t.target.snapThreshold || snapAngle, + rightAngleLocked = Math.ceil(angle / snapAngle) * snapAngle, + leftAngleLocked = Math.floor(angle / snapAngle) * snapAngle; + + if (Math.abs(angle - leftAngleLocked) < snapThreshold) { + angle = leftAngleLocked; + } + else if (Math.abs(angle - rightAngleLocked) < snapThreshold) { + angle = rightAngleLocked; + } + } + + // normalize angle to positive value + if (angle < 0) { + angle = 360 + angle; + } + angle %= 360; + + if (t.target.angle === angle) { + hasRoated = false; + } + else { + t.target.angle = angle; + } + + return hasRoated; + }, + + /** + * Set the cursor type of the canvas element + * @param {String} value Cursor type of the canvas element. + * @see http://www.w3.org/TR/css3-ui/#cursor + */ + setCursor: function (value) { + this.upperCanvasEl.style.cursor = value; + }, + + /** + * @param {fabric.Object} target to reset transform + * @private + */ + _resetObjectTransform: function (target) { + target.scaleX = 1; + target.scaleY = 1; + target.skewX = 0; + target.skewY = 0; + target.setAngle(0); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx to draw the selection on + */ + _drawSelection: function (ctx) { + var groupSelector = this._groupSelector, + left = groupSelector.left, + top = groupSelector.top, + aleft = abs(left), + atop = abs(top); + + if (this.selectionColor) { + ctx.fillStyle = this.selectionColor; + + ctx.fillRect( + groupSelector.ex - ((left > 0) ? 0 : -left), + groupSelector.ey - ((top > 0) ? 0 : -top), + aleft, + atop + ); + } + + if (!this.selectionLineWidth || !this.selectionBorderColor) { + return; + } + ctx.lineWidth = this.selectionLineWidth; + ctx.strokeStyle = this.selectionBorderColor; + + // selection border + if (this.selectionDashArray.length > 1 && !supportLineDash) { + + var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), + py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop); + + ctx.beginPath(); + + fabric.util.drawDashedLine(ctx, px, py, px + aleft, py, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px, py + atop - 1, px + aleft, py + atop - 1, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px, py, px, py + atop, this.selectionDashArray); + fabric.util.drawDashedLine(ctx, px + aleft - 1, py, px + aleft - 1, py + atop, this.selectionDashArray); + + ctx.closePath(); + ctx.stroke(); + } + else { + fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); + ctx.strokeRect( + groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), + groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), + aleft, + atop + ); + } + }, + + /** + * Method that determines what object we are clicking on + * the skipGroup parameter is for internal use, is needed for shift+click action + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through + */ + findTarget: function (e, skipGroup) { + if (this.skipTargetFind) { + return; + } + + var ignoreZoom = true, + pointer = this.getPointer(e, ignoreZoom), + activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(), + activeTarget; + // first check current group (if one exists) + // active group does not check sub targets like normal groups. + // if active group just exits. + if (activeGroup && !skipGroup && activeGroup === this._searchPossibleTargets([activeGroup], pointer)) { + this._fireOverOutEvents(activeGroup, e); + return activeGroup; + } + // if we hit the corner of an activeObject, let's return that. + if (activeObject && activeObject._findTargetCorner(pointer)) { + this._fireOverOutEvents(activeObject, e); + return activeObject; + } + if (activeObject && activeObject === this._searchPossibleTargets([activeObject], pointer)) { + if (!this.preserveObjectStacking) { + this._fireOverOutEvents(activeObject, e); + return activeObject; + } + else { + activeTarget = activeObject; + } + } + + this.targets = []; + var target = this._searchPossibleTargets(this._objects, pointer); + if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { + target = activeTarget; + } + this._fireOverOutEvents(target, e); + return target; + }, + + /** + * @private + */ + _fireOverOutEvents: function(target, e) { + if (target) { + if (this._hoveredTarget !== target) { + if (this._hoveredTarget) { + this.fire('mouse:out', { target: this._hoveredTarget, e: e }); + this._hoveredTarget.fire('mouseout', { e: e }); + } + this.fire('mouse:over', { target: target, e: e }); + target.fire('mouseover', { e: e }); + this._hoveredTarget = target; + } + } + else if (this._hoveredTarget) { + this.fire('mouse:out', { target: this._hoveredTarget, e: e }); + this._hoveredTarget.fire('mouseout', { e: e }); + this._hoveredTarget = null; + } + }, + + /** + * @private + */ + _checkTarget: function(pointer, obj) { + if (obj && + obj.visible && + obj.evented && + this.containsPoint(null, obj, pointer)){ + if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { + var isTransparent = this.isTargetTransparent(obj, pointer.x, pointer.y); + if (!isTransparent) { + return true; + } + } + else { + return true; + } + } + }, + + /** + * @private + */ + _searchPossibleTargets: function(objects, pointer) { + + // Cache all targets where their bounding box contains point. + var target, i = objects.length, normalizedPointer, subTarget; + // Do not check for currently grouped objects, since we check the parent group itself. + // untill we call this function specifically to search inside the activeGroup + while (i--) { + if (this._checkTarget(pointer, objects[i])) { + target = objects[i]; + if (target.type === 'group' && target.subTargetCheck) { + normalizedPointer = this._normalizePointer(target, pointer); + subTarget = this._searchPossibleTargets(target._objects, normalizedPointer); + subTarget && this.targets.push(subTarget); + } + break; + } + } + return target; + }, + + /** + * Returns pointer coordinates without the effect of the viewport + * @param {Object} pointer with "x" and "y" number values + * @return {Object} object with "x" and "y" number values + */ + restorePointerVpt: function(pointer) { + return fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + }, + + /** + * Returns pointer coordinates relative to canvas. + * Can return coordinates with or without viewportTransform. + * ignoreZoom false gives back coordinates that represent + * the point clicked on canvas element. + * ignoreZoom true gives back coordinates after being processed + * by the viewportTransform ( sort of coordinates of what is displayed + * on the canvas where you are clicking. + * To interact with your shapes top and left you want to use ignoreZoom true + * most of the time, while ignoreZoom false will give you coordinates + * compatible with the object.oCoords system. + * of the time. + * @param {Event} e + * @param {Boolean} ignoreZoom + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e, ignoreZoom, upperCanvasEl) { + if (!upperCanvasEl) { + upperCanvasEl = this.upperCanvasEl; + } + var pointer = getPointer(e), + bounds = upperCanvasEl.getBoundingClientRect(), + boundsWidth = bounds.width || 0, + boundsHeight = bounds.height || 0, + cssScale; + + if (!boundsWidth || !boundsHeight ) { + if ('top' in bounds && 'bottom' in bounds) { + boundsHeight = Math.abs( bounds.top - bounds.bottom ); + } + if ('right' in bounds && 'left' in bounds) { + boundsWidth = Math.abs( bounds.right - bounds.left ); + } + } + + this.calcOffset(); + + pointer.x = pointer.x - this._offset.left; + pointer.y = pointer.y - this._offset.top; + if (!ignoreZoom) { + pointer = this.restorePointerVpt(pointer); + } + + if (boundsWidth === 0 || boundsHeight === 0) { + // If bounds are not available (i.e. not visible), do not apply scale. + cssScale = { width: 1, height: 1 }; + } + else { + cssScale = { + width: upperCanvasEl.width / boundsWidth, + height: upperCanvasEl.height / boundsHeight + }; + } + + return { + x: pointer.x * cssScale.width, + y: pointer.y * cssScale.height + }; + }, + + /** + * @private + * @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 + */ + _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 {HTMLElement} element canvas element to apply styles on + */ + _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, + 'touch-action': 'none' + }); + 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; + }, + + /** + * @private + * @param {Object} object + */ + _setActiveObject: function(object) { + var obj = this._activeObject; + if (obj) { + obj.set('active', false); + if (object !== obj && obj.onDeselect && typeof obj.onDeselect === 'function') { + obj.onDeselect(); + } + } + this._activeObject = object; + object.set('active', true); + }, + + /** + * 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) { + var currentActiveObject = this.getActiveObject(); + if (currentActiveObject && currentActiveObject !== object) { + currentActiveObject.fire('deselected', { e: e }); + } + this._setActiveObject(object); + this.renderAll(); + this.fire('object:selected', { target: object, e: e }); + object.fire('selected', { e: e }); + return this; + }, + + /** + * Returns currently active object + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (this.getActiveObject() === obj) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared', { target: obj }); + obj.fire('deselected'); + } + if (this._hoveredTarget === obj) { + this._hoveredTarget = null; + } + this.callSuper('_onObjectRemoved', obj); + }, + + /** + * @private + */ + _discardActiveObject: function() { + var obj = this._activeObject; + if (obj) { + obj.set('active', false); + if (obj.onDeselect && typeof obj.onDeselect === 'function') { + obj.onDeselect(); + } + } + this._activeObject = null; + }, + + /** + * Discards currently active object and fire events. If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parmater and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @param {event} e + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function (e) { + var activeObject = this._activeObject; + if (activeObject) { + this.fire('before:selection:cleared', { target: activeObject, e: e }); + this._discardActiveObject(); + this.fire('selection:cleared', { e: e }); + activeObject.fire('deselected', { e: e }); + } + return this; + }, + + /** + * @private + * @param {fabric.Group} group + */ + _setActiveGroup: function(group) { + this._activeGroup = group; + if (group) { + group.set('active', true); + } + }, + + /** + * Sets active group to a specified one. If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parmater and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @param {fabric.Group} group Group to set as a current one + * @param {Event} e Event object + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveGroup: function (group, e) { + this._setActiveGroup(group); + if (group) { + this.fire('object:selected', { target: group, e: e }); + group.fire('selected', { e: e }); + } + return this; + }, + + /** + * Returns currently active group + * @return {fabric.Group} Current group + */ + getActiveGroup: function () { + return this._activeGroup; + }, + + /** + * @private + */ + _discardActiveGroup: function() { + var g = this.getActiveGroup(); + if (g) { + g.destroy(); + } + this.setActiveGroup(null); + }, + + /** + * Discards currently active group and fire events If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parmater and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveGroup: function (e) { + var g = this.getActiveGroup(); + if (g) { + this.fire('before:selection:cleared', { e: e, target: g }); + this._discardActiveGroup(); + this.fire('selection:cleared', { e: e }); + } + return this; + }, + + /** + * Deactivates all objects on canvas, removing any active group or object + * @return {fabric.Canvas} thisArg + * @chainable + */ + deactivateAll: function () { + var allObjects = this.getObjects(), + i = 0, + len = allObjects.length, + obj; + for ( ; i < len; i++) { + obj = allObjects[i]; + obj && obj.set('active', false); + } + this._discardActiveGroup(); + this._discardActiveObject(); + return this; + }, + + /** + * Deactivates all objects and dispatches appropriate events If the function is called by fabric + * as a consequence of a mouse event, the event is passed as a parmater and + * sent to the fire function for the custom events. When used as a method the + * e param does not have any application. + * @return {fabric.Canvas} thisArg + * @chainable + */ + deactivateAllWithDispatch: function (e) { + this.discardActiveGroup(e); + this.discardActiveObject(e); + this.deactivateAll(); + return this; + }, + + /** + * Clears a canvas element and removes all event listeners + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + this.callSuper('dispose'); + var wrapper = this.wrapperEl; + this.removeListeners(); + wrapper.removeChild(this.upperCanvasEl); + wrapper.removeChild(this.lowerCanvasEl); + delete this.upperCanvasEl; + if (wrapper.parentNode) { + wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); + } + delete this.wrapperEl; + return this; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this.discardActiveGroup(); + this.discardActiveObject(); + this.clearContext(this.contextTop); + return this.callSuper('clear'); + }, + + /** + * Draws objects' controls (borders/controls) + * @param {CanvasRenderingContext2D} ctx Context to render controls on + */ + drawControls: function(ctx) { + var activeGroup = this.getActiveGroup(); + + if (activeGroup) { + activeGroup._renderControls(ctx); + } + else { + this._drawObjectsControls(ctx); + } + }, + + /** + * @private + */ + _drawObjectsControls: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; ++i) { + if (!this._objects[i] || !this._objects[i].active) { + continue; + } + this._objects[i]._renderControls(ctx); + } + }, + + /** + * @private + */ + _toObject: function(instance, methodName, propertiesToInclude) { + //If the object is part of the current selection group, it should + //be transformed appropriately + //i.e. it should be serialised as it would appear if the selection group + //were to be destroyed. + var originalProperties = this._realizeGroupTransformOnObject(instance), + object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); + //Undo the damage we did by changing all of its properties + this._unwindGroupTransformOnObject(instance, originalProperties); + return object; + }, + + /** + * Realises an object's group transformation on it + * @private + * @param {fabric.Object} [instance] the object to transform (gets mutated) + * @returns the original values of instance which were changed + */ + _realizeGroupTransformOnObject: function(instance) { + var layoutProps = ['angle', 'flipX', 'flipY', 'height', 'left', 'scaleX', 'scaleY', 'top', 'width']; + if (instance.group && instance.group === this.getActiveGroup()) { + //Copy all the positionally relevant properties across now + var originalValues = {}; + layoutProps.forEach(function(prop) { + originalValues[prop] = instance[prop]; + }); + this.getActiveGroup().realizeTransform(instance); + return originalValues; + } + else { + return null; + } + }, + + /** + * Restores the changed properties of instance + * @private + * @param {fabric.Object} [instance] the object to un-transform (gets mutated) + * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject + */ + _unwindGroupTransformOnObject: function(instance, originalValues) { + if (originalValues) { + instance.set(originalValues); + } + }, + + /** + * @private + */ + _setSVGObject: function(markup, instance, reviver) { + var originalProperties; + //If the object is in a selection group, simulate what would happen to that + //object when the group is deselected + originalProperties = this._realizeGroupTransformOnObject(instance); + this.callSuper('_setSVGObject', markup, instance, reviver); + this._unwindGroupTransformOnObject(instance, originalProperties); + }, + }); + + // 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() { }; + } + + /** + * @ignore + * @class fabric.Element + * @alias fabric.Canvas + * @deprecated Use {@link fabric.Canvas} instead. + * @constructor + */ + fabric.Element = fabric.Canvas; +})(); + + +(function() { + + var cursorOffset = { + mt: 0, // n + tr: 1, // ne + mr: 2, // e + br: 3, // se + mb: 4, // s + bl: 5, // sw + ml: 6, // w + tl: 7 // nw + }, + addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * Map of cursor style values for each of the object controls + * @private + */ + cursorMap: [ + 'n-resize', + 'ne-resize', + 'e-resize', + 'se-resize', + 's-resize', + 'sw-resize', + 'w-resize', + 'nw-resize' + ], + + /** + * Adds mouse listeners to canvas + * @private + */ + _initEventListeners: function () { + + this._bindEvents(); + + addListener(fabric.window, 'resize', this._onResize); + + // mouse events + addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'mouseout', this._onMouseOut); + addListener(this.upperCanvasEl, 'mouseenter', this._onMouseEnter); + addListener(this.upperCanvasEl, 'wheel', this._onMouseWheel); + addListener(this.upperCanvasEl, 'contextmenu', this._onContextMenu); + + // touch events + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown, { passive: false }); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove, { passive: false }); + + if (typeof eventjs !== 'undefined' && 'add' in eventjs) { + eventjs.add(this.upperCanvasEl, 'gesture', this._onGesture); + eventjs.add(this.upperCanvasEl, 'drag', this._onDrag); + eventjs.add(this.upperCanvasEl, 'orientation', this._onOrientationChange); + eventjs.add(this.upperCanvasEl, 'shake', this._onShake); + eventjs.add(this.upperCanvasEl, 'longpress', this._onLongPress); + } + }, + + /** + * @private + */ + _bindEvents: function() { + 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 = this._onGesture.bind(this); + this._onDrag = this._onDrag.bind(this); + this._onShake = this._onShake.bind(this); + this._onLongPress = this._onLongPress.bind(this); + this._onOrientationChange = this._onOrientationChange.bind(this); + this._onMouseWheel = this._onMouseWheel.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onMouseEnter = this._onMouseEnter.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + }, + + /** + * Removes all event listeners + */ + removeListeners: function() { + removeListener(fabric.window, 'resize', this._onResize); + + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'mouseout', this._onMouseOut); + removeListener(this.upperCanvasEl, 'mouseenter', this._onMouseEnter); + removeListener(this.upperCanvasEl, 'wheel', this._onMouseWheel); + removeListener(this.upperCanvasEl, 'contextmenu', this._onContextMenu); + + removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + + if (typeof eventjs !== 'undefined' && 'remove' in eventjs) { + eventjs.remove(this.upperCanvasEl, 'gesture', this._onGesture); + eventjs.remove(this.upperCanvasEl, 'drag', this._onDrag); + eventjs.remove(this.upperCanvasEl, 'orientation', this._onOrientationChange); + eventjs.remove(this.upperCanvasEl, 'shake', this._onShake); + eventjs.remove(this.upperCanvasEl, 'longpress', this._onLongPress); + } + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function(e, self) { + this.__onTransformGesture && this.__onTransformGesture(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function(e, self) { + this.__onDrag && this.__onDrag(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on wheel event + */ + _onMouseWheel: function(e) { + this.__onMouseWheel(e); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseOut: function(e) { + var target = this._hoveredTarget; + this.fire('mouse:out', { target: target, e: e }); + this._hoveredTarget = null; + target && target.fire('mouseout', { e: e }); + if (this._iTextInstances) { + this._iTextInstances.forEach(function(obj) { + if (obj.isEditing) { + obj.hiddenTextarea.focus(); + } + }); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mouseenter + */ + _onMouseEnter: function(e) { + if (!this.findTarget(e)) { + this.fire('mouse:over', { target: null, e: e }); + this._hoveredTarget = null; + } + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function(e, self) { + this.__onOrientationChange && this.__onOrientationChange(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function(e, self) { + this.__onShake && this.__onShake(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onLongPress: function(e, self) { + this.__onLongPress && this.__onLongPress(e, self); + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onContextMenu: function (e) { + if (this.stopContextMenu) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDown: function (e) { + this.__onMouseDown(e); + + addListener(fabric.document, 'touchend', this._onMouseUp, { passive: false }); + addListener(fabric.document, 'touchmove', this._onMouseMove, { passive: false }); + + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + + if (e.type === 'touchstart') { + // Unbind mousedown to prevent double triggers from touch devices + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + } + else { + addListener(fabric.document, 'mouseup', this._onMouseUp); + addListener(fabric.document, 'mousemove', this._onMouseMove); + } + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUp: function (e) { + this.__onMouseUp(e); + + removeListener(fabric.document, 'mouseup', this._onMouseUp); + removeListener(fabric.document, 'touchend', this._onMouseUp); + + removeListener(fabric.document, 'mousemove', this._onMouseMove); + removeListener(fabric.document, 'touchmove', this._onMouseMove); + + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove, { passive: false }); + + if (e.type === 'touchend') { + // Wait 400ms before rebinding mousedown to prevent double triggers + // from touch devices + var _this = this; + setTimeout(function() { + addListener(_this.upperCanvasEl, 'mousedown', _this._onMouseDown); + }, 400); + } + }, + + /** + * @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(); + + if (activeObject && activeObject.isEditing && target === activeObject) { + // if we mouse up/down over a editing textbox a cursor change, + // there is no need to re render + return false; + } + return !!( + (target && ( + target.isMoving || + target !== activeObject)) + || + (!target && !!activeObject) + || + (!target && !activeObject && !this._groupSelector) + || + (pointer && + this._previousPointer && + this.selection && ( + pointer.x !== this._previousPointer.x || + pointer.y !== this._previousPointer.y)) + ); + }, + + /** + * 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, searchTarget = true, transform = this._currentTransform, + groupSelector = this._groupSelector, + isClick = (!groupSelector || (groupSelector.left === 0 && groupSelector.top === 0)); + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._onMouseUpInDrawingMode(e); + return; + } + + if (transform) { + this._finalizeCurrentTransform(); + searchTarget = !transform.actionPerformed; + } + + target = searchTarget ? this.findTarget(e, true) : transform.target; + + var shouldRender = this._shouldRender(target, this.getPointer(e)); + + if (target || !isClick) { + this._maybeGroupObjects(e); + } + else { + // those are done by default on mouse up + // by _maybeGroupObjects, we are skipping it in case of no target find + this._groupSelector = null; + this._currentTransform = null; + } + + if (target) { + target.isMoving = false; + } + + this._handleCursorAndEvent(e, target, 'up'); + target && (target.__corner = 0); + shouldRender && this.renderAll(); + }, + + /** + * set cursor for mouse up and handle mouseUp event + * @param {Event} e event from mouse + * @param {fabric.Object} target receiving event + * @param {String} eventType event to fire (up, down or move) + */ + _handleCursorAndEvent: function(e, target, eventType) { + this._setCursorFromEvent(e, target); + this._handleEvent(e, eventType, target ? target : null); + }, + + /** + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @param {fabric.Object} targetObj receiving event + */ + _handleEvent: function(e, eventType, targetObj) { + var target = typeof targetObj === 'undefined' ? this.findTarget(e) : targetObj, + targets = this.targets || [], + options = { e: e, target: target, subTargets: targets }; + this.fire('mouse:' + eventType, options); + target && target.fire('mouse' + eventType, options); + for (var i = 0; i < targets.length; i++) { + targets[i].fire('mouse' + eventType, options); + } + }, + + /** + * @private + */ + _finalizeCurrentTransform: function() { + + var transform = this._currentTransform, + target = transform.target; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + this._restoreOriginXY(target); + + if (transform.actionPerformed || (this.stateful && target.hasStateChanged())) { + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + }, + + /** + * @private + * @param {Object} target Object to restore + */ + _restoreOriginXY: function(target) { + if (this._previousOriginX && this._previousOriginY) { + + var originPoint = target.translateToOriginPoint( + target.getCenterPoint(), + this._previousOriginX, + this._previousOriginY); + + target.originX = this._previousOriginX; + target.originY = this._previousOriginY; + + target.left = originPoint.x; + target.top = originPoint.y; + + this._previousOriginX = null; + this._previousOriginY = null; + } + }, + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDownInDrawingMode: function(e) { + this._isCurrentlyDrawing = true; + this.discardActiveObject(e).renderAll(); + if (this.clipTo) { + fabric.util.clipContext(this, this.contextTop); + } + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseDown(pointer); + this._handleEvent(e, 'down'); + }, + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode: function(e) { + if (this._isCurrentlyDrawing) { + var pointer = this.getPointer(e); + this.freeDrawingBrush.onMouseMove(pointer); + } + this.setCursor(this.freeDrawingCursor); + this._handleEvent(e, 'move'); + }, + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUpInDrawingMode: function(e) { + this._isCurrentlyDrawing = false; + if (this.clipTo) { + this.contextTop.restore(); + } + this.freeDrawingBrush.onMouseUp(); + this._handleEvent(e, 'up'); + }, + + /** + * Method that defines the actions when mouse is clicked 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) { + + var target = this.findTarget(e); + + // if right click just fire events + var isRightClick = 'which' in e ? e.which === 3 : e.button === 2; + if (isRightClick) { + if (this.fireRightClick) { + this._handleEvent(e, 'down', target ? target : null); + } + return; + } + + var isMiddleClick = 'which' in e ? e.which === 2 : e.button === 1; + if (isMiddleClick) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'down', target ? target : null); + } + return; + } + + if (this.isDrawingMode) { + this._onMouseDownInDrawingMode(e); + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) { + return; + } + + // save pointer for check in __onMouseUp event + var pointer = this.getPointer(e, true); + this._previousPointer = pointer; + + var shouldRender = this._shouldRender(target, pointer), + shouldGroup = this._shouldGroup(e, target); + + if (this._shouldClearSelection(e, target)) { + this._clearSelection(e, target, pointer); + } + else if (shouldGroup) { + this._handleGrouping(e, target); + target = this.getActiveGroup(); + } + + if (target) { + if (target.selectable && (target.__corner || !shouldGroup)) { + this._beforeTransform(e, target); + this._setupCurrentTransform(e, target); + } + var activeObject = this.getActiveObject(); + if (target !== this.getActiveGroup() && target !== activeObject) { + this.deactivateAll(); + if (target.selectable) { + activeObject && activeObject.fire('deselected', { e: e }); + this.setActiveObject(target, e); + } + } + } + this._handleEvent(e, 'down', target ? target : null); + // we must renderAll so that we update the visuals + shouldRender && this.renderAll(); + }, + + /** + * @private + */ + _beforeTransform: function(e, target) { + this.stateful && target.saveState(); + + // determine if it's a drag or rotate case + if (target._findTargetCorner(this.getPointer(e))) { + this.onBeforeScaleRotate(target); + } + + }, + + /** + * @private + */ + _clearSelection: function(e, target, pointer) { + this.deactivateAllWithDispatch(e); + + if (target && target.selectable) { + this.setActiveObject(target, e); + } + else if (this.selection) { + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + } + }, + + /** + * @private + * @param {Object} target Object for that origin is set to center + */ + _setOriginToCenter: function(target) { + this._previousOriginX = this._currentTransform.target.originX; + this._previousOriginY = this._currentTransform.target.originY; + + var center = target.getCenterPoint(); + + target.originX = 'center'; + target.originY = 'center'; + + target.left = center.x; + target.top = center.y; + + this._currentTransform.left = target.left; + this._currentTransform.top = target.top; + }, + + /** + * @private + * @param {Object} target Object for that center is set to origin + */ + _setCenterToOrigin: function(target) { + var originPoint = target.translateToOriginPoint( + target.getCenterPoint(), + this._previousOriginX, + this._previousOriginY); + + target.originX = this._previousOriginX; + target.originY = this._previousOriginY; + + target.left = originPoint.x; + target.top = originPoint.y; + + this._previousOriginX = null; + this._previousOriginY = null; + }, + + /** + * 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) { + this._onMouseMoveInDrawingMode(e); + return; + } + if (typeof e.touches !== 'undefined' && e.touches.length > 1) { + return; + } + + var groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection + if (groupSelector) { + pointer = this.getPointer(e, true); + + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; + + this.renderTop(); + } + else if (!this._currentTransform) { + target = this.findTarget(e); + this._setCursorFromEvent(e, target); + } + else { + this._transformObject(e); + } + this._handleEvent(e, 'move', target ? target : null); + }, + + /** + * Method that defines actions when an Event Mouse Wheel + * @param {Event} e Event object fired on mouseup + */ + __onMouseWheel: function(e) { + this._handleEvent(e, 'wheel'); + }, + + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject: function(e) { + var pointer = this.getPointer(e), + transform = this._currentTransform; + + transform.reset = false; + transform.target.isMoving = true; + transform.shiftKey = e.shiftKey; + transform.altKey = e[this.centeredKey]; + + this._beforeScaleTransform(e, transform); + this._performTransformAction(e, transform, pointer); + + transform.actionPerformed && this.renderAll(); + }, + + /** + * @private + */ + _performTransformAction: function(e, transform, pointer) { + var x = pointer.x, + y = pointer.y, + target = transform.target, + action = transform.action, + actionPerformed = false; + + if (action === 'rotate') { + (actionPerformed = this._rotateObject(x, y)) && this._fire('rotating', target, e); + } + else if (action === 'scale') { + (actionPerformed = this._onScale(e, transform, x, y)) && this._fire('scaling', target, e); + } + else if (action === 'scaleX') { + (actionPerformed = this._scaleObject(x, y, 'x')) && this._fire('scaling', target, e); + } + else if (action === 'scaleY') { + (actionPerformed = this._scaleObject(x, y, 'y')) && this._fire('scaling', target, e); + } + else if (action === 'skewX') { + (actionPerformed = this._skewObject(x, y, 'x')) && this._fire('skewing', target, e); + } + else if (action === 'skewY') { + (actionPerformed = this._skewObject(x, y, 'y')) && this._fire('skewing', target, e); + } + else { + actionPerformed = this._translateObject(x, y); + if (actionPerformed) { + this._fire('moving', target, e); + this.setCursor(target.moveCursor || this.moveCursor); + } + } + transform.actionPerformed = transform.actionPerformed || actionPerformed; + }, + + /** + * @private + */ + _fire: function(eventName, target, e) { + this.fire('object:' + eventName, { target: target, e: e }); + target.fire(eventName, { e: e }); + }, + + /** + * @private + */ + _beforeScaleTransform: function(e, transform) { + if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { + var centerTransform = this._shouldCenterTransform(transform.target); + + // Switch from a normal resize to center-based + if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || + // Switch from center-based resize to normal one + (!centerTransform && transform.originX === 'center' && transform.originY === 'center') + ) { + this._resetCurrentTransform(); + transform.reset = true; + } + } + }, + + /** + * @private + * @param {Event} e Event object + * @param {Object} transform current tranform + * @param {Number} x mouse position x from origin + * @param {Number} y mouse poistion y from origin + * @return {Boolean} true if the scaling occurred + */ + _onScale: function(e, transform, x, y) { + if ((e[this.uniScaleKey] || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { + transform.currentAction = 'scale'; + return this._scaleObject(x, y); + } + else { + // Switch from a normal resize to proportional + if (!transform.reset && transform.currentAction === 'scale') { + this._resetCurrentTransform(); + } + + transform.currentAction = 'scaleEqually'; + return this._scaleObject(x, y, 'equally'); + } + }, + + /** + * 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) { + if (!target || !target.selectable) { + this.setCursor(this.defaultCursor); + return false; + } + + var hoverCursor = target.hoverCursor || this.hoverCursor, + activeGroup = this.getActiveGroup(), + // only show proper corner when group selection is not active + corner = target._findTargetCorner + && (!activeGroup || !activeGroup.contains(target)) + && target._findTargetCorner(this.getPointer(e, true)); + + if (!corner) { + this.setCursor(hoverCursor); + } + else { + this._setCornerCursor(corner, target, e); + } + //actually unclear why it should return something + //is never evaluated + return true; + }, + + /** + * @private + */ + _setCornerCursor: function(corner, target, e) { + if (corner in cursorOffset) { + this.setCursor(this._getRotatedCornerCursor(corner, target, e)); + } + else if (corner === 'mtr' && target.hasRotatingPoint) { + this.setCursor(this.rotationCursor); + } + else { + this.setCursor(this.defaultCursor); + return false; + } + }, + + /** + * @private + */ + _getRotatedCornerCursor: function(corner, target, e) { + var n = Math.round((target.getAngle() % 360) / 45); + + if (n < 0) { + n += 8; // full circle ahead + } + n += cursorOffset[corner]; + if (e[this.altActionKey] && cursorOffset[corner] % 2 === 0) { + //if we are holding shift and we are on a mx corner... + n += 2; + } + // normalize n to be from 0 to 7 + n %= 8; + + return this.cursorMap[n]; + } + }); +})(); + + +(function() { + + var min = Math.min, + max = Math.max; + + fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + * @return {Boolean} + */ + _shouldGroup: function(e, target) { + var activeObject = this.getActiveObject(); + return e[this.selectionKey] && target && target.selectable && + (this.getActiveGroup() || (activeObject && activeObject !== target)) + && this.selection; + }, + + /** + * @private + * @param {Event} e Event object + * @param {fabric.Object} target + */ + _handleGrouping: function (e, target) { + var activeGroup = this.getActiveGroup(); + + if (target === activeGroup) { + // if it's a group, find target again, using activeGroup objects + target = this.findTarget(e, true); + // if even object is not found, bail out + if (!target) { + return; + } + } + if (activeGroup) { + this._updateActiveGroup(target, e); + } + else { + this._createActiveGroup(target, e); + } + + if (this._activeGroup) { + this._activeGroup.saveCoords(); + } + }, + + /** + * @private + */ + _updateActiveGroup: function(target, e) { + var activeGroup = this.getActiveGroup(); + + if (activeGroup.contains(target)) { + + activeGroup.removeWithUpdate(target); + target.set('active', false); + + if (activeGroup.size() === 1) { + // remove group alltogether if after removal it only contains 1 object + this.discardActiveGroup(e); + // activate last remaining object + this.setActiveObject(activeGroup.item(0), e); + return; + } + } + else { + activeGroup.addWithUpdate(target); + } + this.fire('selection:created', { target: activeGroup, e: e }); + activeGroup.set('active', true); + }, + + /** + * @private + */ + _createActiveGroup: function(target, e) { + + if (this._activeObject && target !== this._activeObject) { + + var group = this._createGroup(target); + group.addWithUpdate(); + + this.setActiveGroup(group, e); + this._activeObject = null; + + this.fire('selection:created', { target: group, e: e }); + } + + target.set('active', true); + }, + + /** + * @private + * @param {Object} target + */ + _createGroup: function(target) { + + var objects = this.getObjects(), + isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), + groupObjects = isActiveLower + ? [this._activeObject, target] + : [target, this._activeObject]; + this._activeObject.isEditing && this._activeObject.exitEditing(); + return new fabric.Group(groupObjects, { + canvas: this + }); + }, + + /** + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects: function (e) { + + var group = this._collectObjects(); + + // 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(), { + canvas: this + }); + group.addWithUpdate(); + this.setActiveGroup(group, e); + group.saveCoords(); + this.fire('selection:created', { target: group, e: e }); + this.renderAll(); + } + }, + + /** + * @private + */ + _collectObjects: function() { + var group = [], + currentObject, + x1 = this._groupSelector.ex, + y1 = this._groupSelector.ey, + x2 = x1 + this._groupSelector.left, + y2 = y1 + this._groupSelector.top, + selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), + selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), + isClick = x1 === x2 && y1 === y2; + + for (var i = this._objects.length; i--; ) { + currentObject = this._objects[i]; + + if (!currentObject || !currentObject.selectable || !currentObject.visible) { + continue; + } + + if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || + currentObject.containsPoint(selectionX1Y1) || + currentObject.containsPoint(selectionX2Y2) + ) { + currentObject.set('active', true); + group.push(currentObject); + + // only add one object if it's a click + if (isClick) { + break; + } + } + } + + return group; + }, + + /** + * @private + */ + _maybeGroupObjects: function(e) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.setObjectsCoords().setCoords(); + activeGroup.isMoving = false; + this.setCursor(this.defaultCursor); + } + + // clear selection and current transformation + this._groupSelector = null; + this._currentTransform = null; + } + }); + +})(); + + +(function () { + + var supportQuality = fabric.StaticCanvas.supports('toDataURLWithQuality'); + + 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. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} + * @example Generate jpeg dataURL with lower quality + * var dataURL = canvas.toDataURL({ + * format: 'jpeg', + * quality: 0.8 + * }); + * @example Generate cropped png dataURL (clipping of canvas) + * var dataURL = canvas.toDataURL({ + * format: 'png', + * left: 100, + * top: 100, + * width: 200, + * height: 200 + * }); + * @example Generate double scaled png dataURL + * var dataURL = canvas.toDataURL({ + * format: 'png', + * multiplier: 2 + * }); + */ + toDataURL: function (options) { + options || (options = { }); + + var format = options.format || 'png', + quality = options.quality || 1, + multiplier = options.multiplier || 1, + cropping = { + left: options.left || 0, + top: options.top || 0, + width: options.width || 0, + height: options.height || 0, + }; + return this.__toDataURLWithMultiplier(format, quality, cropping, multiplier); + }, + + /** + * @private + */ + __toDataURLWithMultiplier: function(format, quality, cropping, multiplier) { + + var origWidth = this.getWidth(), + origHeight = this.getHeight(), + scaledWidth = (cropping.width || this.getWidth()) * multiplier, + scaledHeight = (cropping.height || this.getHeight()) * multiplier, + zoom = this.getZoom(), + newZoom = zoom * multiplier, + vp = this.viewportTransform, + translateX = (vp[4] - cropping.left) * multiplier, + translateY = (vp[5] - cropping.top) * multiplier, + newVp = [newZoom, 0, 0, newZoom, translateX, translateY], + originalInteractive = this.interactive; + + this.viewportTransform = newVp; + // setting interactive to false avoid exporting controls + this.interactive && (this.interactive = false); + if (origWidth !== scaledWidth || origHeight !== scaledHeight) { + // this.setDimensions is going to renderAll also; + this.setDimensions({ width: scaledWidth, height: scaledHeight }); + } + else { + this.renderAll(); + } + var data = this.__toDataURL(format, quality, cropping); + originalInteractive && (this.interactive = originalInteractive); + this.viewportTransform = vp; + //setDimensions with no option object is taking care of: + //this.width, this.height, this.renderAll() + this.setDimensions({ width: origWidth, height: origHeight }); + return data; + }, + + /** + * @private + */ + __toDataURL: function(format, quality) { + + var canvasEl = this.contextContainer.canvas; + // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 + if (format === 'jpg') { + format = 'jpeg'; + } + + var data = supportQuality + ? canvasEl.toDataURL('image/' + format, quality) + : canvasEl.toDataURL('image/' + format); + + return data; + }, + + /** + * Exports canvas element to a dataurl image (allowing to change image size via multiplier). + * @deprecated since 1.0.13 + * @param {String} format (png|jpeg) + * @param {Number} multiplier + * @param {Number} quality (0..1) + * @return {String} + */ + toDataURLWithMultiplier: function (format, multiplier, quality) { + return this.toDataURL({ + format: format, + multiplier: multiplier, + quality: quality + }); + }, + }); + +})(); + + +fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { + + /** + * Populates canvas with data from the specified dataless JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toDatalessJSON} + * @deprecated since 1.2.2 + * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @return {fabric.Canvas} instance + * @chainable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + */ + loadFromDatalessJSON: function (json, callback, reviver) { + return this.loadFromJSON(json, callback, reviver); + }, + + /** + * Populates canvas with data from the specified JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toJSON} + * @param {String|Object} json JSON string or object + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: {@link fabric.Image}) + * are initialized + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @return {fabric.Canvas} instance + * @chainable + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} + * @example loadFromJSON + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); + * @example loadFromJSON with reviver + * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { + * // `o` = json object + * // `object` = fabric.Object instance + * // ... do some stuff ... + * }); + */ + loadFromJSON: function (json, callback, reviver) { + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : fabric.util.object.clone(json); + + this.clear(); + + var _this = this; + this._enlivenObjects(serialized.objects, function () { + _this._setBgOverlay(serialized, function () { + // remove parts i cannot set as options + delete serialized.objects; + delete serialized.backgroundImage; + delete serialized.overlayImage; + delete serialized.background; + delete serialized.overlay; + // this._initOptions does too many things to just + // call it. Normally loading an Object from JSON + // create the Object instance. Here the Canvas is + // already an instance and we are just loading things over it + _this._setOptions(serialized); + callback && callback(); + }); + }, reviver); + return this; + }, + + /** + * @private + * @param {Object} serialized Object with background and overlay information + * @param {Function} callback Invoked after all background and overlay images/patterns loaded + */ + _setBgOverlay: function(serialized, callback) { + var _this = this, + loaded = { + backgroundColor: false, + overlayColor: false, + backgroundImage: false, + overlayImage: false + }; + + if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { + callback && callback(); + return; + } + + var cbIfLoaded = function () { + if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { + _this.renderAll(); + callback && callback(); + } + }; + + this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); + this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); + this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); + this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); + }, + + /** + * @private + * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) + * @param {(Object|String)} value Value to set + * @param {Object} loaded Set loaded property to true if property is set + * @param {Object} callback Callback function to invoke after property is set + */ + __setBgOverlay: function(property, value, loaded, callback) { + var _this = this; + + if (!value) { + loaded[property] = true; + callback && callback(); + return; + } + + if (property === 'backgroundImage' || property === 'overlayImage') { + fabric.util.enlivenObjects([value], function(enlivedObject){ + _this[property] = enlivedObject[0]; + loaded[property] = true; + callback && callback(); + }); + } + else { + this['set' + fabric.util.string.capitalize(property, true)](value, function() { + loaded[property] = true; + callback && callback(); + }); + } + }, + + /** + * @private + * @param {Array} objects + * @param {Function} callback + * @param {Function} [reviver] + */ + _enlivenObjects: function (objects, callback, reviver) { + var _this = this; + + if (!objects || objects.length === 0) { + callback && callback(); + return; + } + + var renderOnAddRemove = this.renderOnAddRemove; + this.renderOnAddRemove = false; + + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + enlivenedObjects.forEach(function(obj, index) { + // we splice the array just in case some custom classes restored from JSON + // will add more object to canvas at canvas init. + _this.insertAt(obj, index); + }); + + _this.renderOnAddRemove = renderOnAddRemove; + callback && callback(); + }, 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 + * @param {Array} [properties] Array of properties to include in the cloned canvas and children + */ + clone: function (callback, properties) { + var data = JSON.stringify(this.toJSON(properties)); + 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) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + degreesToRadians = fabric.util.degreesToRadians, + supportsLineDash = fabric.StaticCanvas.supports('setLineDash'), + objectCaching = !fabric.isLikelyNode; + + if (fabric.Object) { + return; + } + + /** + * Root object class from which all 2d shape classes inherit from + * @class fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects} + * @see {@link fabric.Object#initialize} for constructor definition + * + * @fires added + * @fires removed + * + * @fires selected + * @fires deselected + * @fires modified + * @fires rotating + * @fires scaling + * @fires moving + * @fires skewing + * + * @fires mousedown + * @fires mouseup + * @fires mouseover + * @fires mouseout + * @fires mousewheel + */ + fabric.Object = fabric.util.createClass(fabric.CommonMethods, /** @lends fabric.Object.prototype */ { + + /** + * Retrieves object's {@link fabric.Object#clipTo|clipping function} + * @method getClipTo + * @memberOf fabric.Object.prototype + * @return {Function} + */ + + /** + * Sets object's {@link fabric.Object#clipTo|clipping function} + * @method setClipTo + * @memberOf fabric.Object.prototype + * @param {Function} clipTo Clipping function + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#transformMatrix|transformMatrix} + * @method getTransformMatrix + * @memberOf fabric.Object.prototype + * @return {Array} transformMatrix + */ + + /** + * Sets object's {@link fabric.Object#transformMatrix|transformMatrix} + * @method setTransformMatrix + * @memberOf fabric.Object.prototype + * @param {Array} transformMatrix + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#visible|visible} state + * @method getVisible + * @memberOf fabric.Object.prototype + * @return {Boolean} True if visible + */ + + /** + * Sets object's {@link fabric.Object#visible|visible} state + * @method setVisible + * @memberOf fabric.Object.prototype + * @param {Boolean} value visible value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#shadow|shadow} + * @method getShadow + * @memberOf fabric.Object.prototype + * @return {Object} Shadow instance + */ + + /** + * Retrieves object's {@link fabric.Object#stroke|stroke} + * @method getStroke + * @memberOf fabric.Object.prototype + * @return {String} stroke value + */ + + /** + * Sets object's {@link fabric.Object#stroke|stroke} + * @method setStroke + * @memberOf fabric.Object.prototype + * @param {String} value stroke value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#strokeWidth|strokeWidth} + * @method getStrokeWidth + * @memberOf fabric.Object.prototype + * @return {Number} strokeWidth value + */ + + /** + * Sets object's {@link fabric.Object#strokeWidth|strokeWidth} + * @method setStrokeWidth + * @memberOf fabric.Object.prototype + * @param {Number} value strokeWidth value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#originX|originX} + * @method getOriginX + * @memberOf fabric.Object.prototype + * @return {String} originX value + */ + + /** + * Sets object's {@link fabric.Object#originX|originX} + * @method setOriginX + * @memberOf fabric.Object.prototype + * @param {String} value originX value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#originY|originY} + * @method getOriginY + * @memberOf fabric.Object.prototype + * @return {String} originY value + */ + + /** + * Sets object's {@link fabric.Object#originY|originY} + * @method setOriginY + * @memberOf fabric.Object.prototype + * @param {String} value originY value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#fill|fill} + * @method getFill + * @memberOf fabric.Object.prototype + * @return {String} Fill value + */ + + /** + * Sets object's {@link fabric.Object#fill|fill} + * @method setFill + * @memberOf fabric.Object.prototype + * @param {String} value Fill value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#opacity|opacity} + * @method getOpacity + * @memberOf fabric.Object.prototype + * @return {Number} Opacity value (0-1) + */ + + /** + * Sets object's {@link fabric.Object#opacity|opacity} + * @method setOpacity + * @memberOf fabric.Object.prototype + * @param {Number} value Opacity value (0-1) + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#angle|angle} (in degrees) + * @method getAngle + * @memberOf fabric.Object.prototype + * @return {Number} + */ + + /** + * Retrieves object's {@link fabric.Object#top|top position} + * @method getTop + * @memberOf fabric.Object.prototype + * @return {Number} Top value (in pixels) + */ + + /** + * Sets object's {@link fabric.Object#top|top position} + * @method setTop + * @memberOf fabric.Object.prototype + * @param {Number} value Top value (in pixels) + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#left|left position} + * @method getLeft + * @memberOf fabric.Object.prototype + * @return {Number} Left value (in pixels) + */ + + /** + * Sets object's {@link fabric.Object#left|left position} + * @method setLeft + * @memberOf fabric.Object.prototype + * @param {Number} value Left value (in pixels) + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#scaleX|scaleX} value + * @method getScaleX + * @memberOf fabric.Object.prototype + * @return {Number} scaleX value + */ + + /** + * Sets object's {@link fabric.Object#scaleX|scaleX} value + * @method setScaleX + * @memberOf fabric.Object.prototype + * @param {Number} value scaleX value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#scaleY|scaleY} value + * @method getScaleY + * @memberOf fabric.Object.prototype + * @return {Number} scaleY value + */ + + /** + * Sets object's {@link fabric.Object#scaleY|scaleY} value + * @method setScaleY + * @memberOf fabric.Object.prototype + * @param {Number} value scaleY value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#flipX|flipX} value + * @method getFlipX + * @memberOf fabric.Object.prototype + * @return {Boolean} flipX value + */ + + /** + * Sets object's {@link fabric.Object#flipX|flipX} value + * @method setFlipX + * @memberOf fabric.Object.prototype + * @param {Boolean} value flipX value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Retrieves object's {@link fabric.Object#flipY|flipY} value + * @method getFlipY + * @memberOf fabric.Object.prototype + * @return {Boolean} flipY value + */ + + /** + * Sets object's {@link fabric.Object#flipY|flipY} value + * @method setFlipY + * @memberOf fabric.Object.prototype + * @param {Boolean} value flipY value + * @return {fabric.Object} thisArg + * @chainable + */ + + /** + * Type of an object (rect, circle, path, etc.). + * Note that this property is meant to be read-only and not meant to be modified. + * If you modify, certain parts of Fabric (such as JSON loading) won't work correctly. + * @type String + * @default + */ + type: 'object', + + /** + * Horizontal origin of transformation of an object (one of "left", "right", "center") + * See http://jsfiddle.net/1ow02gea/40/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originX: 'left', + + /** + * Vertical origin of transformation of an object (one of "top", "bottom", "center") + * See http://jsfiddle.net/1ow02gea/40/ on how originX/originY affect objects in groups + * @type String + * @default + */ + originY: 'top', + + /** + * Top position of an object. Note that by default it's relative to object top. 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 left. 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, + + /** + * Angle of skew on x axes of an object (in degrees) + * @type Number + * @default + */ + skewX: 0, + + /** + * Angle of skew on y axes of an object (in degrees) + * @type Number + * @default + */ + skewY: 0, + + /** + * Size of object's controlling corners (in pixels) + * @type Number + * @default + */ + cornerSize: 13, + + /** + * 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, + + /** + * Default cursor value used when moving this object on canvas + * @type String + * @default + */ + moveCursor: 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)', + + /** + * Array specifying dash pattern of an object's borders (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + borderDashArray: null, + + /** + * Color of controlling corners of an object (when it's active) + * @type String + * @default + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * Color of controlling corners of an object (when it's active and transparentCorners false) + * @since 1.6.2 + * @type String + * @default + */ + cornerStrokeColor: null, + + /** + * Specify style of control, 'rect' or 'circle' + * @since 1.6.2 + * @type String + */ + cornerStyle: 'rect', + + /** + * Array specifying dash pattern of an object's control (hasBorder must be true) + * @since 1.6.2 + * @type Array + */ + cornerDashArray: null, + + /** + * When true, this object will use center point as the origin of transformation + * when being scaled via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredScaling: false, + + /** + * When true, this object will use center point as the origin of transformation + * when being rotated via the controls. + * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). + * @since 1.3.4 + * @type Boolean + * @default + */ + centeredRotation: true, + + /** + * Color of object's fill + * @type String + * @default + */ + fill: 'rgb(0,0,0)', + + /** + * Fill rule used to fill an object + * accepted values are nonzero, evenodd + * Backwards incompatibility note: This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead) + * @type String + * @default + */ + fillRule: 'nonzero', + + /** + * Composite rule used for canvas globalCompositeOperation + * @type String + * @default + */ + globalCompositeOperation: 'source-over', + + /** + * Background color of an object. + * @type String + * @default + */ + backgroundColor: '', + + /** + * Selection Background color of an object. colored layer behind the object when it is active. + * does not mix good with globalCompositeOperation methods. + * @type String + * @default + */ + selectionBackgroundColor: '', + + /** + * 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). + * But events still fire on it. + * @type Boolean + * @default + */ + selectable: true, + + /** + * When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4 + * @type Boolean + * @default + */ + evented: 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) + * Note that context origin is at the object's center point (not left/top corner) + * @type Function + */ + clipTo: null, + + /** + * When `true`, object horizontal movement is locked + * @type Boolean + * @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, + + /** + * When `true`, object horizontal skewing is locked + * @type Boolean + * @default + */ + lockSkewingX: false, + + /** + * When `true`, object vertical skewing is locked + * @type Boolean + * @default + */ + lockSkewingY: false, + + /** + * When `true`, object cannot be flipped by scaling into negative values + * @type Boolean + * @default + */ + lockScalingFlip: false, + + /** + * When `true`, object is not exported in SVG or OBJECT/JSON + * since 1.6.3 + * @type Boolean + * @default + */ + excludeFromExport: false, + + /** + * When `true`, object is cached on an additional canvas. + * default to true + * since 1.7.0 + * @type Boolean + * @default true + */ + objectCaching: objectCaching, + + /** + * When `true`, object properties are checked for cache invalidation. In some particular + * situation you may want this to be disabled ( spray brush, very big pathgroups, groups) + * or if your application does not allow you to modify properties for groups child you want + * to disable it for groups. + * default to false + * since 1.7.0 + * @type Boolean + * @default false + */ + statefullCache: false, + + /** + * When `true`, cache does not get updated during scaling. The picture will get blocky if scaled + * too much and will be redrawn with correct details at the end of scaling. + * this setting is performance and application dependant. + * default to true + * since 1.7.0 + * @type Boolean + * @default true + */ + noScaleCache: true, + + /** + * When set to `true`, object's cache will be rerendered next render call. + * since 1.7.0 + * @type Boolean + * @default true + */ + dirty: true, + + /** + * When set to `true`, force the object to have its own cache, even if it is inside a group + * it may be needed when your object behave in a particular way on the cache and always needs + * its own isolated canvas to render correctly. + * since 1.7.5 + * @type Boolean + * @default false + */ + needsItsOwnCache: 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 globalCompositeOperation shadow clipTo visible backgroundColor ' + + 'skewX skewY' + ).split(' '), + + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: ( + 'fill stroke strokeWidth strokeDashArray width height stroke strokeWidth strokeDashArray' + + ' strokeLineCap strokeLineJoin strokeMiterLimit fillRule backgroundColor' + ).split(' '), + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + options = options || { }; + if (options) { + this.setOptions(options); + } + if (this.objectCaching) { + this._createCacheCanvas(); + } + }, + + /** + * Create a the canvas used to keep the cached copy of the object + * @private + */ + _createCacheCanvas: function() { + this._cacheProperties = {}; + this._cacheCanvas = fabric.document.createElement('canvas'); + this._cacheContext = this._cacheCanvas.getContext('2d'); + this._updateCacheCanvas(); + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var zoom = this.canvas && this.canvas.getZoom() || 1, + objectScale = this.getObjectScaling(), + dim = this._getNonTransformedDimensions(), + retina = this.canvas && this.canvas._isRetinaScaling() ? fabric.devicePixelRatio : 1, + zoomX = objectScale.scaleX * zoom * retina, + zoomY = objectScale.scaleY * zoom * retina, + width = dim.x * zoomX, + height = dim.y * zoomY; + return { + width: width + 2, + height: height + 2, + zoomX: zoomX, + zoomY: zoomY + }; + }, + + /** + * Update width and height of the canvas for cache + * returns true or false if canvas needed resize. + * @private + * @return {Boolean} true if the canvas has been resized + */ + _updateCacheCanvas: function() { + if (this.noScaleCache && this.canvas && this.canvas._currentTransform) { + var action = this.canvas._currentTransform.action; + if (action.slice(0, 5) === 'scale') { + return false; + } + } + var dims = this._getCacheCanvasDimensions(), + width = dims.width, height = dims.height, + zoomX = dims.zoomX, zoomY = dims.zoomY; + + if (width !== this.cacheWidth || height !== this.cacheHeight) { + this._cacheCanvas.width = Math.ceil(width); + this._cacheCanvas.height = Math.ceil(height); + this._cacheContext.translate(width / 2, height / 2); + this._cacheContext.scale(zoomX, zoomY); + this.cacheWidth = width; + this.cacheHeight = height; + this.zoomX = zoomX; + this.zoomY = zoomY; + return true; + } + return false; + }, + + /** + * Sets object's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + this._setOptions(options); + this._initGradient(options.fill, 'fill'); + this._initGradient(options.stroke, 'stroke'); + this._initClipping(options); + this._initPattern(options.fill, 'fill'); + this._initPattern(options.stroke, 'stroke'); + }, + + /** + * Transforms context when rendering an object + * @param {CanvasRenderingContext2D} ctx Context + * @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node + */ + transform: function(ctx, fromLeft) { + if (this.group && !this.group._transformDone && this.group === this.canvas._activeGroup) { + this.group.transform(ctx); + } + var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint(); + ctx.translate(center.x, center.y); + this.angle && ctx.rotate(degreesToRadians(this.angle)); + ctx.scale( + this.scaleX * (this.flipX ? -1 : 1), + this.scaleY * (this.flipY ? -1 : 1) + ); + this.skewX && ctx.transform(1, 0, Math.tan(degreesToRadians(this.skewX)), 1, 0, 0); + this.skewY && ctx.transform(1, Math.tan(degreesToRadians(this.skewY)), 0, 1, 0, 0); + }, + + /** + * 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, + + 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, + stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, + strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), + strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() : 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), + backgroundColor: this.backgroundColor, + fillRule: this.fillRule, + globalCompositeOperation: this.globalCompositeOperation, + transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : null, + skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), + skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS) + }; + + fabric.util.populateWithProperties(this, object, propertiesToInclude); + + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + 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) { + var prototype = fabric.util.getKlass(object.type).prototype, + stateProperties = prototype.stateProperties; + stateProperties.forEach(function(prop) { + if (object[prop] === prototype[prop]) { + delete object[prop]; + } + var isArray = Object.prototype.toString.call(object[prop]) === '[object Array]' && + Object.prototype.toString.call(prototype[prop]) === '[object Array]'; + + // basically a check for [] === [] + if (isArray && object[prop].length === 0 && prototype[prop].length === 0) { + delete object[prop]; + } + }); + + return object; + }, + + /** + * Returns a string representation of an instance + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Return the object scale factor counting also the group scaling + * @return {Object} object with scaleX and scaleY properties + */ + getObjectScaling: function() { + var scaleX = this.scaleX, scaleY = this.scaleY; + if (this.group) { + var scaling = this.group.getObjectScaling(); + scaleX *= scaling.scaleX; + scaleY *= scaling.scaleY; + } + return { scaleX: scaleX, scaleY: scaleY }; + }, + + /** + * @private + * @param {String} key + * @param {*} 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 === 'shadow' && value && !(value instanceof fabric.Shadow)) { + value = new fabric.Shadow(value); + } + else if (key === 'dirty' && this.group) { + this.group.set('dirty', value); + } + + this[key] = value; + + if (this.cacheProperties.indexOf(key) > -1) { + if (this.group) { + this.group.set('dirty', true); + } + this.dirty = true; + } + + if (this.group && this.stateProperties.indexOf(key) > -1) { + this.group.set('dirty', true); + } + + if (key === 'width' || key === 'height') { + this.minScaleLimit = Math.min(0.1, 1 / Math.max(this.width, this.height)); + } + + return this; + }, + + /** + * This callback function is called by the parent group of an object every + * time a non-delegated property changes on the group. It is passed the key + * and value as parameters. Not adding in this function's signature to avoid + * Travis build error about unused variables. + */ + setOnGroup: function() { + // implemented by sub-classes, as needed. + }, + + /** + * Sets sourcePath of an object + * @param {String} value Value to set sourcePath to + * @return {fabric.Object} thisArg + * @chainable + */ + setSourcePath: function(value) { + this.sourcePath = value; + return this; + }, + + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf fabric.Object.prototype + * @return {Boolean} flipY value // TODO + */ + getViewportTransform: function() { + if (this.canvas && this.canvas.viewportTransform) { + return this.canvas.viewportTransform; + } + return fabric.iMatrix.concat(); + }, + + /** + * 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; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + ctx.save(); + //setup fill rule for current object + this._setupCompositeOperation(ctx); + this.drawSelectionBackground(ctx); + if (!noTransform) { + this.transform(ctx); + } + this._setOpacity(ctx); + this._setShadow(ctx); + if (this.transformMatrix) { + ctx.transform.apply(ctx, this.transformMatrix); + } + this.clipTo && fabric.util.clipContext(this, ctx); + if (this.shouldCache()) { + if (!this._cacheCanvas) { + this._createCacheCanvas(); + } + if (this.isCacheDirty(noTransform)) { + this.statefullCache && this.saveState({ propertySet: 'cacheProperties' }); + this.drawObject(this._cacheContext, noTransform); + this.dirty = false; + } + this.drawCacheOnCanvas(ctx); + } + else { + this.drawObject(ctx, noTransform); + if (noTransform && this.objectCaching && this.statefullCache) { + this.saveState({ propertySet: 'cacheProperties' }); + } + } + this.clipTo && ctx.restore(); + ctx.restore(); + }, + + /** + * Decide if the object should cache or not. + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + return this.objectCaching && + (!this.group || this.needsItsOwnCache || !this.group.isCaching()); + }, + + /** + * Check if this object or a child object will cast a shadow + * used by Group.shouldCache to know if child has a shadow recursively + * @return {Boolean} + */ + willDrawShadow: function() { + return !!this.shadow; + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + drawObject: function(ctx, noTransform) { + this._renderBackground(ctx); + this._setStrokeStyles(ctx); + this._setFillStyles(ctx); + this._render(ctx, noTransform); + }, + + /** + * Paint the cached copy of the object on the target context. + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawCacheOnCanvas: function(ctx) { + ctx.scale(1 / this.zoomX, 1 / this.zoomY); + ctx.drawImage(this._cacheCanvas, -this.cacheWidth / 2, -this.cacheHeight / 2); + }, + + /** + * Check if cache is dirty + * @param {Boolean} skipCanvas skip canvas checks because this object is painted + * on parent canvas. + */ + isCacheDirty: function(skipCanvas) { + if (!skipCanvas && this._updateCacheCanvas()) { + // in this case the context is already cleared. + return true; + } + else { + if (this.dirty || (this.statefullCache && this.hasStateChanged('cacheProperties'))) { + if (!skipCanvas) { + var width = this.cacheWidth / this.zoomX; + var height = this.cacheHeight / this.zoomY; + this._cacheContext.clearRect(-width / 2, -height / 2, width, height); + } + return true; + } + } + return false; + }, + + /** + * Draws a background for the object big as its untrasformed dimensions + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground: function(ctx) { + if (!this.backgroundColor) { + return; + } + var dim = this._getNonTransformedDimensions(); + ctx.fillStyle = this.backgroundColor; + + ctx.fillRect( + -dim.x / 2, + -dim.y / 2, + dim.x, + dim.y + ); + // if there is background color no other shadows + // should be casted + this._removeShadow(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setOpacity: function(ctx) { + ctx.globalAlpha *= this.opacity; + }, + + _setStrokeStyles: function(ctx) { + if (this.stroke) { + ctx.lineWidth = this.strokeWidth; + ctx.lineCap = this.strokeLineCap; + ctx.lineJoin = this.strokeLineJoin; + ctx.miterLimit = this.strokeMiterLimit; + ctx.strokeStyle = this.stroke.toLive + ? this.stroke.toLive(ctx, this) + : this.stroke; + } + }, + + _setFillStyles: function(ctx) { + if (this.fill) { + ctx.fillStyle = this.fill.toLive + ? this.fill.toLive(ctx, this) + : this.fill; + } + }, + + /** + * @private + * Sets line dash + * @param {CanvasRenderingContext2D} ctx Context to set the dash line on + * @param {Array} dashArray array representing dashes + * @param {Function} alternative function to call if browaser does not support lineDash + */ + _setLineDash: function(ctx, dashArray, alternative) { + if (!dashArray) { + return; + } + // Spec requires the concatenation of two copies the dash list when the number of elements is odd + if (1 & dashArray.length) { + dashArray.push.apply(dashArray, dashArray); + } + if (supportsLineDash) { + ctx.setLineDash(dashArray); + } + else { + alternative && alternative(ctx); + } + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderControls: function(ctx) { + if (!this.active || (this.group && this.group !== this.canvas.getActiveGroup())) { + return; + } + + var vpt = this.getViewportTransform(), + matrix = this.calcTransformMatrix(), + options; + matrix = fabric.util.multiplyTransformMatrices(vpt, matrix); + options = fabric.util.qrDecompose(matrix); + + ctx.save(); + ctx.translate(options.translateX, options.translateY); + ctx.lineWidth = 1 * this.borderScaleFactor; + if (!this.group) { + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + } + if (this.group && this.group === this.canvas.getActiveGroup()) { + ctx.rotate(degreesToRadians(options.angle)); + this.drawBordersInGroup(ctx, options); + } + else { + ctx.rotate(degreesToRadians(this.angle)); + this.drawBorders(ctx); + } + this.drawControls(ctx); + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setShadow: function(ctx) { + if (!this.shadow) { + return; + } + + var multX = (this.canvas && this.canvas.viewportTransform[0]) || 1, + multY = (this.canvas && this.canvas.viewportTransform[3]) || 1, + scaling = this.getObjectScaling(); + if (this.canvas && this.canvas._isRetinaScaling()) { + multX *= fabric.devicePixelRatio; + multY *= fabric.devicePixelRatio; + } + ctx.shadowColor = this.shadow.color; + ctx.shadowBlur = this.shadow.blur * (multX + multY) * (scaling.scaleX + scaling.scaleY) / 4; + ctx.shadowOffsetX = this.shadow.offsetX * multX * scaling.scaleX; + ctx.shadowOffsetY = this.shadow.offsetY * multY * scaling.scaleY; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _removeShadow: function(ctx) { + if (!this.shadow) { + return; + } + + ctx.shadowColor = ''; + ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Object} filler fabric.Pattern or fabric.Gradient + */ + _applyPatternGradientTransform: function(ctx, filler) { + if (!filler.toLive) { + return; + } + var transform = filler.gradientTransform || filler.patternTransform; + if (transform) { + ctx.transform.apply(ctx, transform); + } + var offsetX = -this.width / 2 + filler.offsetX || 0, + offsetY = -this.height / 2 + filler.offsetY || 0; + ctx.translate(offsetX, offsetY); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderFill: function(ctx) { + if (!this.fill) { + return; + } + + ctx.save(); + this._applyPatternGradientTransform(ctx, this.fill); + if (this.fillRule === 'evenodd') { + ctx.fill('evenodd'); + } + else { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderStroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke); + this._applyPatternGradientTransform(ctx, this.stroke); + ctx.stroke(); + ctx.restore(); + }, + + /** + * Clones an instance, some objects are async, so using callback method will work for every object. + * Using the direct return does not work for images and groups. + * @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 {Function} callback callback, invoked with an instance as a first argument + * @param {Object} [options] for clone as image, passed to toDataURL + * @param {Boolean} [options.enableRetinaScaling] enable retina scaling for the cloned image + * @return {fabric.Object} thisArg + */ + cloneAsImage: function(callback, options) { + var dataUrl = this.toDataURL(options); + fabric.util.loadImage(dataUrl, function(img) { + if (callback) { + callback(new fabric.Image(img)); + } + }); + 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. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetina] Enable retina scaling for clone image. Introduce in 1.6.4 + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + */ + toDataURL: function(options) { + options || (options = { }); + + var el = fabric.util.createCanvasElement(), + boundingRect = this.getBoundingRect(); + + el.width = boundingRect.width; + el.height = boundingRect.height; + fabric.util.wrapElement(el, 'div'); + var canvas = new fabric.StaticCanvas(el, { enableRetinaScaling: options.enableRetinaScaling }); + // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 + if (options.format === 'jpg') { + options.format = 'jpeg'; + } + + if (options.format === 'jpeg') { + canvas.backgroundColor = '#fff'; + } + + var origParams = { + active: this.get('active'), + left: this.getLeft(), + top: this.getTop() + }; + + this.set('active', false); + this.setPositionByOrigin(new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2), 'center', 'center'); + + var originalCanvas = this.canvas; + canvas.add(this); + var data = canvas.toDataURL(options); + + this.set(origParams).setCoords(); + this.canvas = originalCanvas; + + canvas.dispose(); + canvas = null; + + return data; + }, + + /** + * Returns true if specified type is identical to the type of an instance + * @param {String} type Type to check against + * @return {Boolean} + */ + isType: function(type) { + return this.type === type; + }, + + /** + * Returns complexity of an instance + * @return {Number} complexity of this instance (is 1 unless subclassed) + */ + complexity: function() { + return 1; + }, + + /** + * 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'} + * @param {Object} [options.gradientTransform] transforMatrix for gradient + * @return {fabric.Object} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/58y8b/|jsFiddle demo} + * @example Set linear gradient + * object.setGradient('fill', { + * type: 'linear', + * x1: -object.width / 2, + * y1: 0, + * x2: object.width / 2, + * y2: 0, + * colorStops: { + * 0: 'red', + * 0.5: '#005555', + * 1: 'rgba(0,0,255,0.5)' + * } + * }); + * canvas.renderAll(); + * @example Set radial gradient + * object.setGradient('fill', { + * type: 'radial', + * x1: 0, + * y1: 0, + * x2: 0, + * y2: 0, + * r1: object.width / 2, + * r2: 10, + * colorStops: { + * 0: 'red', + * 0.5: '#005555', + * 1: 'rgba(0,0,255,0.5)' + * } + * }); + * canvas.renderAll(); + */ + setGradient: function(property, options) { + options || (options = { }); + + var gradient = { colorStops: [] }; + + gradient.type = options.type || (options.r1 || options.r2 ? 'radial' : 'linear'); + gradient.coords = { + x1: options.x1, + y1: options.y1, + x2: options.x2, + y2: options.y2 + }; + + if (options.r1 || options.r2) { + gradient.coords.r1 = options.r1; + gradient.coords.r2 = options.r2; + } + + gradient.gradientTransform = options.gradientTransform; + fabric.Gradient.prototype.addColorStop.call(gradient, options.colorStops); + + return this.set(property, fabric.Gradient.forObject(this, gradient)); + }, + + /** + * Sets pattern fill of an object + * @param {Object} options Options object + * @param {(String|HTMLImageElement)} options.source Pattern source + * @param {String} [options.repeat=repeat] Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) + * @param {Number} [options.offsetX=0] Pattern horizontal offset from object's left/top corner + * @param {Number} [options.offsetY=0] Pattern vertical offset from object's left/top corner + * @return {fabric.Object} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/QT3pa/|jsFiddle demo} + * @example Set pattern + * fabric.util.loadImage('http://fabricjs.com/assets/escheresque_ste.png', function(img) { + * object.setPatternFill({ + * source: img, + * repeat: 'repeat' + * }); + * canvas.renderAll(); + * }); + */ + setPatternFill: function(options) { + return this.set('fill', new fabric.Pattern(options)); + }, + + /** + * Sets {@link fabric.Object#shadow|shadow} of an object + * @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)") + * @param {String} [options.color=rgb(0,0,0)] Shadow color + * @param {Number} [options.blur=0] Shadow blur + * @param {Number} [options.offsetX=0] Shadow horizontal offset + * @param {Number} [options.offsetY=0] Shadow vertical offset + * @return {fabric.Object} thisArg + * @chainable + * @see {@link http://jsfiddle.net/fabricjs/7gvJG/|jsFiddle demo} + * @example Set shadow with string notation + * object.setShadow('2px 2px 10px rgba(0,0,0,0.2)'); + * canvas.renderAll(); + * @example Set shadow with object notation + * object.setShadow({ + * color: 'red', + * blur: 10, + * offsetX: 20, + * offsetY: 20 + * }); + * canvas.renderAll(); + */ + setShadow: function(options) { + return this.set('shadow', options ? new fabric.Shadow(options) : null); + }, + + /** + * Sets "color" of an instance (alias of `set('fill', …)`) + * @param {String} color Color value + * @return {fabric.Object} thisArg + * @chainable + */ + setColor: function(color) { + this.set('fill', color); + return this; + }, + + /** + * Sets "angle" of an instance + * @param {Number} angle Angle value (in degrees) + * @return {fabric.Object} thisArg + * @chainable + */ + setAngle: function(angle) { + var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; + + if (shouldCenterOrigin) { + this._setOriginToCenter(); + } + + this.set('angle', angle); + + if (shouldCenterOrigin) { + this._resetOrigin(); + } + + 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 && this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object horizontally on current viewport of 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 + */ + viewportCenterH: function () { + this.canvas && this.canvas.viewportCenterObjectH(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 && this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically on current viewport of 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 + */ + viewportCenterV: function () { + this.canvas && this.canvas.viewportCenterObjectV(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 () { + this.canvas && this.canvas.centerObject(this); + return this; + }, + + /** + * Centers object on current viewport of 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 + */ + viewportCenter: function () { + this.canvas && this.canvas.viewportCenterObject(this); + return this; + }, + + /** + * Removes object from canvas to which it was added last + * @return {fabric.Object} thisArg + * @chainable + */ + remove: function() { + this.canvas && this.canvas.remove(this); + return this; + }, + + /** + * Returns coordinates of a pointer relative to an object + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Object} Coordinates of a pointer (x, y) + */ + getLocalPointer: function(e, pointer) { + pointer = pointer || this.canvas.getPointer(e); + var pClicked = new fabric.Point(pointer.x, pointer.y), + objectLeftTop = this._getLeftTopCoords(); + if (this.angle) { + pClicked = fabric.util.rotatePoint( + pClicked, objectLeftTop, degreesToRadians(-this.angle)); + } + return { + x: pClicked.x - objectLeftTop.x, + y: pClicked.y - objectLeftTop.y + }; + }, + + /** + * Sets canvas globalCompositeOperation for specific object + * custom composition operation for the particular object can be specifed using globalCompositeOperation property + * @param {CanvasRenderingContext2D} ctx Rendering canvas context + */ + _setupCompositeOperation: function (ctx) { + if (this.globalCompositeOperation) { + ctx.globalCompositeOperation = this.globalCompositeOperation; + } + } + }); + + 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 to use 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; + + fabric.Object._fromObject = function(className, object, callback, forceAsync, extraParam) { + var klass = fabric[className]; + object = clone(object, true); + if (forceAsync) { + fabric.util.enlivenPatterns([object.fill, object.stroke], function(patterns) { + if (typeof patterns[0] !== 'undefined') { + object.fill = patterns[0]; + } + if (typeof patterns[1] !== 'undefined') { + object.stroke = patterns[1]; + } + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + }); + } + else { + var instance = extraParam ? new klass(object[extraParam], object) : new klass(object); + callback && callback(instance); + return instance; + } + }; + + /** + * Unique id used internally when creating SVG elements + * @static + * @memberOf fabric.Object + * @type Number + */ + fabric.Object.__uid = 0; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + originXOffset = { + left: -0.5, + center: 0, + right: 0.5 + }, + originYOffset = { + top: -0.5, + center: 0, + bottom: 0.5 + }; + + 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} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {String} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {String} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + translateToGivenOrigin: function(point, fromOriginX, fromOriginY, toOriginX, toOriginY) { + var x = point.x, + y = point.y, + offsetX, offsetY, dim; + + if (typeof fromOriginX === 'string') { + fromOriginX = originXOffset[fromOriginX]; + } + else { + fromOriginX -= 0.5; + } + + if (typeof toOriginX === 'string') { + toOriginX = originXOffset[toOriginX]; + } + else { + toOriginX -= 0.5; + } + + offsetX = toOriginX - fromOriginX; + + if (typeof fromOriginY === 'string') { + fromOriginY = originYOffset[fromOriginY]; + } + else { + fromOriginY -= 0.5; + } + + if (typeof toOriginY === 'string') { + toOriginY = originYOffset[toOriginY]; + } + else { + toOriginY -= 0.5; + } + + offsetY = toOriginY - fromOriginY; + + if (offsetX || offsetY) { + dim = this._getTransformedDimensions(); + x = point.x + offsetX * dim.x; + y = point.y + offsetY * dim.y; + } + + return new fabric.Point(x, y); + }, + + /** + * 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 p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center'); + if (this.angle) { + return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * Translates the coordinates from center to origin coordinates (based on the object's dimensions) + * @param {fabric.Point} center 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 p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + if (this.angle) { + return fabric.util.rotatePoint(p, center, degreesToRadians(this.angle)); + } + return p; + }, + + /** + * 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} point The point relative to the global coordinate system + * @param {String} originX Horizontal origin: 'left', 'center' or 'right' + * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {fabric.Point} + */ + toLocalPoint: function(point, originX, originY) { + var center = this.getCenterPoint(), + p, p2; + + if (typeof originX !== 'undefined' && typeof originY !== 'undefined' ) { + p = this.translateToGivenOrigin(center, 'center', 'center', originX, originY); + } + else { + p = new fabric.Point(this.left, this.top); + } + + p2 = new fabric.Point(point.x, point.y); + if (this.angle) { + p2 = fabric.util.rotatePoint(p2, center, -degreesToRadians(this.angle)); + } + return p2.subtractEquals(p); + }, + + /** + * 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} pos 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), + 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), + hypotFull = this.getWidth(), + xFull = Math.cos(angle) * hypotFull, + yFull = Math.sin(angle) * hypotFull, + offsetFrom, offsetTo; + + //TODO: this function does not consider mixed situation like top, center. + if (typeof this.originX === 'string') { + offsetFrom = originXOffset[this.originX]; + } + else { + offsetFrom = this.originX - 0.5; + } + if (typeof to === 'string') { + offsetTo = originXOffset[to]; + } + else { + offsetTo = to - 0.5; + } + this.left += xFull * (offsetTo - offsetFrom); + this.top += yFull * (offsetTo - offsetFrom); + this.setCoords(); + this.originX = to; + }, + + /** + * Sets the origin/position of the object to it's center point + * @private + * @return {void} + */ + _setOriginToCenter: function() { + this._originalOriginX = this.originX; + this._originalOriginY = this.originY; + + var center = this.getCenterPoint(); + + this.originX = 'center'; + this.originY = 'center'; + + this.left = center.x; + this.top = center.y; + }, + + /** + * Resets the origin/position of the object to it's original origin + * @private + * @return {void} + */ + _resetOrigin: function() { + var originPoint = this.translateToOriginPoint( + this.getCenterPoint(), + this._originalOriginX, + this._originalOriginY); + + this.originX = this._originalOriginX; + this.originY = this._originalOriginY; + + this.left = originPoint.x; + this.top = originPoint.y; + + this._originalOriginX = null; + this._originalOriginY = null; + }, + + /** + * @private + */ + _getLeftTopCoords: function() { + return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); + }, + + /** + * Callback; invoked right before object is about to go from active to inactive + */ + onDeselect: function() { + /* NOOP */ + } + }); + +})(); + + +(function() { + + function getCoords(coords) { + return [ + new fabric.Point(coords.tl.x, coords.tl.y), + new fabric.Point(coords.tr.x, coords.tr.y), + new fabric.Point(coords.br.x, coords.br.y), + new fabric.Point(coords.bl.x, coords.bl.y) + ]; + } + + var degreesToRadians = fabric.util.degreesToRadians, + multiplyMatrices = fabric.util.multiplyTransformMatrices; + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Describe object's corner position in canvas element coordinates. + * properties are tl,mt,tr,ml,mr,bl,mb,br,mtr for the main controls. + * each property is an object with x, y and corner. + * The `corner` property contains in a similar manner the 4 points of the + * interactive area of the corner. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, viewportTransform, top, left, padding. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcCoords; + * @memberOf fabric.Object.prototype + */ + oCoords: null, + + /** + * Describe object's corner position in canvas object absolute coordinates + * properties are tl,tr,bl,br and describe the four main corner. + * each property is an object with x, y, instance of Fabric.Point. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, top, left. + * Those coordinates are usefull to understand where an object is. They get updated + * with oCoords but they do not need to be updated when zoom or panning change. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcCoords(true); + * @memberOf fabric.Object.prototype + */ + aCoords: null, + + /** + * return correct set of coordinates for intersection + */ + getCoords: function(absolute, calculate) { + if (!this.oCoords) { + this.setCoords(); + } + var coords = absolute ? this.aCoords : this.oCoords; + return getCoords(calculate ? this.calcCoords(absolute) : coords); + }, + + /** + * 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 + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with an area formed by 2 points + */ + intersectsWithRect: function(pointTL, pointBR, absolute, calculate) { + var coords = this.getCoords(absolute, calculate), + intersection = fabric.Intersection.intersectPolygonRectangle( + coords, + pointTL, + pointBR + ); + return intersection.status === 'Intersection'; + }, + + /** + * Checks if object intersects with another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with another object + */ + intersectsWithObject: function(other, absolute, calculate) { + var intersection = fabric.Intersection.intersectPolygonPolygon( + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) + ); + + return intersection.status === 'Intersection' + || other.isContainedWithinObject(this, absolute, calculate) + || this.isContainedWithinObject(other, absolute, calculate); + }, + + /** + * Checks if object is fully contained within area of another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area of another object + */ + isContainedWithinObject: function(other, absolute, calculate) { + var points = this.getCoords(absolute, calculate), + i = 0, lines = other._getImageLines( + calculate ? other.calcCoords(absolute) : absolute ? other.aCoords : other.oCoords + ); + for (; i < 4; i++) { + if (!other.containsPoint(points[i], lines)) { + return false; + } + } + return true; + }, + + /** + * 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 + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area formed by 2 points + */ + isContainedWithinRect: function(pointTL, pointBR, absolute, calculate) { + var boundingRect = this.getBoundingRect(absolute, calculate); + + 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 {fabric.Point} point Point to check against + * @param {Object} [lines] object returned from @method _getImageLines + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if point is inside the object + */ + containsPoint: function(point, lines, absolute, calculate) { + var lines = lines || this._getImageLines( + calculate ? this.calcCoords(absolute) : absolute ? this.aCoords : this.oCoords + ), + xPoints = this._findCrossPoints(point, lines); + + // if xPoints is odd then point is inside the object + return (xPoints !== 0 && xPoints % 2 === 1); + }, + + /** + * Checks if object is contained within the canvas with current viewportTransform + * the check is done stopping at first point that appear on screen + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within canvas + */ + isOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + var points = this.getCoords(true, calculate), point; + for (var i = 0; i < 4; i++) { + point = points[i]; + if (point.x <= pointBR.x && point.x >= pointTL.x && point.y <= pointBR.y && point.y >= pointTL.y) { + return true; + } + } + // no points on screen, check intersection with absolute coordinates + if (this.intersectsWithRect(pointTL, pointBR, true)) { + return true; + } + // worst case scenario the object is so big that contanins the screen + var centerPoint = { x: (pointTL.x + pointBR.x) / 2, y: (pointTL.y + pointBR.y) / 2 }; + if (this.containsPoint(centerPoint, null, true)) { + return true; + } + return false; + }, + + /** + * 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 {fabric.Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + */ + // remove yi, not used but left code here just in case. + _findCrossPoints: function(point, lines) { + var b1, b2, a1, a2, xi, // yi, + xcount = 0, + iLine; + + for (var lineKey in lines) { + iLine = lines[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) + * the box is intented as aligned to axis of canvas. + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Object} Object with left, top, width, height properties + */ + getBoundingRect: function(absolute, calculate) { + var coords = this.getCoords(absolute, calculate); + return fabric.util.makeBoundingBoxFromPoints(coords); + }, + + /** + * Returns width of an object bounding box counting transformations + * @return {Number} width value + */ + getWidth: function() { + return this._getTransformedDimensions().x; + }, + + /** + * Returns height of an object bounding box counting transformations + * to be renamed in 2.0 + * @return {Number} height value + */ + getHeight: function() { + return this._getTransformedDimensions().y; + }, + + /** + * 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 {Number} value 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; + return this.setCoords(); + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New width value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRect().width / 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 {Number} value 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.getBoundingRect().height / this.getHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * Calculate and returns the .coords of an object. + * @return {Object} Object with tl, tr, br, bl .... + * @chainable + */ + calcCoords: function(absolute) { + var theta = degreesToRadians(this.angle), + vpt = this.getViewportTransform(), + dim = absolute ? this._getTransformedDimensions() : this._calculateCurrentDimensions(), + currentWidth = dim.x, currentHeight = dim.y, + sinTh = Math.sin(theta), + cosTh = Math.cos(theta), + _angle = currentWidth > 0 ? Math.atan(currentHeight / currentWidth) : 0, + _hypotenuse = (currentWidth / Math.cos(_angle)) / 2, + offsetX = Math.cos(_angle + theta) * _hypotenuse, + offsetY = Math.sin(_angle + theta) * _hypotenuse, + center = this.getCenterPoint(), + // offset added for rotate and scale actions + coords = absolute ? center : fabric.util.transformPoint(center, vpt), + tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY), + tr = new fabric.Point(tl.x + (currentWidth * cosTh), tl.y + (currentWidth * sinTh)), + bl = new fabric.Point(tl.x - (currentHeight * sinTh), tl.y + (currentHeight * cosTh)), + br = new fabric.Point(coords.x + offsetX, coords.y + offsetY); + if (!absolute) { + var ml = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2), + mt = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2), + mr = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2), + mb = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2), + mtr = new fabric.Point(mt.x + sinTh * this.rotatingPointOffset, mt.y - cosTh * this.rotatingPointOffset); + } + + // debugging + + /* setTimeout(function() { + canvas.contextTop.fillStyle = 'green'; + canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + canvas.contextTop.fillRect(br.x, br.y, 3, 3); + canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + canvas.contextTop.fillRect(mtr.x, mtr.y, 3, 3); + }, 50); */ + + var coords = { + // corners + tl: tl, tr: tr, br: br, bl: bl, + }; + if (!absolute) { + // middle + coords.ml = ml; + coords.mt = mt; + coords.mr = mr; + coords.mb = mb; + // rotating point + coords.mtr = mtr; + } + return coords; + }, + + /** + * Sets corner position coordinates based on current angle, width and height + * See https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords + * @param {Boolean} [ignoreZoom] set oCoords with or without the viewport transform. + * @param {Boolean} [skipAbsolute] skip calculation of aCoords, usefull in setViewportTransform + * @return {fabric.Object} thisArg + * @chainable + */ + setCoords: function(ignoreZoom, skipAbsolute) { + this.oCoords = this.calcCoords(ignoreZoom); + if (!skipAbsolute) { + this.aCoords = this.calcCoords(true); + } + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + ignoreZoom || (this._setCornerCoords && this._setCornerCoords()); + + return this; + }, + + /** + * calculate rotation matrix of an object + * @return {Array} rotation matrix for the object + */ + _calcRotateMatrix: function() { + if (this.angle) { + var theta = degreesToRadians(this.angle), cos = Math.cos(theta), sin = Math.sin(theta); + // trying to keep rounding error small, ugly but it works. + if (cos === 6.123233995736766e-17 || cos === -1.8369701987210297e-16) { + cos = 0; + } + return [cos, sin, -sin, cos, 0, 0]; + } + return fabric.iMatrix.concat(); + }, + + /** + * calculate trasform Matrix that represent current transformation from + * object properties. + * @param {Boolean} [skipGroup] return transformMatrix for object and not go upward with parents + * @return {Array} matrix Transform Matrix for the object + */ + calcTransformMatrix: function(skipGroup) { + var center = this.getCenterPoint(), + translateMatrix = [1, 0, 0, 1, center.x, center.y], + rotateMatrix = this._calcRotateMatrix(), + dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true), + matrix = this.group && !skipGroup ? this.group.calcTransformMatrix() : fabric.iMatrix.concat(); + matrix = multiplyMatrices(matrix, translateMatrix); + matrix = multiplyMatrices(matrix, rotateMatrix); + matrix = multiplyMatrices(matrix, dimensionMatrix); + return matrix; + }, + + _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { + var skewMatrixX = [1, 0, Math.tan(degreesToRadians(skewX)), 1], + skewMatrixY = [1, Math.tan(degreesToRadians(skewY)), 0, 1], + scaleX = this.scaleX * (flipping && this.flipX ? -1 : 1), + scaleY = this.scaleY * (flipping && this.flipY ? -1 : 1), + scaleMatrix = [scaleX, 0, 0, scaleY], + m = multiplyMatrices(scaleMatrix, skewMatrixX, true); + return multiplyMatrices(m, skewMatrixY, true); + }, + + /* + * Calculate object dimensions from its properties + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getNonTransformedDimensions: function() { + var strokeWidth = this.strokeWidth, + w = this.width + strokeWidth, + h = this.height + strokeWidth; + return { x: w, y: h }; + }, + + /* + * Calculate object bounding boxdimensions from its properties scale, skew. + * @private + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; + } + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions = this._getNonTransformedDimensions(), + dimX = dimensions.x / 2, dimY = dimensions.y / 2, + points = [ + { + x: -dimX, + y: -dimY + }, + { + x: dimX, + y: -dimY + }, + { + x: -dimX, + y: dimY + }, + { + x: dimX, + y: dimY + }], + i, transformMatrix = this._calcDimensionsTransformMatrix(skewX, skewY, false), + bbox; + for (i = 0; i < points.length; i++) { + points[i] = fabric.util.transformPoint(points[i], transformMatrix); + } + bbox = fabric.util.makeBoundingBoxFromPoints(points); + return { x: bbox.width, y: bbox.height }; + }, + + /* + * Calculate object dimensions for controls. include padding and canvas zoom + * private + */ + _calculateCurrentDimensions: function() { + var vpt = this.getViewportTransform(), + dim = this._getTransformedDimensions(), + p = fabric.util.transformPoint(dim, vpt, true); + + return p.scalarAdd(2 * this.padding); + }, + }); +})(); + + +fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * 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; + } +}); + + +/* _TO_SVG_START_ */ +(function() { + + function getSvgColorString(prop, value) { + if (!value) { + return prop + ': none; '; + } + else if (value.toLive) { + return prop + ': url(#SVGID_' + value.id + '); '; + } + else { + var color = new fabric.Color(value), + str = prop + ': ' + color.toRgb() + '; ', + opacity = color.getAlpha(); + if (opacity !== 1) { + //change the color in rgb + opacity + str += prop + '-opacity: ' + opacity.toString() + '; '; + } + return str; + } + } + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + /** + * Returns styles-string for svg-export + * @param {Boolean} skipShadow a boolean to skip shadow filter output + * @return {String} + */ + getSvgStyles: function(skipShadow) { + + var fillRule = this.fillRule, + strokeWidth = this.strokeWidth ? this.strokeWidth : '0', + strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : 'none', + strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt', + strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter', + strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4', + opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', + visibility = this.visible ? '' : ' visibility: hidden;', + filter = skipShadow ? '' : this.getSvgFilter(), + fill = getSvgColorString('fill', this.fill), + stroke = getSvgColorString('stroke', this.stroke); + + return [ + stroke, + 'stroke-width: ', strokeWidth, '; ', + 'stroke-dasharray: ', strokeDashArray, '; ', + 'stroke-linecap: ', strokeLineCap, '; ', + 'stroke-linejoin: ', strokeLineJoin, '; ', + 'stroke-miterlimit: ', strokeMiterLimit, '; ', + fill, + 'fill-rule: ', fillRule, '; ', + 'opacity: ', opacity, ';', + filter, + visibility + ].join(''); + }, + + /** + * Returns filter for svg shadow + * @return {String} + */ + getSvgFilter: function() { + return this.shadow ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; + }, + + /** + * Returns id attribute for svg output + * @return {String} + */ + getSvgId: function() { + return this.id ? 'id="' + this.id + '" ' : ''; + }, + + /** + * Returns transform-string for svg-export + * @return {String} + */ + getSvgTransform: function() { + if (this.group && this.group.type === 'path-group') { + return ''; + } + var toFixed = fabric.util.toFixed, + angle = this.getAngle(), + skewX = (this.getSkewX() % 360), + skewY = (this.getSkewY() % 360), + center = this.getCenterPoint(), + + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + + translatePart = this.type === 'path-group' ? '' : 'translate(' + + toFixed(center.x, NUM_FRACTION_DIGITS) + + ' ' + + toFixed(center.y, NUM_FRACTION_DIGITS) + + ')', + + anglePart = angle !== 0 + ? (' rotate(' + toFixed(angle, NUM_FRACTION_DIGITS) + ')') + : '', + + scalePart = (this.scaleX === 1 && this.scaleY === 1) + ? '' : + (' scale(' + + toFixed(this.scaleX, NUM_FRACTION_DIGITS) + + ' ' + + toFixed(this.scaleY, NUM_FRACTION_DIGITS) + + ')'), + + skewXPart = skewX !== 0 ? ' skewX(' + toFixed(skewX, NUM_FRACTION_DIGITS) + ')' : '', + + skewYPart = skewY !== 0 ? ' skewY(' + toFixed(skewY, NUM_FRACTION_DIGITS) + ')' : '', + + addTranslateX = this.type === 'path-group' ? this.width : 0, + + flipXPart = this.flipX ? ' matrix(-1 0 0 1 ' + addTranslateX + ' 0) ' : '', + + addTranslateY = this.type === 'path-group' ? this.height : 0, + + flipYPart = this.flipY ? ' matrix(1 0 0 -1 0 ' + addTranslateY + ')' : ''; + + return [ + translatePart, anglePart, scalePart, flipXPart, flipYPart, skewXPart, skewYPart + ].join(''); + }, + + /** + * Returns transform-string for svg-export from the transform matrix of single elements + * @return {String} + */ + getSvgTransformMatrix: function() { + return this.transformMatrix ? ' matrix(' + this.transformMatrix.join(' ') + ') ' : ''; + }, + + /** + * @private + */ + _createBaseSVGMarkup: function() { + var markup = []; + + if (this.fill && this.fill.toLive) { + markup.push(this.fill.toSVG(this, false)); + } + if (this.stroke && this.stroke.toLive) { + markup.push(this.stroke.toSVG(this, false)); + } + if (this.shadow) { + markup.push(this.shadow.toSVG(this)); + } + return markup; + } + }); +})(); +/* _TO_SVG_END_ */ + + +(function() { + + var extend = fabric.util.object.extend, + originalSet = 'stateProperties'; + + /* + Depends on `stateProperties` + */ + function saveProps(origin, destination, props) { + var tmpObj = { }, deep = true; + props.forEach(function(prop) { + tmpObj[prop] = origin[prop]; + }); + extend(origin[destination], tmpObj, deep); + } + + function _isEqual(origValue, currentValue, firstPass) { + if (!fabric.isLikelyNode && origValue instanceof Element) { + // avoid checking deep html elements + return origValue === currentValue; + } + else if (origValue instanceof Array) { + if (origValue.length !== currentValue.length) { + return false; + } + for (var i = 0, len = origValue.length; i < len; i++) { + if (origValue[i] !== currentValue[i]) { + return false; + } + } + return true; + } + else if (origValue && typeof origValue === 'object') { + if (!firstPass && Object.keys(origValue).length !== Object.keys(currentValue).length) { + return false; + } + for (var key in origValue) { + if (!_isEqual(origValue[key], currentValue[key])) { + return false; + } + } + return true; + } + else { + return origValue === currentValue; + } + } + + + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * Returns true if object state (one of its state properties) was changed + * @param {String} [propertySet] optional name for the set of property we want to save + * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called + */ + hasStateChanged: function(propertySet) { + propertySet = propertySet || originalSet; + propertySet = '_' + propertySet; + if (!Object.keys(this[propertySet]).length) { + return true; + } + return !_isEqual(this[propertySet], this, true); + }, + + /** + * 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) { + var propertySet = options && options.propertySet || originalSet, + destination = '_' + propertySet; + if (!this[destination]) { + return this.setupState(options); + } + saveProps(this, destination, this[propertySet]); + if (options && options.stateProperties) { + saveProps(this, destination, options.stateProperties); + } + return this; + }, + + /** + * Setups state of an object + * @param {Object} [options] Object with additional `stateProperties` array to include when saving state + * @return {fabric.Object} thisArg + */ + setupState: function(options) { + options = options || { }; + var propertySet = options.propertySet || originalSet; + options.propertySet = propertySet; + this['_' + propertySet] = { }; + this.saveState(options); + return this; + } + }); +})(); + + +(function() { + + var degreesToRadians = fabric.util.degreesToRadians, + /* eslint-disable camelcase */ + isVML = function() { return typeof G_vmlCanvasManager !== 'undefined'; }; + /* eslint-enable camelcase */ + fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { + + /** + * The object interactivity controls. + * @private + */ + _controlsVisibility: null, + + /** + * Determines which corner has been clicked + * @private + * @param {Object} pointer The pointer indicating the mouse position + * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found + */ + _findTargetCorner: function(pointer) { + if (!this.hasControls || !this.active) { + return false; + } + + var ex = pointer.x, + ey = pointer.y, + xPoints, + lines; + this.__corner = 0; + for (var i in this.oCoords) { + + if (!this.isControlVisible(i)) { + continue; + } + + if (i === 'mtr' && !this.hasRotatingPoint) { + continue; + } + + if (this.get('lockUniScaling') && + (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { + continue; + } + + lines = this._getImageLines(this.oCoords[i].corner); + + // debugging + + // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + 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, + newTheta = degreesToRadians(45 - this.angle), + /* Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, */ + /* 0.707106 stands for sqrt(2)/2 */ + cornerHypotenuse = this.cornerSize * 0.707106, + cosHalfOffset = cornerHypotenuse * Math.cos(newTheta), + sinHalfOffset = cornerHypotenuse * Math.sin(newTheta), + x, y; + + for (var point in coords) { + x = coords[point].x; + y = coords[point].y; + coords[point].corner = { + tl: { + x: x - sinHalfOffset, + y: y - cosHalfOffset + }, + tr: { + x: x + cosHalfOffset, + y: y - sinHalfOffset + }, + bl: { + x: x - cosHalfOffset, + y: y + sinHalfOffset + }, + br: { + x: x + sinHalfOffset, + y: y + cosHalfOffset + } + }; + } + }, + + /** + * Draws a colored layer behind the object, inside its selection borders. + * Requires public options: padding, selectionBackgroundColor + * this function is called when the context is transformed + * has checks to be skipped when the object is on a staticCanvas + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawSelectionBackground: function(ctx) { + if (!this.selectionBackgroundColor || this.group || !this.active || + (this.canvas && !this.canvas.interactive)) { + return this; + } + ctx.save(); + var center = this.getCenterPoint(), wh = this._calculateCurrentDimensions(), + vpt = this.canvas.viewportTransform; + ctx.translate(center.x, center.y); + ctx.scale(1 / vpt[0], 1 / vpt[3]); + ctx.rotate(degreesToRadians(this.angle)); + ctx.fillStyle = this.selectionBackgroundColor; + ctx.fillRect(-wh.x / 2, -wh.y / 2, wh.x, wh.y); + ctx.restore(); + return this; + }, + + /** + * 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 wh = this._calculateCurrentDimensions(), + strokeWidth = 1 / this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth; + + ctx.save(); + ctx.strokeStyle = this.borderColor; + this._setLineDash(ctx, this.borderDashArray, null); + + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { + + var rotateHeight = -height / 2; + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight - this.rotatingPointOffset); + ctx.closePath(); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + /** + * Draws borders of an object's bounding box when it is inside a group. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @param {object} options object representing current object parameters + * @return {fabric.Object} thisArg + * @chainable + */ + drawBordersInGroup: function(ctx, options) { + if (!this.hasBorders) { + return this; + } + + var p = this._getNonTransformedDimensions(), + matrix = fabric.util.customTransformMatrix(options.scaleX, options.scaleY, options.skewX), + wh = fabric.util.transformPoint(p, matrix), + strokeWidth = 1 / this.borderScaleFactor, + width = wh.x + strokeWidth, + height = wh.y + strokeWidth; + + ctx.save(); + this._setLineDash(ctx, this.borderDashArray, null); + ctx.strokeStyle = this.borderColor; + + ctx.strokeRect( + -width / 2, + -height / 2, + width, + height + ); + + ctx.restore(); + return this; + }, + + /** + * Draws corners of an object's bounding box. + * Requires public properties: width, height + * 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 wh = this._calculateCurrentDimensions(), + width = wh.x, + height = wh.y, + scaleOffset = this.cornerSize, + left = -(width + scaleOffset) / 2, + top = -(height + scaleOffset) / 2, + methodName = this.transparentCorners ? 'stroke' : 'fill'; + + ctx.save(); + ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + if (!this.transparentCorners) { + ctx.strokeStyle = this.cornerStrokeColor; + } + this._setLineDash(ctx, this.cornerDashArray, null); + + // top-left + this._drawControl('tl', ctx, methodName, + left, + top); + + // top-right + this._drawControl('tr', ctx, methodName, + left + width, + top); + + // bottom-left + this._drawControl('bl', ctx, methodName, + left, + top + height); + + // bottom-right + this._drawControl('br', ctx, methodName, + left + width, + top + height); + + if (!this.get('lockUniScaling')) { + + // middle-top + this._drawControl('mt', ctx, methodName, + left + width / 2, + top); + + // middle-bottom + this._drawControl('mb', ctx, methodName, + left + width / 2, + top + height); + + // middle-right + this._drawControl('mr', ctx, methodName, + left + width, + top + height / 2); + + // middle-left + this._drawControl('ml', ctx, methodName, + left, + top + height / 2); + } + + // middle-top-rotate + if (this.hasRotatingPoint) { + this._drawControl('mtr', ctx, methodName, + left + width / 2, + top - this.rotatingPointOffset); + } + + ctx.restore(); + + return this; + }, + + /** + * @private + */ + _drawControl: function(control, ctx, methodName, left, top) { + if (!this.isControlVisible(control)) { + return; + } + var size = this.cornerSize, stroke = !this.transparentCorners && this.cornerStrokeColor; + switch (this.cornerStyle) { + case 'circle': + ctx.beginPath(); + ctx.arc(left + size / 2, top + size / 2, size / 2, 0, 2 * Math.PI, false); + ctx[methodName](); + if (stroke) { + ctx.stroke(); + } + break; + default: + isVML() || this.transparentCorners || ctx.clearRect(left, top, size, size); + ctx[methodName + 'Rect'](left, top, size, size); + if (stroke) { + ctx.strokeRect(left, top, size, size); + } + } + }, + + /** + * Returns true if the specified control is visible, false otherwise. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @returns {Boolean} true if the specified control is visible, false otherwise + */ + isControlVisible: function(controlName) { + return this._getControlsVisibility()[controlName]; + }, + + /** + * Sets the visibility of the specified control. + * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. + * @param {Boolean} visible true to set the specified control visible, false otherwise + * @return {fabric.Object} thisArg + * @chainable + */ + setControlVisible: function(controlName, visible) { + this._getControlsVisibility()[controlName] = visible; + return this; + }, + + /** + * Sets the visibility state of object controls. + * @param {Object} [options] Options object + * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it + * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it + * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it + * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it + * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it + * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it + * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it + * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it + * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it + * @return {fabric.Object} thisArg + * @chainable + */ + setControlsVisibility: function(options) { + options || (options = { }); + + for (var p in options) { + this.setControlVisible(p, options[p]); + } + return this; + }, + + /** + * Returns the instance of the control visibility set for this object. + * @private + * @returns {Object} + */ + _getControlsVisibility: function() { + if (!this._controlsVisibility) { + this._controlsVisibility = { + tl: true, + tr: true, + br: true, + bl: true, + ml: true, + mt: true, + mr: true, + mb: true, + mtr: true + }; + } + return this._controlsVisibility; + } + }); +})(); + + +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 Property to animate (if string) or properties to animate (if object) + * @param {Number|Object} value Value to animate property to (if string was given first) or options object + * @return {fabric.Object} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#animation} + * @chainable + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + var propsToAnimate = [], prop, skipCallbacks; + for (prop in arguments[0]) { + propsToAnimate.push(prop); + } + for (var i = 0, len = propsToAnimate.length; i < len; i++) { + prop = propsToAnimate[i]; + skipCallbacks = i !== len - 1; + this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @param {String} property Property to animate + * @param {String} to Value to animate to + * @param {Object} [options] Options object + * @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked + */ + _animate: function(property, to, options, skipCallbacks) { + var _this = this, propPair; + + to = to.toString(); + + if (!options) { + options = { }; + } + else { + options = fabric.util.object.clone(options); + } + + if (~property.indexOf('.')) { + propPair = property.split('.'); + } + + var currentValue = propPair + ? this.get(propPair[0])[propPair[1]] + : this.get(property); + + if (!('from' in options)) { + options.from = currentValue; + } + + if (~to.indexOf('=')) { + to = currentValue + parseFloat(to.replace('=', '')); + } + else { + to = parseFloat(to); + } + + fabric.util.animate({ + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + abort: options.abort && function() { + return options.abort.call(_this); + }, + onChange: function(value) { + if (propPair) { + _this[propPair[0]][propPair[1]] = value; + } + else { + _this.set(property, value); + } + if (skipCallbacks) { + return; + } + options.onChange && options.onChange(); + }, + onComplete: function() { + if (skipCallbacks) { + return; + } + + _this.setCoords(); + options.onComplete && options.onComplete(); + } + }); + } +}); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }, + supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push( + 'x1', + 'x2', + 'y1', + 'y2' + ); + + /** + * Line class + * @class fabric.Line + * @extends fabric.Object + * @see {@link fabric.Line#initialize} for constructor definition + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'line', + + /** + * x value or first line edge + * @type Number + * @default + */ + x1: 0, + + /** + * y value or first line edge + * @type Number + * @default + */ + y1: 0, + + /** + * x value or second line edge + * @type Number + * @default + */ + x2: 0, + + /** + * y value or second line edge + * @type Number + * @default + */ + y2: 0, + + cacheProperties: cacheProperties, + + /** + * Constructor + * @param {Array} [points] Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} thisArg + */ + initialize: function(points, options) { + if (!points) { + points = [0, 0, 0, 0]; + } + + this.callSuper('initialize', options); + + this.set('x1', points[0]); + this.set('y1', points[1]); + this.set('x2', points[2]); + this.set('y2', points[3]); + + this._setWidthHeight(options); + }, + + /** + * @private + * @param {Object} [options] Options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.width = Math.abs(this.x2 - this.x1); + this.height = Math.abs(this.y2 - this.y1); + + this.left = 'left' in options + ? options.left + : this._getLeftToOriginX(); + + this.top = 'top' in options + ? options.top + : this._getTopToOriginY(); + }, + + /** + * @private + * @param {String} key + * @param {*} value + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + if (typeof coordProps[key] !== 'undefined') { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. + */ + _getLeftToOriginX: makeEdgeToOriginGetter( + { // property names + origin: 'originX', + axis1: 'x1', + axis2: 'x2', + dimension: 'width' + }, + { // possible values of origin + nearest: 'left', + center: 'center', + farthest: 'right' + } + ), + + /** + * @private + * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. + */ + _getTopToOriginY: makeEdgeToOriginGetter( + { // property names + origin: 'originY', + axis1: 'y1', + axis2: 'y2', + dimension: 'height' + }, + { // possible values of origin + nearest: 'top', + center: 'center', + farthest: 'bottom' + } + ), + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + + if (noTransform) { + // Line coords are distances from left-top of canvas to origin of line. + // To render line in a path-group, we need to translate them to + // distances from center of path-group to center of line. + var cp = this.getCenterPoint(), + offset = this.strokeWidth / 2; + ctx.translate( + cp.x - (this.strokeLineCap === 'butt' && this.height === 0 ? 0 : offset), + cp.y - (this.strokeLineCap === 'butt' && this.width === 0 ? 0 : offset) + ); + } + + if (!this.strokeDashArray || this.strokeDashArray && supportsLineDash) { + // move from center (of virtual box) to its left/top corner + // we can't assume x1, y1 is top left and x2, y2 is bottom right + var p = this.calcLinePoints(); + ctx.moveTo(p.x1, p.y1); + ctx.lineTo(p.x2, p.y2); + } + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = this.stroke || ctx.fillStyle; + this.stroke && this._renderStroke(ctx); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var p = this.calcLinePoints(); + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, p.x1, p.y1, p.x2, p.y2, this.strokeDashArray); + ctx.closePath(); + }, + + /** + * Returns object representation of an instance + * @methd toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); + }, + + /* + * Calculate object dimensions from its properties + * @private + */ + _getNonTransformedDimensions: function() { + var dim = this.callSuper('_getNonTransformedDimensions'); + if (this.strokeLineCap === 'butt') { + if (this.width === 0) { + dim.y -= this.strokeWidth; + } + if (this.height === 0) { + dim.x -= this.strokeWidth; + } + } + return dim; + }, + + /** + * Recalculates line points given width and height + * @private + */ + calcLinePoints: function() { + var xMult = this.x1 <= this.x2 ? -1 : 1, + yMult = this.y1 <= this.y2 ? -1 : 1, + x1 = (xMult * this.width * 0.5), + y1 = (yMult * this.height * 0.5), + x2 = (xMult * this.width * -0.5), + y2 = (yMult * this.height * -0.5); + + return { + x1: x1, + x2: x2, + y1: y1, + y2: y2 + }; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var markup = this._createBaseSVGMarkup(), + p = { x1: this.x1, x2: this.x2, y1: this.y1, y2: this.y2 }; + + if (!(this.group && this.group.type === 'path-group')) { + p = this.calcLinePoints(); + } + markup.push( + '\n' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) + * @static + * @memberOf fabric.Line + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @memberOf fabric.Line + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromElement = function(element, options) { + options = options || { }; + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES), + points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + options.originX = 'left'; + options.originY = 'top'; + return new fabric.Line(points, extend(parsedAttributes, options)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Line instance from an object representation + * @static + * @memberOf fabric.Line + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromObject = function(object, callback, forceAsync) { + function _callback(instance) { + delete instance.points; + callback && callback(instance); + }; + var options = clone(object, true); + options.points = [object.x1, object.y1, object.x2, object.y2]; + var line = fabric.Object._fromObject('Line', options, _callback, forceAsync, 'points'); + if (line) { + delete line.points; + } + return line; + }; + + /** + * Produces a function that calculates distance from canvas edge to Line origin. + */ + function makeEdgeToOriginGetter(propertyNames, originValues) { + var origin = propertyNames.origin, + axis1 = propertyNames.axis1, + axis2 = propertyNames.axis2, + dimension = propertyNames.dimension, + nearest = originValues.nearest, + center = originValues.center, + farthest = originValues.farthest; + + return function() { + switch (this.get(origin)) { + case nearest: + return Math.min(this.get(axis1), this.get(axis2)); + case center: + return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); + case farthest: + return Math.max(this.get(axis1), this.get(axis2)); + } + }; + + } + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + pi = Math.PI, + extend = fabric.util.object.extend; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push( + 'radius' + ); + + /** + * Circle class + * @class fabric.Circle + * @extends fabric.Object + * @see {@link fabric.Circle#initialize} for constructor definition + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'circle', + + /** + * Radius of this circle + * @type Number + * @default + */ + radius: 0, + + /** + * Start angle of the circle, moving clockwise + * @type Number + * @default 0 + */ + startAngle: 0, + + /** + * End angle of the circle + * @type Number + * @default 2Pi + */ + endAngle: pi * 2, + + cacheProperties: cacheProperties, + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Circle} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this.set('radius', options && options.radius || 0); + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Circle} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + + if (key === 'radius') { + this.setRadius(value); + } + + return this; + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['radius', 'startAngle', 'endAngle'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var markup = this._createBaseSVGMarkup(), x = 0, y = 0, + angle = (this.endAngle - this.startAngle) % ( 2 * pi); + + if (angle === 0) { + if (this.group && this.group.type === 'path-group') { + x = this.left + this.radius; + y = this.top + this.radius; + } + markup.push( + '\n' + ); + } + else { + var startX = Math.cos(this.startAngle) * this.radius, + startY = Math.sin(this.startAngle) * this.radius, + endX = Math.cos(this.endAngle) * this.radius, + endY = Math.sin(this.endAngle) * this.radius, + largeFlag = angle > pi ? '1' : '0'; + + markup.push( + '\n' + ); + } + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + ctx.arc(noTransform ? this.left + this.radius : 0, + noTransform ? this.top + this.radius : 0, + this.radius, + this.startAngle, + this.endAngle, false); + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @return {fabric.Circle} thisArg + */ + setRadius: function(value) { + this.radius = value; + return this.set('width', value * 2).set('height', value * 2); + }, + }); + + /* _FROM_SVG_START_ */ + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @memberOf fabric.Circle + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @static + * @memberOf fabric.Circle + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @throws {Error} If value of `r` attribute is missing or invalid + * @return {fabric.Circle} Instance of fabric.Circle + */ + fabric.Circle.fromElement = function(element, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); + + if (!isValidRadius(parsedAttributes)) { + throw new Error('value of `r` attribute is required and can not be negative'); + } + + parsedAttributes.left = parsedAttributes.left || 0; + parsedAttributes.top = parsedAttributes.top || 0; + + var obj = new fabric.Circle(extend(parsedAttributes, options)); + + obj.left -= obj.radius; + obj.top -= obj.radius; + return obj; + }; + + /** + * @private + */ + function isValidRadius(attributes) { + return (('radius' in attributes) && (attributes.radius >= 0)); + } + /* _FROM_SVG_END_ */ + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @static + * @memberOf fabric.Circle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {Object} Instance of fabric.Circle + */ + fabric.Circle.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Circle', object, callback, forceAsync); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + 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 + * @see {@link fabric.Triangle#initialize} for constructor definition + */ + 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) { + this.callSuper('initialize', options); + this.set('width', options && options.width || 100) + .set('height', options && options.height || 100); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx 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 {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, -widthBy2, heightBy2, 0, -heightBy2, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, 0, -heightBy2, widthBy2, heightBy2, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, widthBy2, heightBy2, -widthBy2, heightBy2, this.strokeDashArray); + ctx.closePath(); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var markup = this._createBaseSVGMarkup(), + widthBy2 = this.width / 2, + heightBy2 = this.height / 2, + points = [ + -widthBy2 + ' ' + heightBy2, + '0 ' + -heightBy2, + widthBy2 + ' ' + heightBy2 + ] + .join(','); + + markup.push( + '' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + }); + + /** + * Returns {@link fabric.Triangle} instance from an object representation + * @static + * @memberOf fabric.Triangle + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as first argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Triangle} + */ + fabric.Triangle.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Triangle', object, callback, forceAsync); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + 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; + } + + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push( + 'rx', + 'ry' + ); + + /** + * Ellipse class + * @class fabric.Ellipse + * @extends fabric.Object + * @return {fabric.Ellipse} thisArg + * @see {@link fabric.Ellipse#initialize} for constructor definition + */ + 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, + + cacheProperties: cacheProperties, + + /** + * Constructor + * @param {Object} [options] Options object + * @return {fabric.Ellipse} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this.set('rx', options && options.rx || 0); + this.set('ry', options && options.ry || 0); + }, + + /** + * @private + * @param {String} key + * @param {*} value + * @return {fabric.Ellipse} thisArg + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + switch (key) { + + case 'rx': + this.rx = value; + this.set('width', value * 2); + break; + + case 'ry': + this.ry = value; + this.set('height', value * 2); + break; + + } + return this; + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRx: function() { + return this.get('rx') * this.get('scaleX'); + }, + + /** + * Returns Vertical radius of an object (according to how an object is scaled) + * @return {Number} + */ + getRy: function() { + return this.get('ry') * this.get('scaleY'); + }, + + /** + * 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 this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var markup = this._createBaseSVGMarkup(), x = 0, y = 0; + if (this.group && this.group.type === 'path-group') { + x = this.left + this.rx; + y = this.top + this.ry; + } + markup.push( + '\n' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + ctx.save(); + ctx.transform(1, 0, 0, this.ry / this.rx, 0, 0); + ctx.arc( + noTransform ? this.left + this.rx : 0, + noTransform ? (this.top + this.ry) * this.rx / this.ry : 0, + this.rx, + 0, + piBy2, + false); + ctx.restore(); + this._renderFill(ctx); + this._renderStroke(ctx); + }, + }); + + /* _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); + + parsedAttributes.left = parsedAttributes.left || 0; + parsedAttributes.top = parsedAttributes.top || 0; + + var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); + + ellipse.top -= ellipse.ry; + ellipse.left -= ellipse.rx; + return ellipse; + }; + /* _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 + * @param {function} [callback] invoked with new instance as first argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Ellipse', object, callback, forceAsync); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.Rect) { + fabric.warn('fabric.Rect is already defined'); + return; + } + + var stateProperties = fabric.Object.prototype.stateProperties.concat(); + stateProperties.push('rx', 'ry'); + + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push('rx', 'ry'); + + /** + * Rectangle class + * @class fabric.Rect + * @extends fabric.Object + * @return {fabric.Rect} thisArg + * @see {@link fabric.Rect#initialize} for constructor definition + */ + 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, + + cacheProperties: cacheProperties, + + /** + * Constructor + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(options) { + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * 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 {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + _render: function(ctx, noTransform) { + + // optimize 1x1 case (used in spray brush) + if (this.width === 1 && this.height === 1) { + ctx.fillRect(-0.5, -0.5, 1, 1); + return; + } + + var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, + ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, + w = this.width, + h = this.height, + x = noTransform ? this.left : -this.width / 2, + y = noTransform ? this.top : -this.height / 2, + isRounded = rx !== 0 || ry !== 0, + /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ + k = 1 - 0.5522847498; + ctx.beginPath(); + + ctx.moveTo(x + rx, y); + + ctx.lineTo(x + w - rx, y); + isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); + + ctx.lineTo(x + w, y + h - ry); + isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h); + + ctx.lineTo(x + rx, y + h); + isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); + + ctx.lineTo(x, y + ry); + isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); + + ctx.closePath(); + + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var x = -this.width / 2, + y = -this.height / 2, + w = this.width, + h = this.height; + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y, x + w, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y + h, x, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x, y + h, x, y, this.strokeDashArray); + ctx.closePath(); + }, + + /** + * 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 this.callSuper('toObject', ['rx', 'ry'].concat(propertiesToInclude)); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var markup = this._createBaseSVGMarkup(), x = this.left, y = this.top; + if (!(this.group && this.group.type === 'path-group')) { + x = -this.width / 2; + y = -this.height / 2; + } + markup.push( + '\n'); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + }); + + /* _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(' ')); + + /** + * 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; + } + options = options || { }; + + var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); + + parsedAttributes.left = parsedAttributes.left || 0; + parsedAttributes.top = parsedAttributes.top || 0; + var rect = new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect.visible = rect.visible && rect.width > 0 && rect.height > 0; + return rect; + }; + /* _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 + * @param {Function} [callback] Callback to invoke when an fabric.Rect instance is created + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {Object} instance of fabric.Rect + */ + fabric.Rect.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Rect', object, callback, forceAsync); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed, + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push('points'); + + /** + * Polyline class + * @class fabric.Polyline + * @extends fabric.Object + * @see {@link fabric.Polyline#initialize} for constructor definition + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polyline', + + /** + * Points array + * @type Array + * @default + */ + points: null, + + /** + * Minimum X from points values, necessary to offset points + * @type Number + * @default + */ + minX: 0, + + /** + * Minimum Y from points values, necessary to offset points + * @type Number + * @default + */ + minY: 0, + + cacheProperties: cacheProperties, + + /** + * Constructor + * @param {Array} points Array of points (where each point is an object with x and y) + * @param {Object} [options] Options object + * @return {fabric.Polyline} thisArg + * @example + * var poly = new fabric.Polyline([ + * { x: 10, y: 10 }, + * { x: 50, y: 30 }, + * { x: 40, y: 70 }, + * { x: 60, y: 50 }, + * { x: 100, y: 150 }, + * { x: 40, y: 100 } + * ], { + * stroke: 'red', + * left: 100, + * top: 100 + * }); + */ + initialize: function(points, options) { + options = options || {}; + this.points = points || []; + this.callSuper('initialize', options); + this._calcDimensions(); + if (!('top' in options)) { + this.top = this.minY; + } + if (!('left' in options)) { + this.left = this.minX; + } + this.pathOffset = { + x: this.minX + this.width / 2, + y: this.minY + this.height / 2 + }; + }, + + /** + * @private + */ + _calcDimensions: function() { + + var points = this.points, + minX = min(points, 'x'), + minY = min(points, 'y'), + maxX = max(points, 'x'), + maxY = max(points, 'y'); + + this.width = (maxX - minX) || 0; + this.height = (maxY - minY) || 0; + this.minX = minX || 0; + this.minY = minY || 0; + }, + + /** + * 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() + }); + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var points = [], diffX, diffY, + markup = this._createBaseSVGMarkup(); + + if (!(this.group && this.group.type === 'path-group')) { + diffX = this.pathOffset.x; + diffY = this.pathOffset.y; + } + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push( + toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', + toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ' + ); + } + markup.push( + '<', this.type, ' ', this.getSvgId(), + 'points="', points.join(''), + '" style="', this.getSvgStyles(), + '" transform="', this.getSvgTransform(), + ' ', this.getSvgTransformMatrix(), + '"/>\n' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + commonRender: function(ctx, noTransform) { + var point, len = this.points.length, + x = noTransform ? 0 : this.pathOffset.x, + y = noTransform ? 0 : this.pathOffset.y; + + if (!len || isNaN(this.points[len - 1].y)) { + // do not draw if no points or odd points + // NaN comes from parseFloat of a empty string in parser + return false; + } + ctx.beginPath(); + ctx.moveTo(this.points[0].x - x, this.points[0].y - y); + for (var i = 0; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x - x, point.y - y); + } + return true; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + _render: function(ctx, noTransform) { + if (!this.commonRender(ctx, noTransform)) { + return; + } + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var p1, p2; + + ctx.beginPath(); + for (var i = 0, len = this.points.length; i < len; i++) { + p1 = this.points[i]; + p2 = this.points[i + 1] || p1; + fabric.util.drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, this.strokeDashArray); + } + }, + + /** + * 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); + + return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @memberOf fabric.Polyline + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Polyline} Instance of fabric.Polyline + */ + fabric.Polyline.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Polyline', object, callback, forceAsync, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * Polygon class + * @class fabric.Polygon + * @extends fabric.Polyline + * @see {@link fabric.Polygon#initialize} for constructor definition + */ + fabric.Polygon = fabric.util.createClass(fabric.Polyline, /** @lends fabric.Polygon.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'polygon', + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + _render: function(ctx, noTransform) { + if (!this.commonRender(ctx, noTransform)) { + return; + } + ctx.closePath(); + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + this.callSuper('_renderDashedStroke', ctx); + ctx.closePath(); + }, + }); + + /* _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); + + return new fabric.Polygon(points, extend(parsedAttributes, options)); + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @memberOf fabric.Polygon + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Path instance is created + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Polygon} Instance of fabric.Polygon + */ + fabric.Polygon.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Polygon', object, callback, forceAsync, 'points'); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + 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, + commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }, + repeatedCommands = { + m: 'l', + M: 'L' + }; + + if (fabric.Path) { + fabric.warn('fabric.Path is already defined'); + return; + } + + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push('path'); + + /** + * Path class + * @class fabric.Path + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} + * @see {@link fabric.Path#initialize} for constructor definition + */ + fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'path', + + /** + * Array of path points + * @type Array + * @default + */ + path: null, + + /** + * Minimum X from points values, necessary to offset points + * @type Number + * @default + */ + minX: 0, + + /** + * Minimum Y from points values, necessary to offset points + * @type Number + * @default + */ + minY: 0, + + cacheProperties: cacheProperties, + + /** + * 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 || { }; + + if (options) { + this.setOptions(options); + } + + if (!path) { + path = []; + } + + var fromArray = _toString.call(path) === '[object Array]'; + + this.path = fromArray + ? path + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + : path.match && path.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + if (!this.path) { + return; + } + + if (!fromArray) { + this.path = this._parsePath(); + } + + this._setPositionDimensions(options); + + if (this.objectCaching) { + this._createCacheCanvas(); + } + }, + + /** + * @private + * @param {Object} options Options object + */ + _setPositionDimensions: function(options) { + var calcDim = this._parseDimensions(); + + this.minX = calcDim.left; + this.minY = calcDim.top; + this.width = calcDim.width; + this.height = calcDim.height; + + if (typeof options.left === 'undefined') { + this.left = calcDim.left + (this.originX === 'center' + ? this.width / 2 + : this.originX === 'right' + ? this.width + : 0); + } + + if (typeof options.top === 'undefined') { + this.top = calcDim.top + (this.originY === 'center' + ? this.height / 2 + : this.originY === 'bottom' + ? this.height + : 0); + } + + this.pathOffset = this.pathOffset || { + x: this.minX + this.width / 2, + y: this.minY + this.height / 2 + }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _renderPathCommands: function(ctx) { + var current, // current instruction + previous = null, + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + tempX, + tempY, + l = -this.pathOffset.x, + t = -this.pathOffset.y; + + if (this.group && this.group.type === 'path-group') { + l = 0; + t = 0; + } + + ctx.beginPath(); + + for (var i = 0, len = this.path.length; i < len; ++i) { + + 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]; + subpathStartX = x; + subpathStartY = y; + ctx.moveTo(x + l, y + t); + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + ctx.moveTo(x + l, y + t); + break; + + case 'c': // bezierCurveTo, relative + tempX = x + current[5]; + tempY = y + current[6]; + controlX = x + current[3]; + controlY = y + current[4]; + ctx.bezierCurveTo( + x + current[1] + l, // x1 + y + current[2] + t, // y1 + controlX + l, // x2 + controlY + t, // y2 + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'C': // bezierCurveTo, absolute + x = current[5]; + y = current[6]; + 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]; + + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + ctx.bezierCurveTo( + controlX + l, + controlY + t, + x + current[1] + l, + y + current[2] + t, + tempX + l, + tempY + t + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = x + current[1]; + controlY = y + current[2]; + + x = tempX; + y = tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + ctx.bezierCurveTo( + controlX + l, + controlY + t, + current[1] + l, + current[2] + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = current[1]; + controlY = current[2]; + + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + controlX = x + current[1]; + controlY = y + current[2]; + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + + ctx.quadraticCurveTo( + current[1] + l, + current[2] + t, + tempX + l, + tempY + t + ); + 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 { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + + break; + + case 'T': + tempX = current[1]; + tempY = current[2]; + + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'a': + // TODO: optimize this + drawArc(ctx, x + l, y + t, [ + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + x + l, + current[7] + y + t + ]); + x += current[6]; + y += current[7]; + break; + + case 'A': + // TODO: optimize this + drawArc(ctx, x + l, y + t, [ + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + l, + current[7] + t + ]); + x = current[6]; + y = current[7]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + ctx.closePath(); + break; + } + previous = current; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx context to render path on + */ + _render: function(ctx) { + this._renderPathCommands(ctx); + this._renderFill(ctx); + this._renderStroke(ctx); + }, + + /** + * 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', ['sourcePath', 'pathOffset'].concat(propertiesToInclude)), { + path: this.path.map(function(item) { return item.slice(); }), + top: this.top, + left: this.left, + }); + 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; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var chunks = [], + markup = this._createBaseSVGMarkup(), addTransform = ''; + + for (var i = 0, len = this.path.length; i < len; i++) { + chunks.push(this.path[i].join(' ')); + } + var path = chunks.join(' '); + if (!(this.group && this.group.type === 'path-group')) { + addTransform = ' translate(' + (-this.pathOffset.x) + ', ' + (-this.pathOffset.y) + ') '; + } + markup.push( + '\n' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * 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+)|(\.\d+)))(?:e[-+]?\d+)?)/ig, + match, + coordsStr; + + for (var i = 0, coordsParsed, len = this.path.length; i < len; i++) { + currentPath = this.path[i]; + + coordsStr = currentPath.slice(1).trim(); + coords.length = 0; + + while ((match = re.exec(coordsStr))) { + coords.push(match[0]); + } + + coordsParsed = [currentPath.charAt(0)]; + + for (var j = 0, jlen = coords.length; j < jlen; j++) { + parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + coordsParsed.push(parsed); + } + } + + var command = coordsParsed[0], + commandLength = commandLengths[command.toLowerCase()], + repeatedCommand = repeatedCommands[command] || command; + + if (coordsParsed.length - 1 > commandLength) { + for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { + result.push([command].concat(coordsParsed.slice(k, k + commandLength))); + command = repeatedCommand; + } + } + else { + result.push(coordsParsed); + } + } + + return result; + }, + + /** + * @private + */ + _parseDimensions: function() { + + var aX = [], + aY = [], + current, // current instruction + previous = null, + subpathStartX = 0, + subpathStartY = 0, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + tempX, + tempY, + bounds; + + for (var i = 0, len = this.path.length; i < len; ++i) { + + current = this.path[i]; + + switch (current[0]) { // first letter + + case 'l': // lineto, relative + x += current[1]; + y += current[2]; + bounds = []; + break; + + case 'L': // lineto, absolute + x = current[1]; + y = current[2]; + bounds = []; + break; + + case 'h': // horizontal lineto, relative + x += current[1]; + bounds = []; + break; + + case 'H': // horizontal lineto, absolute + x = current[1]; + bounds = []; + break; + + case 'v': // vertical lineto, relative + y += current[1]; + bounds = []; + break; + + case 'V': // verical lineto, absolute + y = current[1]; + bounds = []; + break; + + case 'm': // moveTo, relative + x += current[1]; + y += current[2]; + subpathStartX = x; + subpathStartY = y; + bounds = []; + break; + + case 'M': // moveTo, absolute + x = current[1]; + y = current[2]; + subpathStartX = x; + subpathStartY = y; + bounds = []; + break; + + case 'c': // bezierCurveTo, relative + tempX = x + current[5]; + tempY = y + current[6]; + controlX = x + current[3]; + controlY = y + current[4]; + bounds = fabric.util.getBoundsOfCurve(x, y, + x + current[1], // x1 + y + current[2], // y1 + controlX, // x2 + controlY, // y2 + tempX, + tempY + ); + x = tempX; + y = tempY; + break; + + case 'C': // bezierCurveTo, absolute + controlX = current[3]; + controlY = current[4]; + bounds = fabric.util.getBoundsOfCurve(x, y, + current[1], + current[2], + controlX, + controlY, + current[5], + current[6] + ); + x = current[5]; + y = current[6]; + break; + + case 's': // shorthand cubic bezierCurveTo, relative + + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + x + current[1], + y + current[2], + tempX, + tempY + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = x + current[1]; + controlY = y + current[2]; + x = tempX; + y = tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + if (previous[0].match(/[CcSs]/) === null) { + // If there is no previous command or if the previous command was not a C, c, S, or s, + // the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + current[1], + current[2], + tempX, + tempY + ); + x = tempX; + y = tempY; + // set control point to 2nd one of this command + // "... the first control point is assumed to be + // the reflection of the second control point on + // the previous command relative to the current point." + controlX = current[1]; + controlY = current[2]; + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + controlX = x + current[1]; + controlY = y + current[2]; + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + tempX, + tempY + ); + x = tempX; + y = tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + controlX = current[1]; + controlY = current[2]; + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + current[3], + current[4] + ); + x = current[3]; + y = current[4]; + break; + + case 't': // shorthand quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[1]; + tempY = y + current[2]; + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + tempX, + tempY + ); + x = tempX; + y = tempY; + + break; + + case 'T': + tempX = current[1]; + tempY = current[2]; + + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else { + // calculate reflection of previous control point + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + bounds = fabric.util.getBoundsOfCurve(x, y, + controlX, + controlY, + controlX, + controlY, + tempX, + tempY + ); + x = tempX; + y = tempY; + break; + + case 'a': + // TODO: optimize this + bounds = fabric.util.getBoundsOfArc(x, y, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6] + x, + current[7] + y + ); + x += current[6]; + y += current[7]; + break; + + case 'A': + // TODO: optimize this + bounds = fabric.util.getBoundsOfArc(x, y, + current[1], + current[2], + current[3], + current[4], + current[5], + current[6], + current[7] + ); + x = current[6]; + y = current[7]; + break; + + case 'z': + case 'Z': + x = subpathStartX; + y = subpathStartY; + break; + } + previous = current; + bounds.forEach(function (point) { + aX.push(point.x); + aY.push(point.y); + }); + aX.push(x); + aY.push(y); + } + + var minX = min(aX) || 0, + minY = min(aY) || 0, + maxX = max(aX) || 0, + maxY = max(aY) || 0, + deltaX = maxX - minX, + deltaY = maxY - minY, + + o = { + left: minX, + top: minY, + 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 + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + */ + fabric.Path.fromObject = function(object, callback, forceAsync) { + // remove this pattern rom 2.0, accept just object. + var path; + if (typeof object.path === 'string') { + fabric.loadSVGFromURL(object.path, function (elements) { + var pathUrl = object.path; + path = elements[0]; + delete object.path; + + fabric.util.object.extend(path, object); + path.setSourcePath(pathUrl); + + callback && callback(path); + }); + } + else { + return fabric.Object._fromObject('Path', object, callback, forceAsync, 'path'); + } + }; + + /* _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) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend; + + if (fabric.PathGroup) { + fabric.warn('fabric.PathGroup is already defined'); + return; + } + + /** + * Path group class + * @class fabric.PathGroup + * @extends fabric.Path + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#path_and_pathgroup} + * @see {@link fabric.PathGroup#initialize} for constructor definition + */ + fabric.PathGroup = fabric.util.createClass(fabric.Object, /** @lends fabric.PathGroup.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'path-group', + + /** + * Fill value + * @type String + * @default + */ + fill: '', + + /** + * Constructor + * @param {Array} paths + * @param {Object} [options] Options object + * @return {fabric.PathGroup} thisArg + */ + initialize: function(paths, options) { + + options = options || { }; + this.paths = paths || []; + + for (var i = this.paths.length; i--;) { + this.paths[i].group = this; + } + + if (options.toBeParsed) { + this.parseDimensionsFromPaths(options); + delete options.toBeParsed; + } + this.setOptions(options); + this.setCoords(); + if (this.objectCaching) { + this._createCacheCanvas(); + } + }, + + /** + * Calculate width and height based on paths contained + */ + parseDimensionsFromPaths: function(options) { + var points, p, xC = [], yC = [], path, height, width, + m; + for (var j = this.paths.length; j--;) { + path = this.paths[j]; + height = path.height + path.strokeWidth; + width = path.width + path.strokeWidth; + points = [ + { x: path.left, y: path.top }, + { x: path.left + width, y: path.top }, + { x: path.left, y: path.top + height }, + { x: path.left + width, y: path.top + height } + ]; + m = this.paths[j].transformMatrix; + for (var i = 0; i < points.length; i++) { + p = points[i]; + if (m) { + p = fabric.util.transformPoint(p, m, false); + } + xC.push(p.x); + yC.push(p.y); + } + } + options.width = Math.max.apply(null, xC); + options.height = Math.max.apply(null, yC); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + drawObject: function(ctx) { + ctx.save(); + ctx.translate(-this.width / 2, -this.height / 2); + for (var i = 0, l = this.paths.length; i < l; ++i) { + this.paths[i].render(ctx, true); + } + ctx.restore(); + }, + + /** + * Decide if the object should cache or not. + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + var parentCache = this.objectCaching && (!this.group || this.needsItsOwnCache || !this.group.isCaching()); + this.caching = parentCache; + if (parentCache) { + for (var i = 0, len = this.paths.length; i < len; i++) { + if (this.paths[i].willDrawShadow()) { + this.caching = false; + return false; + } + } + } + return parentCache; + }, + + /** + * Check if this object or a child object will cast a shadow + * @return {Boolean} + */ + willDrawShadow: function() { + if (this.shadow) { + return true; + } + for (var i = 0, len = this.paths.length; i < len; i++) { + if (this.paths[i].willDrawShadow()) { + return true; + } + } + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isCaching: function() { + return this.caching || this.group && this.group.isCaching(); + }, + + /** + * Check if cache is dirty + */ + isCacheDirty: function() { + if (this.callSuper('isCacheDirty')) { + return true; + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this.paths.length; i < len; i++) { + if (this.paths[i].isCacheDirty(true)) { + var dim = this._getNonTransformedDimensions(); + this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); + return true; + } + } + return false; + }, + + /** + * Sets certain property to a certain value + * @param {String} prop + * @param {*} value + * @return {fabric.PathGroup} thisArg + */ + _set: function(prop, value) { + + if (prop === 'fill' && value && this.isSameColor()) { + var i = this.paths.length; + while (i--) { + this.paths[i]._set(prop, value); + } + } + + return this.callSuper('_set', prop, value); + }, + + /** + * Returns object representation of this path group + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + var pathsToObject = this.paths.map(function(path) { + var originalDefaults = path.includeDefaultValues; + path.includeDefaultValues = path.group.includeDefaultValues; + var obj = path.toObject(propertiesToInclude); + path.includeDefaultValues = originalDefaults; + return obj; + }); + var o = extend(this.callSuper('toObject', ['sourcePath'].concat(propertiesToInclude)), { + paths: pathsToObject + }); + return o; + }, + + /** + * Returns dataless object representation of this path group + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} dataless object representation of an instance + */ + toDatalessObject: function(propertiesToInclude) { + var o = this.toObject(propertiesToInclude); + if (this.sourcePath) { + o.paths = this.sourcePath; + } + return o; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var objects = this.getObjects(), + p = this.getPointByOrigin('left', 'top'), + translatePart = 'translate(' + p.x + ' ' + p.y + ')', + markup = this._createBaseSVGMarkup(); + markup.push( + '\n' + ); + + for (var i = 0, len = objects.length; i < len; i++) { + markup.push('\t', objects[i].toSVG(reviver)); + } + markup.push('\n'); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns a string representation of this path group + * @return {String} string representation of an object + */ + toString: function() { + return '#'; + }, + + /** + * Returns true if all paths in this group are of same color + * @return {Boolean} true if all paths are of the same color (`fill`) + */ + isSameColor: function() { + var firstPathFill = this.getObjects()[0].get('fill') || ''; + if (typeof firstPathFill !== 'string') { + return false; + } + firstPathFill = firstPathFill.toLowerCase(); + return this.getObjects().every(function(path) { + var pathFill = path.get('fill') || ''; + return typeof pathFill === 'string' && (pathFill).toLowerCase() === firstPathFill; + }); + }, + + /** + * Returns number representation of object's complexity + * @return {Number} complexity + */ + complexity: function() { + return this.paths.reduce(function(total, path) { + return total + ((path && path.complexity) ? path.complexity() : 0); + }, 0); + }, + + /** + * Returns all paths in this path group + * @return {Array} array of path objects included in this path group + */ + getObjects: function() { + return this.paths; + } + }); + + /** + * Creates fabric.PathGroup instance from an object representation + * @static + * @memberOf fabric.PathGroup + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.PathGroup instance is created + */ + fabric.PathGroup.fromObject = function(object, callback) { + var originalPaths = object.paths; + delete object.paths; + if (typeof originalPaths === 'string') { + fabric.loadSVGFromURL(originalPaths, function (elements) { + var pathUrl = originalPaths; + var pathGroup = fabric.util.groupSVGElements(elements, object, pathUrl); + object.paths = originalPaths; + callback(pathGroup); + }); + } + else { + fabric.util.enlivenObjects(originalPaths, function(enlivenedObjects) { + var pathGroup = new fabric.PathGroup(enlivenedObjects, object); + object.paths = originalPaths; + callback(pathGroup); + }); + } + }; + + /** + * Indicates that instances of this type are async + * @static + * @memberOf fabric.PathGroup + * @type Boolean + * @default + */ + fabric.PathGroup.async = true; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max; + + 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 + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#groups} + * @see {@link fabric.Group#initialize} for constructor definition + */ + fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'group', + + /** + * Width of stroke + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * Indicates if click events should also check for subtargets + * @type Boolean + * @default + */ + subTargetCheck: false, + + /** + * Constructor + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @param {Boolean} [isAlreadyGrouped] if true, objects have been grouped already. + * @return {Object} thisArg + */ + initialize: function(objects, options, isAlreadyGrouped) { + options = options || { }; + + this._objects = []; + // if objects enclosed in a group have been grouped already, + // we cannot change properties of objects. + // Thus we need to set options to group without objects, + // because delegatedProperties propagate to objects. + isAlreadyGrouped && this.callSuper('initialize', options); + + this._objects = objects || []; + for (var i = this._objects.length; i--; ) { + this._objects[i].group = this; + } + + if (options.originX) { + this.originX = options.originX; + } + if (options.originY) { + this.originY = options.originY; + } + + if (isAlreadyGrouped) { + // do not change coordinate of objects enclosed in a group, + // because objects coordinate system have been group coodinate system already. + this._updateObjectsCoords(true); + } + else { + this._calcBounds(); + this._updateObjectsCoords(); + this.callSuper('initialize', options); + } + + this.setCoords(); + this.saveCoords(); + }, + + /** + * @private + * @param {Boolean} [skipCoordsChange] if true, coordinates of objects enclosed in a group do not change + */ + _updateObjectsCoords: function(skipCoordsChange) { + var center = this.getCenterPoint(); + for (var i = this._objects.length; i--; ){ + this._updateObjectCoords(this._objects[i], center, skipCoordsChange); + } + }, + + /** + * @private + * @param {Object} object + * @param {fabric.Point} center, current center of group. + * @param {Boolean} [skipCoordsChange] if true, coordinates of object dose not change + */ + _updateObjectCoords: function(object, center, skipCoordsChange) { + // do not display corners of objects enclosed in a group + object.__origHasControls = object.hasControls; + object.hasControls = false; + + if (skipCoordsChange) { + return; + } + + var objectLeft = object.getLeft(), + objectTop = object.getTop(), + ignoreZoom = true, skipAbsolute = true; + + object.set({ + left: objectLeft - center.x, + top: objectTop - center.y + }); + object.setCoords(ignoreZoom, skipAbsolute); + }, + + /** + * Returns string represenation of a group + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * 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(); + fabric.util.resetObjectTransform(this); + if (object) { + this._objects.push(object); + object.group = this; + object._set('canvas', this.canvas); + } + // since _restoreObjectsState set objects inactive + this.forEachObject(this._setObjectActive, this); + this._calcBounds(); + this._updateObjectsCoords(); + this.dirty = true; + return this; + }, + + /** + * @private + */ + _setObjectActive: function(object) { + object.set('active', true); + object.group = this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + fabric.util.resetObjectTransform(this); + // since _restoreObjectsState set objects inactive + this.forEachObject(this._setObjectActive, this); + + this.remove(object); + this._calcBounds(); + this._updateObjectsCoords(); + this.dirty = true; + return this; + }, + + /** + * @private + */ + _onObjectAdded: function(object) { + this.dirty = true; + object.group = this; + object._set('canvas', this.canvas); + }, + + /** + * @private + */ + _onObjectRemoved: function(object) { + this.dirty = true; + delete object.group; + object.set('active', false); + }, + + /** + * Properties that are delegated to group objects when reading/writing + * @param {Object} delegatedProperties + */ + delegatedProperties: { + fill: true, + stroke: true, + strokeWidth: true, + fontFamily: true, + fontWeight: true, + fontSize: true, + fontStyle: true, + lineHeight: true, + textDecoration: true, + textAlign: true, + backgroundColor: true + }, + + /** + * @private + */ + _set: function(key, value) { + var i = this._objects.length; + + if (this.delegatedProperties[key] || key === 'canvas') { + while (i--) { + this._objects[i].set(key, value); + } + } + else { + while (i--) { + this._objects[i].setOnGroup(key, value); + } + } + + this.callSuper('_set', key, value); + }, + + /** + * 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 objsToObject = this.getObjects().map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = obj.group.includeDefaultValues; + var _obj = obj.toObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + return extend(this.callSuper('toObject', propertiesToInclude), { + objects: objsToObject + }); + }, + + /** + * Returns object representation of an instance, in dataless mode. + * @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 objsToObject = this.getObjects().map(function(obj) { + var originalDefaults = obj.includeDefaultValues; + obj.includeDefaultValues = obj.group.includeDefaultValues; + var _obj = obj.toDatalessObject(propertiesToInclude); + obj.includeDefaultValues = originalDefaults; + return _obj; + }); + return extend(this.callSuper('toDatalessObject', propertiesToInclude), { + objects: objsToObject + }); + }, + + /** + * Renders instance on a given context + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx) { + this._transformDone = true; + this.callSuper('render', ctx); + this._transformDone = false; + }, + + /** + * Decide if the object should cache or not. + * objectCaching is a global flag, wins over everything + * needsItsOwnCache should be used when the object drawing method requires + * a cache step. None of the fabric classes requires it. + * Generally you do not cache objects in groups because the group outside is cached. + * @return {Boolean} + */ + shouldCache: function() { + var parentCache = this.objectCaching && (!this.group || this.needsItsOwnCache || !this.group.isCaching()); + this.caching = parentCache; + if (parentCache) { + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + this.caching = false; + return false; + } + } + } + return parentCache; + }, + + /** + * Check if this object or a child object will cast a shadow + * @return {Boolean} + */ + willDrawShadow: function() { + if (this.shadow) { + return true; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].willDrawShadow()) { + return true; + } + } + return false; + }, + + /** + * Check if this group or its parent group are caching, recursively up + * @return {Boolean} + */ + isCaching: function() { + return this.caching || this.group && this.group.isCaching(); + }, + + /** + * Execute the drawing operation for an object on a specified context + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + drawObject: function(ctx) { + for (var i = 0, len = this._objects.length; i < len; i++) { + this._renderObject(this._objects[i], ctx); + } + }, + + /** + * Check if cache is dirty + */ + isCacheDirty: function() { + if (this.callSuper('isCacheDirty')) { + return true; + } + if (!this.statefullCache) { + return false; + } + for (var i = 0, len = this._objects.length; i < len; i++) { + if (this._objects[i].isCacheDirty(true)) { + var dim = this._getNonTransformedDimensions(); + this._cacheContext.clearRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); + return true; + } + } + return false; + }, + + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + _renderControls: function(ctx, noTransform) { + ctx.save(); + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + this.callSuper('_renderControls', ctx, noTransform); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i]._renderControls(ctx); + } + ctx.restore(); + }, + + /** + * @private + */ + _renderObject: function(object, ctx) { + // do not render if object is not visible + if (!object.visible) { + return; + } + + var originalHasRotatingPoint = object.hasRotatingPoint; + object.hasRotatingPoint = false; + object.render(ctx); + object.hasRotatingPoint = originalHasRotatingPoint; + }, + + /** + * Retores original state of each of group objects (original state is that which was before group was created). + * @private + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + this._objects.forEach(this._restoreObjectState, this); + return this; + }, + + /** + * Realises the transform from this group onto the supplied object + * i.e. it tells you what would happen if the supplied object was in + * the group, and then the group was destroyed. It mutates the supplied + * object. + * @param {fabric.Object} object + * @return {fabric.Object} transformedObject + */ + realizeTransform: function(object) { + var matrix = object.calcTransformMatrix(), + options = fabric.util.qrDecompose(matrix), + center = new fabric.Point(options.translateX, options.translateY); + object.flipX = false; + object.flipY = false; + object.set('scaleX', options.scaleX); + object.set('scaleY', options.scaleY); + object.skewX = options.skewX; + object.skewY = options.skewY; + object.angle = options.angle; + object.setPositionByOrigin(center, 'center', 'center'); + return object; + }, + + /** + * Restores original state of a specified object in group + * @private + * @param {fabric.Object} object + * @return {fabric.Group} thisArg + */ + _restoreObjectState: function(object) { + this.realizeTransform(object); + object.setCoords(); + object.hasControls = object.__origHasControls; + delete object.__origHasControls; + object.set('active', false); + delete object.group; + + 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 objects inside group + * @return {fabric.Group} thisArg + * @chainable + */ + setObjectsCoords: function() { + var ignoreZoom = true, skipAbsolute = true; + this.forEachObject(function(object) { + object.setCoords(ignoreZoom, skipAbsolute); + }); + return this; + }, + + /** + * @private + */ + _calcBounds: function(onlyWidthHeight) { + var aX = [], + aY = [], + o, prop, + props = ['tr', 'br', 'bl', 'tl'], + i = 0, iLen = this._objects.length, + j, jLen = props.length, + ignoreZoom = true; + + for ( ; i < iLen; ++i) { + o = this._objects[i]; + o.setCoords(ignoreZoom); + for (j = 0; j < jLen; j++) { + prop = props[j]; + aX.push(o.oCoords[prop].x); + aY.push(o.oCoords[prop].y); + } + } + + this.set(this._getBounds(aX, aY, onlyWidthHeight)); + }, + + /** + * @private + */ + _getBounds: function(aX, aY, onlyWidthHeight) { + var minXY = new fabric.Point(min(aX), min(aY)), + maxXY = new fabric.Point(max(aX), max(aY)), + obj = { + width: (maxXY.x - minXY.x) || 0, + height: (maxXY.y - minXY.y) || 0 + }; + + if (!onlyWidthHeight) { + obj.left = minXY.x || 0; + obj.top = minXY.y || 0; + if (this.originX === 'center') { + obj.left += obj.width / 2; + } + if (this.originX === 'right') { + obj.left += obj.width; + } + if (this.originY === 'center') { + obj.top += obj.height / 2; + } + if (this.originY === 'bottom') { + obj.top += obj.height; + } + } + return obj; + }, + + /* _TO_SVG_START_ */ + /** + * Returns svg representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var markup = this._createBaseSVGMarkup(); + markup.push( + '\n' + ); + + for (var i = 0, len = this._objects.length; i < len; i++) { + markup.push('\t', this._objects[i].toSVG(reviver)); + } + + markup.push('\n'); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns requested property + * @param {String} prop Property to get + * @return {*} + */ + 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 {Function} [callback] Callback to invoke when an group instance is created + */ + fabric.Group.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.Group(enlivenedObjects, object, true)); + }); + }; + + /** + * 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) { + + 'use strict'; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + } + + var stateProperties = fabric.Object.prototype.stateProperties.concat(); + stateProperties.push( + 'alignX', + 'alignY', + 'meetOrSlice' + ); + + /** + * Image class + * @class fabric.Image + * @extends fabric.Object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#images} + * @see {@link fabric.Image#initialize} for constructor definition + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'image', + + /** + * crossOrigin value (one of "", "anonymous", "use-credentials") + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + * @type String + * @default + */ + crossOrigin: '', + + /** + * AlignX value, part of preserveAspectRatio (one of "none", "mid", "min", "max") + * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + * This parameter defines how the picture is aligned to its viewport when image element width differs from image width. + * @type String + * @default + */ + alignX: 'none', + + /** + * AlignY value, part of preserveAspectRatio (one of "none", "mid", "min", "max") + * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + * This parameter defines how the picture is aligned to its viewport when image element height differs from image height. + * @type String + * @default + */ + alignY: 'none', + + /** + * meetOrSlice value, part of preserveAspectRatio (one of "meet", "slice"). + * if meet the image is always fully visibile, if slice the viewport is always filled with image. + * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + * @type String + * @default + */ + meetOrSlice: 'meet', + + /** + * Width of a stroke. + * For image quality a stroke multiple of 2 gives better results. + * @type Number + * @default + */ + strokeWidth: 0, + + /** + * private + * contains last value of scaleX to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleX: 1, + + /** + * private + * contains last value of scaleY to detect + * if the Image got resized after the last Render + * @type Number + */ + _lastScaleY: 1, + + /** + * minimum scale factor under which any resizeFilter is triggered to resize the image + * 0 will disable the automatic resize. 1 will trigger automatically always. + * number bigger than 1 can be used in case we want to scale with some filter above + * the natural image dimensions + * @type Number + */ + minimumScaleTrigger: 0.5, + + /** + * 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 `true`, object is cached on an additional canvas. + * default to false for images + * since 1.7.0 + * @type Boolean + * @default + */ + objectCaching: false, + + /** + * Constructor + * @param {HTMLImageElement | String} element Image element + * @param {Object} [options] Options object + * @param {function} [callback] callback function to call after eventual filters applied. + * @return {fabric.Image} thisArg + */ + initialize: function(element, options, callback) { + options || (options = { }); + this.filters = []; + this.resizeFilters = []; + this.callSuper('initialize', options); + this._initElement(element, options, callback); + }, + + /** + * 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 + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element, callback, options) { + + var _callback, _this; + + this._element = element; + this._originalElement = element; + this._initConfig(options); + + if (this.resizeFilters.length === 0) { + _callback = callback; + } + else { + _this = this; + _callback = function() { + _this.applyFilters(callback, _this.resizeFilters, _this._filteredEl || _this._originalElement, true); + }; + } + + if (this.filters.length !== 0) { + this.applyFilters(_callback); + } + else if (_callback) { + _callback(this); + } + + return this; + }, + + /** + * Sets crossOrigin value (on an instance and corresponding image element) + * @return {fabric.Image} thisArg + * @chainable + */ + setCrossOrigin: function(value) { + this.crossOrigin = value; + this._element.crossOrigin = value; + + 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 + }; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _stroke: function(ctx) { + if (!this.stroke || this.strokeWidth === 0) { + return; + } + var w = this.width / 2, h = this.height / 2; + ctx.beginPath(); + ctx.moveTo(-w, -h); + ctx.lineTo(w, -h); + ctx.lineTo(w, h); + ctx.lineTo(-w, h); + ctx.lineTo(-w, -h); + ctx.closePath(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderDashedStroke: function(ctx) { + var x = -this.width / 2, + y = -this.height / 2, + w = this.width, + h = this.height; + + ctx.save(); + this._setStrokeStyles(ctx); + + ctx.beginPath(); + fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y, x + w, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x + w, y + h, x, y + h, this.strokeDashArray); + fabric.util.drawDashedLine(ctx, x, y + h, x, y, this.strokeDashArray); + ctx.closePath(); + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} Object representation of an instance + */ + toObject: function(propertiesToInclude) { + var filters = [], resizeFilters = [], + scaleX = 1, scaleY = 1; + + this.filters.forEach(function(filterObj) { + if (filterObj) { + if (filterObj.type === 'Resize') { + scaleX *= filterObj.scaleX; + scaleY *= filterObj.scaleY; + } + filters.push(filterObj.toObject()); + } + }); + + this.resizeFilters.forEach(function(filterObj) { + filterObj && resizeFilters.push(filterObj.toObject()); + }); + var object = extend( + this.callSuper( + 'toObject', + ['crossOrigin', 'alignX', 'alignY', 'meetOrSlice'].concat(propertiesToInclude) + ), { + src: this.getSrc(), + filters: filters, + resizeFilters: resizeFilters, + }); + + object.width /= scaleX; + object.height /= scaleY; + + return object; + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + var markup = this._createBaseSVGMarkup(), x = -this.width / 2, y = -this.height / 2, + preserveAspectRatio = 'none', filtered = true; + if (this.group && this.group.type === 'path-group') { + x = this.left; + y = this.top; + } + if (this.alignX !== 'none' && this.alignY !== 'none') { + preserveAspectRatio = 'x' + this.alignX + 'Y' + this.alignY + ' ' + this.meetOrSlice; + } + markup.push( + '\n', + '\n' + ); + + if (this.stroke || this.strokeDashArray) { + var origFill = this.fill; + this.fill = null; + markup.push( + '\n' + ); + this.fill = origFill; + } + + markup.push('\n'); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + /* _TO_SVG_END_ */ + + /** + * Returns source of an image + * @param {Boolean} filtered indicates if the src is needed for svg + * @return {String} Source of an image + */ + getSrc: function(filtered) { + var element = filtered ? this._element : this._originalElement; + if (element) { + return fabric.isLikelyNode ? element._src : element.src; + } + else { + return this.src || ''; + } + }, + + /** + * Sets source of an image + * @param {String} src Source string (URL) + * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) + * @param {Object} [options] Options object + * @return {fabric.Image} thisArg + * @chainable + */ + setSrc: function(src, callback, options) { + fabric.util.loadImage(src, function(img) { + return this.setElement(img, callback, options); + }, this, options && options.crossOrigin); + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Applies filters assigned to this image (from "filters" array) + * @method applyFilters + * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated + * @param {Array} filters to be applied + * @param {fabric.Image} imgElement image to filter ( default to this._element ) + * @param {Boolean} forResizing + * @return {CanvasElement} canvasEl to be drawn immediately + * @chainable + */ + applyFilters: function(callback, filters, imgElement, forResizing) { + + filters = filters || this.filters; + imgElement = imgElement || this._originalElement; + + if (!imgElement) { + return; + } + + var replacement = fabric.util.createImage(), + retinaScaling = this.canvas ? this.canvas.getRetinaScaling() : fabric.devicePixelRatio, + minimumScale = this.minimumScaleTrigger / retinaScaling, + _this = this, scaleX, scaleY; + + if (filters.length === 0) { + this._element = imgElement; + callback && callback(this); + return imgElement; + } + + var canvasEl = fabric.util.createCanvasElement(); + canvasEl.width = imgElement.width; + canvasEl.height = imgElement.height; + canvasEl.getContext('2d').drawImage(imgElement, 0, 0, imgElement.width, imgElement.height); + + filters.forEach(function(filter) { + if (!filter) { + return; + } + if (forResizing) { + scaleX = _this.scaleX < minimumScale ? _this.scaleX : 1; + scaleY = _this.scaleY < minimumScale ? _this.scaleY : 1; + if (scaleX * retinaScaling < 1) { + scaleX *= retinaScaling; + } + if (scaleY * retinaScaling < 1) { + scaleY *= retinaScaling; + } + } + else { + scaleX = filter.scaleX; + scaleY = filter.scaleY; + } + filter.applyTo(canvasEl, scaleX, scaleY); + if (!forResizing && filter.type === 'Resize') { + _this.width *= filter.scaleX; + _this.height *= filter.scaleY; + } + }); + + /** @ignore */ + replacement.width = canvasEl.width; + replacement.height = canvasEl.height; + if (fabric.isLikelyNode) { + replacement.src = canvasEl.toBuffer(undefined, fabric.Image.pngCompression); + // onload doesn't fire in some node versions, so we invoke callback manually + _this._element = replacement; + !forResizing && (_this._filteredEl = replacement); + callback && callback(_this); + } + else { + replacement.onload = function() { + _this._element = replacement; + !forResizing && (_this._filteredEl = replacement); + callback && callback(_this); + replacement.onload = canvasEl = null; + }; + replacement.src = canvasEl.toDataURL('image/png'); + } + return canvasEl; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + _render: function(ctx, noTransform) { + var x, y, imageMargins = this._findMargins(), elementToDraw; + + x = (noTransform ? this.left : -this.width / 2); + y = (noTransform ? this.top : -this.height / 2); + + if (this.meetOrSlice === 'slice') { + ctx.beginPath(); + ctx.rect(x, y, this.width, this.height); + ctx.clip(); + } + + if (this.isMoving === false && this.resizeFilters.length && this._needsResize()) { + this._lastScaleX = this.scaleX; + this._lastScaleY = this.scaleY; + elementToDraw = this.applyFilters(null, this.resizeFilters, this._filteredEl || this._originalElement, true); + } + else { + elementToDraw = this._element; + } + elementToDraw && ctx.drawImage(elementToDraw, + x + imageMargins.marginX, + y + imageMargins.marginY, + imageMargins.width, + imageMargins.height + ); + + this._stroke(ctx); + this._renderStroke(ctx); + }, + + /** + * @private, needed to check if image needs resize + */ + _needsResize: function() { + return (this.scaleX !== this._lastScaleX || this.scaleY !== this._lastScaleY); + }, + + /** + * @private + */ + _findMargins: function() { + var width = this.width, height = this.height, scales, + scale, marginX = 0, marginY = 0; + + if (this.alignX !== 'none' || this.alignY !== 'none') { + scales = [this.width / this._element.width, this.height / this._element.height]; + scale = this.meetOrSlice === 'meet' + ? Math.min.apply(null, scales) : Math.max.apply(null, scales); + width = this._element.width * scale; + height = this._element.height * scale; + if (this.alignX === 'Mid') { + marginX = (this.width - width) / 2; + } + if (this.alignX === 'Max') { + marginX = this.width - width; + } + if (this.alignY === 'Mid') { + marginY = (this.height - height) / 2; + } + if (this.alignY === 'Max') { + marginY = this.height - height; + } + } + return { + width: width, + height: height, + marginX: marginX, + marginY: marginY + }; + }, + + /** + * @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 + * @param {Object} [options] Options object + */ + _initElement: function(element, options, callback) { + this.setElement(fabric.util.getById(element), callback, options); + 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); + if (this._element && this.crossOrigin) { + this._element.crossOrigin = this.crossOrigin; + } + }, + + /** + * @private + * @param {Array} filters to be initialized + * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created + */ + _initFilters: function(filters, callback) { + if (filters && filters.length) { + fabric.util.enlivenObjects(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() + ? this.getElement().width || 0 + : 0); + + this.height = 'height' in options + ? options.height + : (this.getElement() + ? this.getElement().height || 0 + : 0); + }, + }); + + /** + * 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) { + fabric.util.loadImage(object.src, function(img, error) { + if (error) { + callback && callback(null, error); + return; + } + fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) { + object.filters = filters || []; + fabric.Image.prototype._initFilters.call(object, object.resizeFilters, function(resizeFilters) { + object.resizeFilters = resizeFilters || []; + return new fabric.Image(img, object, callback); + }); + }); + }, null, object.crossOrigin); + }; + + /** + * 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 && callback(new fabric.Image(img, imgOptions)); + }, null, imgOptions && imgOptions.crossOrigin); + }; + + /* _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 preserveAspectRatio xlink:href crossOrigin'.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), + preserveAR; + + if (parsedAttributes.preserveAspectRatio) { + preserveAR = fabric.util.parsePreserveAspectRatioAttribute(parsedAttributes.preserveAspectRatio); + extend(parsedAttributes, preserveAR); + } + + 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 + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#image_filters} + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + */ +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', + + /** + * Constructor + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * Sets filter's properties from options + * @param {Object} [options] Options object + */ + setOptions: function(options) { + for (var prop in options) { + this[prop] = options[prop]; + } + }, + + /** + * 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(); + } +}); + +fabric.Image.filters.BaseFilter.fromObject = function(object, callback) { + var filter = new fabric.Image.filters[object.type](object); + callback && callback(filter); + return filter; +}; + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Brightness filter class + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Brightness({ + * brightness: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Brightness = createClass(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 + * @param {Number} [options.brightness=0] Value to brighten the image up (-255..255) + */ + initialize: function(options) { + options = options || { }; + this.brightness = options.brightness || 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, + 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 + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness + */ + fabric.Image.filters.Brightness.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Adapted from html5rocks article + * @class fabric.Image.filters.Convolute + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example Sharpen filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 0, -1, 0, + * -1, 5, -1, + * 0, -1, 0 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Blur filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9, + * 1/9, 1/9, 1/9 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter + * var filter = new fabric.Image.filters.Convolute({ + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Emboss filter with opaqueness + * var filter = new fabric.Image.filters.Convolute({ + * opaque: true, + * matrix: [ 1, 1, 1, + * 1, 0.7, -1, + * -1, -1, -1 ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Convolute = createClass(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 + * @param {Boolean} [options.opaque=false] Opaque value (true/false) + * @param {Array} [options.matrix] Filter matrix + */ + initialize: function(options) { + options = options || { }; + + this.opaque = options.opaque; + this.matrix = options.matrix || [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ]; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + + var weights = this.matrix, + context = canvasEl.getContext('2d'), + pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + + side = Math.round(Math.sqrt(weights.length)), + halfSide = Math.floor(side / 2), + src = pixels.data, + sw = pixels.width, + sh = pixels.height, + output = context.createImageData(sw, sh), + dst = output.data, + // go through the destination image pixels + alphaFac = this.opaque ? 1 : 0, + r, g, b, a, dstOff, + scx, scy, srcOff, wt; + + for (var y = 0; y < sh; y++) { + for (var x = 0; x < sw; x++) { + dstOff = (y * sw + x) * 4; + // calculate the weighed sum of the source image pixels that + // fall under the convolution matrix + r = 0; g = 0; b = 0; a = 0; + + for (var cy = 0; cy < side; cy++) { + for (var cx = 0; cx < side; cx++) { + scy = y + cy - halfSide; + scx = x + cx - halfSide; + + // eslint-disable-next-line max-depth + if (scy < 0 || scy > sh || scx < 0 || scx > sw) { + continue; + } + + srcOff = (scy * sw + scx) * 4; + wt = weights[cy * side + cx]; + + r += src[srcOff] * wt; + g += src[srcOff + 1] * wt; + b += src[srcOff + 2] * wt; + a += src[srcOff + 3] * wt; + } + } + 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 + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute + */ + fabric.Image.filters.Convolute.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * GradientTransparency filter class + * @class fabric.Image.filters.GradientTransparency + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.GradientTransparency#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.GradientTransparency({ + * threshold: 200 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + // eslint-disable-next-line max-len + filters.GradientTransparency = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'GradientTransparency', + + /** + * Constructor + * @memberOf fabric.Image.filters.GradientTransparency.prototype + * @param {Object} [options] Options object + * @param {Number} [options.threshold=100] Threshold value + */ + initialize: function(options) { + options = options || { }; + this.threshold = options.threshold || 100; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + total = data.length; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = threshold + 255 * (total - i) / total; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + threshold: this.threshold + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.GradientTransparency} Instance of fabric.Image.filters.GradientTransparency + */ + fabric.Image.filters.GradientTransparency.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Grayscale image filter class + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Grayscale(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Grayscale = createClass(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 + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale + */ + fabric.Image.filters.Grayscale.fromObject = function(object, callback) { + object = object || { }; + object.type = 'Grayscale'; + return fabric.Image.filters.BaseFilter.fromObject(object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Invert filter class + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Invert(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Invert = createClass(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 + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert + */ + fabric.Image.filters.Invert.fromObject = function(object, callback) { + object = object || { }; + object.type = 'Invert'; + return fabric.Image.filters.BaseFilter.fromObject(object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Mask filter class + * See http://resources.aleph-1.com/mask/ + * @class fabric.Image.filters.Mask + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Mask#initialize} for constructor definition + */ + filters.Mask = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Mask', + + /** + * Constructor + * @memberOf fabric.Image.filters.Mask.prototype + * @param {Object} [options] Options object + * @param {fabric.Image} [options.mask] Mask image object + * @param {Number} [options.channel=0] Rgb channel (0, 1, 2 or 3) + */ + initialize: function(options) { + options = options || { }; + + this.mask = options.mask; + this.channel = [0, 1, 2, 3].indexOf(options.channel) > -1 ? options.channel : 0; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + if (!this.mask) { + return; + } + + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + maskEl = this.mask.getElement(), + maskCanvasEl = fabric.util.createCanvasElement(), + channel = this.channel, + i, + iLen = imageData.width * imageData.height * 4; + + maskCanvasEl.width = canvasEl.width; + maskCanvasEl.height = canvasEl.height; + + maskCanvasEl.getContext('2d').drawImage(maskEl, 0, 0, canvasEl.width, canvasEl.height); + + var maskImageData = maskCanvasEl.getContext('2d').getImageData(0, 0, canvasEl.width, canvasEl.height), + maskData = maskImageData.data; + + for (i = 0; i < iLen; i += 4) { + data[i + 3] = maskData[i + channel]; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + mask: this.mask.toObject(), + channel: this.channel + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when a mask filter instance is created + */ + fabric.Image.filters.Mask.fromObject = function(object, callback) { + fabric.util.loadImage(object.mask.src, function(img) { + object.mask = new fabric.Image(img, object.mask); + return fabric.Image.filters.BaseFilter.fromObject(object, callback); + }); + }; + + /** + * Indicates that instances of this type are async + * @static + * @type Boolean + * @default + */ + fabric.Image.filters.Mask.async = true; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Noise filter class + * @class fabric.Image.filters.Noise + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Noise({ + * noise: 700 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Noise = createClass(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 + * @param {Number} [options.noise=0] Noise value + */ + initialize: function(options) { + options = options || { }; + this.noise = options.noise || 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, + 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 + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise + */ + fabric.Image.filters.Noise.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Pixelate filter class + * @class fabric.Image.filters.Pixelate + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Pixelate({ + * blocksize: 8 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Pixelate = createClass(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 + * @param {Number} [options.blocksize=4] Blocksize for pixelate + */ + 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 + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate + */ + fabric.Image.filters.Pixelate.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Remove white filter class + * @class fabric.Image.filters.RemoveWhite + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.RemoveWhite#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.RemoveWhite({ + * threshold: 40, + * distance: 140 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.RemoveWhite = createClass(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 + * @param {Number} [options.threshold=30] Threshold value + * @param {Number} [options.distance=20] Distance value + */ + initialize: function(options) { + options = options || { }; + this.threshold = options.threshold || 30; + this.distance = options.distance || 20; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + distance = this.distance, + limit = 255 - threshold, + abs = Math.abs, + r, g, b; + + 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] = 0; + } + } + + 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 + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.RemoveWhite} Instance of fabric.Image.filters.RemoveWhite + */ + fabric.Image.filters.RemoveWhite.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Sepia filter class + * @class fabric.Image.filters.Sepia + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Sepia = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Sepia', + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, avg; + + for (i = 0; i < iLen; i += 4) { + avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; + data[i] = avg + 100; + data[i + 1] = avg + 50; + data[i + 2] = avg + 255; + } + + context.putImageData(imageData, 0, 0); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Sepia} Instance of fabric.Image.filters.Sepia + */ + fabric.Image.filters.Sepia.fromObject = function(object, callback) { + object = object || { }; + object.type = 'Sepia'; + return new fabric.Image.filters.BaseFilter.fromObject(object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Sepia2 filter class + * @class fabric.Image.filters.Sepia2 + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Sepia2(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Sepia2 = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Sepia2', + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, r, g, b; + + for (i = 0; i < iLen; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; + data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; + data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; + } + + context.putImageData(imageData, 0, 0); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Sepia2} Instance of fabric.Image.filters.Sepia2 + */ + fabric.Image.filters.Sepia2.fromObject = function(object, callback) { + object = object || { }; + object.type = 'Sepia2'; + return new fabric.Image.filters.BaseFilter.fromObject(object, callback); + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Tint filter class + * Adapted from https://github.com/mezzoblue/PaintbrushJS + * @class fabric.Image.filters.Tint + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Tint#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example Tint filter with hex color and opacity + * var filter = new fabric.Image.filters.Tint({ + * color: '#3513B0', + * opacity: 0.5 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Tint filter with rgba color + * var filter = new fabric.Image.filters.Tint({ + * color: 'rgba(53, 21, 176, 0.5)' + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Tint = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Tint', + + /** + * Constructor + * @memberOf fabric.Image.filters.Tint.prototype + * @param {Object} [options] Options object + * @param {String} [options.color=#000000] Color to tint the image with + * @param {Number} [options.opacity] Opacity value that controls the tint effect's transparency (0..1) + */ + initialize: function(options) { + options = options || { }; + + this.color = options.color || '#000000'; + this.opacity = typeof options.opacity !== 'undefined' + ? options.opacity + : new fabric.Color(this.color).getAlpha(); + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, + tintR, tintG, tintB, + r, g, b, alpha1, + source; + + source = new fabric.Color(this.color).getSource(); + + tintR = source[0] * this.opacity; + tintG = source[1] * this.opacity; + tintB = source[2] * this.opacity; + + alpha1 = 1 - this.opacity; + + for (i = 0; i < iLen; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + // alpha compositing + data[i] = tintR + r * alpha1; + data[i + 1] = tintG + g * alpha1; + data[i + 2] = tintB + b * alpha1; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + color: this.color, + opacity: this.opacity + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Tint} Instance of fabric.Image.filters.Tint + */ + fabric.Image.filters.Tint.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Multiply filter class + * Adapted from http://www.laurenscorijn.com/articles/colormath-basics + * @class fabric.Image.filters.Multiply + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example Multiply filter with hex color + * var filter = new fabric.Image.filters.Multiply({ + * color: '#F0F' + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + * @example Multiply filter with rgb color + * var filter = new fabric.Image.filters.Multiply({ + * color: 'rgb(53, 21, 176)' + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Multiply = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Multiply.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Multiply', + + /** + * Constructor + * @memberOf fabric.Image.filters.Multiply.prototype + * @param {Object} [options] Options object + * @param {String} [options.color=#000000] Color to multiply the image pixels with + */ + initialize: function(options) { + options = options || { }; + + this.color = options.color || '#000000'; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, + source; + + source = new fabric.Color(this.color).getSource(); + + for (i = 0; i < iLen; i += 4) { + data[i] *= source[0] / 255; + data[i + 1] *= source[1] / 255; + data[i + 2] *= source[2] / 255; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + color: this.color + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Multiply} Instance of fabric.Image.filters.Multiply + */ + fabric.Image.filters.Multiply.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + 'use strict'; + + var fabric = global.fabric, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Blend filter class + * @class fabric.Image.filter.Blend + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @example + * var filter = new fabric.Image.filters.Blend({ + * color: '#000', + * mode: 'multiply' + * }); + * + * var filter = new fabric.Image.filters.Blend({ + * image: fabricImageObject, + * mode: 'multiply', + * alpha: 0.5 + * }); + + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + + filters.Blend = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Blend.prototype */ { + type: 'Blend', + + initialize: function(options) { + options = options || {}; + this.color = options.color || '#000'; + this.image = options.image || false; + this.mode = options.mode || 'multiply'; + this.alpha = options.alpha || 1; + }, + + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + tr, tg, tb, + r, g, b, + _r, _g, _b, + source, + isImage = false; + + if (this.image) { + // Blend images + isImage = true; + + var _el = fabric.util.createCanvasElement(); + _el.width = this.image.width; + _el.height = this.image.height; + + var tmpCanvas = new fabric.StaticCanvas(_el); + tmpCanvas.add(this.image); + var context2 = tmpCanvas.getContext('2d'); + source = context2.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height).data; + } + else { + // Blend color + source = new fabric.Color(this.color).getSource(); + + tr = source[0] * this.alpha; + tg = source[1] * this.alpha; + tb = source[2] * this.alpha; + } + + for (var i = 0, len = data.length; i < len; i += 4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + if (isImage) { + tr = source[i] * this.alpha; + tg = source[i + 1] * this.alpha; + tb = source[i + 2] * this.alpha; + } + + switch (this.mode) { + case 'multiply': + data[i] = r * tr / 255; + data[i + 1] = g * tg / 255; + data[i + 2] = b * tb / 255; + break; + case 'screen': + data[i] = 1 - (1 - r) * (1 - tr); + data[i + 1] = 1 - (1 - g) * (1 - tg); + data[i + 2] = 1 - (1 - b) * (1 - tb); + break; + case 'add': + data[i] = Math.min(255, r + tr); + data[i + 1] = Math.min(255, g + tg); + data[i + 2] = Math.min(255, b + tb); + break; + case 'diff': + case 'difference': + data[i] = Math.abs(r - tr); + data[i + 1] = Math.abs(g - tg); + data[i + 2] = Math.abs(b - tb); + break; + case 'subtract': + _r = r - tr; + _g = g - tg; + _b = b - tb; + + data[i] = (_r < 0) ? 0 : _r; + data[i + 1] = (_g < 0) ? 0 : _g; + data[i + 2] = (_b < 0) ? 0 : _b; + break; + case 'darken': + data[i] = Math.min(r, tr); + data[i + 1] = Math.min(g, tg); + data[i + 2] = Math.min(b, tb); + break; + case 'lighten': + data[i] = Math.max(r, tr); + data[i + 1] = Math.max(g, tg); + data[i + 2] = Math.max(b, tb); + break; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + color: this.color, + image: this.image, + mode: this.mode, + alpha: this.alpha + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Blend} Instance of fabric.Image.filters.Blend + */ + fabric.Image.filters.Blend.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor, + sqrt = Math.sqrt, abs = Math.abs, max = Math.max, round = Math.round, sin = Math.sin, + ceil = Math.ceil, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Resize image filter class + * @class fabric.Image.filters.Resize + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Resize(); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Resize = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Resize', + + /** + * Resize type + * @param {String} resizeType + * @default + */ + resizeType: 'hermite', + + /** + * Scale factor for resizing, x axis + * @param {Number} scaleX + * @default + */ + scaleX: 0, + + /** + * Scale factor for resizing, y axis + * @param {Number} scaleY + * @default + */ + scaleY: 0, + + /** + * LanczosLobes parameter for lanczos filter + * @param {Number} lanczosLobes + * @default + */ + lanczosLobes: 3, + + /** + * Applies filter to canvas element + * @memberOf fabric.Image.filters.Resize.prototype + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} scaleX + * @param {Number} scaleY + */ + applyTo: function(canvasEl, scaleX, scaleY) { + if (scaleX === 1 && scaleY === 1) { + return; + } + + this.rcpScaleX = 1 / scaleX; + this.rcpScaleY = 1 / scaleY; + + var oW = canvasEl.width, oH = canvasEl.height, + dW = round(oW * scaleX), dH = round(oH * scaleY), + imageData; + + if (this.resizeType === 'sliceHack') { + imageData = this.sliceByTwo(canvasEl, oW, oH, dW, dH); + } + if (this.resizeType === 'hermite') { + imageData = this.hermiteFastResize(canvasEl, oW, oH, dW, dH); + } + if (this.resizeType === 'bilinear') { + imageData = this.bilinearFiltering(canvasEl, oW, oH, dW, dH); + } + if (this.resizeType === 'lanczos') { + imageData = this.lanczosResize(canvasEl, oW, oH, dW, dH); + } + canvasEl.width = dW; + canvasEl.height = dH; + canvasEl.getContext('2d').putImageData(imageData, 0, 0); + }, + + /** + * Filter sliceByTwo + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + sliceByTwo: function(canvasEl, oW, oH, dW, dH) { + var context = canvasEl.getContext('2d'), imageData, + multW = 0.5, multH = 0.5, signW = 1, signH = 1, + doneW = false, doneH = false, stepW = oW, stepH = oH, + tmpCanvas = fabric.util.createCanvasElement(), + tmpCtx = tmpCanvas.getContext('2d'); + dW = floor(dW); + dH = floor(dH); + tmpCanvas.width = max(dW, oW); + tmpCanvas.height = max(dH, oH); + + if (dW > oW) { + multW = 2; + signW = -1; + } + if (dH > oH) { + multH = 2; + signH = -1; + } + imageData = context.getImageData(0, 0, oW, oH); + canvasEl.width = max(dW, oW); + canvasEl.height = max(dH, oH); + context.putImageData(imageData, 0, 0); + + while (!doneW || !doneH) { + oW = stepW; + oH = stepH; + if (dW * signW < floor(stepW * multW * signW)) { + stepW = floor(stepW * multW); + } + else { + stepW = dW; + doneW = true; + } + if (dH * signH < floor(stepH * multH * signH)) { + stepH = floor(stepH * multH); + } + else { + stepH = dH; + doneH = true; + } + imageData = context.getImageData(0, 0, oW, oH); + tmpCtx.putImageData(imageData, 0, 0); + context.clearRect(0, 0, stepW, stepH); + context.drawImage(tmpCanvas, 0, 0, oW, oH, 0, 0, stepW, stepH); + } + return context.getImageData(0, 0, dW, dH); + }, + + /** + * Filter lanczosResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + lanczosResize: function(canvasEl, oW, oH, dW, dH) { + + function lanczosCreate(lobes) { + return function(x) { + if (x > lobes) { + return 0; + } + x *= Math.PI; + if (abs(x) < 1e-16) { + return 1; + } + var xx = x / lobes; + return sin(x) * sin(xx) / x / xx; + }; + } + + function process(u) { + var v, i, weight, idx, a, red, green, + blue, alpha, fX, fY; + center.x = (u + 0.5) * ratioX; + icenter.x = floor(center.x); + for (v = 0; v < dH; v++) { + center.y = (v + 0.5) * ratioY; + icenter.y = floor(center.y); + a = 0; red = 0; green = 0; blue = 0; alpha = 0; + for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { + if (i < 0 || i >= oW) { + continue; + } + fX = floor(1000 * abs(i - center.x)); + if (!cacheLanc[fX]) { + cacheLanc[fX] = { }; + } + for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { + if (j < 0 || j >= oH) { + continue; + } + fY = floor(1000 * abs(j - center.y)); + if (!cacheLanc[fX][fY]) { + cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000); + } + weight = cacheLanc[fX][fY]; + if (weight > 0) { + idx = (j * oW + i) * 4; + a += weight; + red += weight * srcData[idx]; + green += weight * srcData[idx + 1]; + blue += weight * srcData[idx + 2]; + alpha += weight * srcData[idx + 3]; + } + } + } + idx = (v * dW + u) * 4; + destData[idx] = red / a; + destData[idx + 1] = green / a; + destData[idx + 2] = blue / a; + destData[idx + 3] = alpha / a; + } + + if (++u < dW) { + return process(u); + } + else { + return destImg; + } + } + + var context = canvasEl.getContext('2d'), + srcImg = context.getImageData(0, 0, oW, oH), + destImg = context.getImageData(0, 0, dW, dH), + srcData = srcImg.data, destData = destImg.data, + lanczos = lanczosCreate(this.lanczosLobes), + ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, + rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, + range2X = ceil(ratioX * this.lanczosLobes / 2), + range2Y = ceil(ratioY * this.lanczosLobes / 2), + cacheLanc = { }, center = { }, icenter = { }; + + return process(0); + }, + + /** + * bilinearFiltering + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + bilinearFiltering: function(canvasEl, oW, oH, dW, dH) { + var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl, + color, offset = 0, origPix, ratioX = this.rcpScaleX, + ratioY = this.rcpScaleY, context = canvasEl.getContext('2d'), + w4 = 4 * (oW - 1), img = context.getImageData(0, 0, oW, oH), + pixels = img.data, destImage = context.getImageData(0, 0, dW, dH), + destPixels = destImage.data; + for (i = 0; i < dH; i++) { + for (j = 0; j < dW; j++) { + x = floor(ratioX * j); + y = floor(ratioY * i); + xDiff = ratioX * j - x; + yDiff = ratioY * i - y; + origPix = 4 * (y * oW + x); + + for (chnl = 0; chnl < 4; chnl++) { + a = pixels[origPix + chnl]; + b = pixels[origPix + 4 + chnl]; + c = pixels[origPix + w4 + chnl]; + d = pixels[origPix + w4 + 4 + chnl]; + color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + + c * yDiff * (1 - xDiff) + d * xDiff * yDiff; + destPixels[offset++] = color; + } + } + } + return destImage; + }, + + /** + * hermiteFastResize + * @param {Object} canvasEl Canvas element to apply filter to + * @param {Number} oW Original Width + * @param {Number} oH Original Height + * @param {Number} dW Destination Width + * @param {Number} dH Destination Height + * @returns {ImageData} + */ + hermiteFastResize: function(canvasEl, oW, oH, dW, dH) { + var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, + ratioWHalf = ceil(ratioW / 2), + ratioHHalf = ceil(ratioH / 2), + context = canvasEl.getContext('2d'), + img = context.getImageData(0, 0, oW, oH), data = img.data, + img2 = context.getImageData(0, 0, dW, dH), data2 = img2.data; + for (var j = 0; j < dH; j++) { + for (var i = 0; i < dW; i++) { + var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0, + gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH; + for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { + var dy = abs(centerY - (yy + 0.5)) / ratioHHalf, + centerX = (i + 0.5) * ratioW, w0 = dy * dy; + for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { + var dx = abs(centerX - (xx + 0.5)) / ratioWHalf, + w = sqrt(w0 + dx * dx); + /* eslint-disable max-depth */ + if (w > 1 && w < -1) { + continue; + } + //hermite filter + weight = 2 * w * w * w - 3 * w * w + 1; + if (weight > 0) { + dx = 4 * (xx + yy * oW); + //alpha + gxA += weight * data[dx + 3]; + weightsAlpha += weight; + //colors + if (data[dx + 3] < 255) { + weight = weight * data[dx + 3] / 250; + } + gxR += weight * data[dx]; + gxG += weight * data[dx + 1]; + gxB += weight * data[dx + 2]; + weights += weight; + } + /* eslint-enable max-depth */ + } + } + data2[x2] = gxR / weights; + data2[x2 + 1] = gxG / weights; + data2[x2 + 2] = gxB / weights; + data2[x2 + 3] = gxA / weightsAlpha; + } + } + return img2; + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return { + type: this.type, + scaleX: this.scaleX, + scaleY: this.scaleY, + resizeType: this.resizeType, + lanczosLobes: this.lanczosLobes + }; + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize + */ + fabric.Image.filters.Resize.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Color Matrix filter class + * @class fabric.Image.filters.ColorMatrix + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.ColorMatrix#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @see {@Link http://www.webwasp.co.uk/tutorials/219/Color_Matrix_Filter.php} + * @see {@Link http://phoboslab.org/log/2013/11/fast-image-filters-with-webgl} + * @example Kodachrome filter + * var filter = new fabric.Image.filters.ColorMatrix({ + * matrix: [ + 1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502, + -0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203, + -0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946, + 0, 0, 0, 1, 0 + ] + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.ColorMatrix = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.ColorMatrix.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'ColorMatrix', + + /** + * Constructor + * @memberOf fabric.Image.filters.ColorMatrix.prototype + * @param {Object} [options] Options object + * @param {Array} [options.matrix] Color Matrix to modify the image data with + */ + initialize: function( options ) { + options || ( options = {} ); + this.matrix = options.matrix || [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0 + ]; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function( canvasEl ) { + var context = canvasEl.getContext( '2d' ), + imageData = context.getImageData( 0, 0, canvasEl.width, canvasEl.height ), + data = imageData.data, + iLen = data.length, + i, + r, + g, + b, + a, + m = this.matrix; + + for ( i = 0; i < iLen; i += 4 ) { + r = data[ i ]; + g = data[ i + 1 ]; + b = data[ i + 2 ]; + a = data[ i + 3 ]; + + data[ i ] = r * m[ 0 ] + g * m[ 1 ] + b * m[ 2 ] + a * m[ 3 ] + m[ 4 ]; + data[ i + 1 ] = r * m[ 5 ] + g * m[ 6 ] + b * m[ 7 ] + a * m[ 8 ] + m[ 9 ]; + data[ i + 2 ] = r * m[ 10 ] + g * m[ 11 ] + b * m[ 12 ] + a * m[ 13 ] + m[ 14 ]; + data[ i + 3 ] = r * m[ 15 ] + g * m[ 16 ] + b * m[ 17 ] + a * m[ 18 ] + m[ 19 ]; + } + + context.putImageData( imageData, 0, 0 ); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + type: this.type, + matrix: this.matrix + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] function to invoke after filter creation + * @return {fabric.Image.filters.ColorMatrix} Instance of fabric.Image.filters.ColorMatrix + */ + fabric.Image.filters.ColorMatrix.fromObject = fabric.Image.filters.BaseFilter.fromObject; +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Contrast filter class + * @class fabric.Image.filters.Contrast + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Contrast#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Contrast({ + * contrast: 40 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Contrast = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Contrast.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Contrast', + + /** + * Constructor + * @memberOf fabric.Image.filters.Contrast.prototype + * @param {Object} [options] Options object + * @param {Number} [options.contrast=0] Value to contrast the image up (-255...255) + */ + initialize: function(options) { + options = options || { }; + this.contrast = options.contrast || 0; + }, + + /** + * Applies filter to canvas element + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + contrastF = 259 * (this.contrast + 255) / (255 * (259 - this.contrast)); + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i] = contrastF * (data[i] - 128) + 128; + data[i + 1] = contrastF * (data[i + 1] - 128) + 128; + data[i + 2] = contrastF * (data[i + 2] - 128) + 128; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + contrast: this.contrast + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Contrast} Instance of fabric.Image.filters.Contrast + */ + fabric.Image.filters.Contrast.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + filters = fabric.Image.filters, + createClass = fabric.util.createClass; + + /** + * Saturate filter class + * @class fabric.Image.filters.Saturate + * @memberOf fabric.Image.filters + * @extends fabric.Image.filters.BaseFilter + * @see {@link fabric.Image.filters.Saturate#initialize} for constructor definition + * @see {@link http://fabricjs.com/image-filters|ImageFilters demo} + * @example + * var filter = new fabric.Image.filters.Saturate({ + * saturate: 100 + * }); + * object.filters.push(filter); + * object.applyFilters(canvas.renderAll.bind(canvas)); + */ + filters.Saturate = createClass(filters.BaseFilter, /** @lends fabric.Image.filters.Saturate.prototype */ { + + /** + * Filter type + * @param {String} type + * @default + */ + type: 'Saturate', + + /** + * Constructor + * @memberOf fabric.Image.filters.Saturate.prototype + * @param {Object} [options] Options object + * @param {Number} [options.saturate=0] Value to saturate the image (-100...100) + */ + initialize: function(options) { + options = options || { }; + this.saturate = options.saturate || 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, + max, adjust = -this.saturate * 0.01; + + for (var i = 0, len = data.length; i < len; i += 4) { + max = Math.max(data[i], data[i + 1], data[i + 2]); + data[i] += max !== data[i] ? (max - data[i]) * adjust : 0; + data[i + 1] += max !== data[i + 1] ? (max - data[i + 1]) * adjust : 0; + data[i + 2] += max !== data[i + 2] ? (max - data[i + 2]) * adjust : 0; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Returns object representation of an instance + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + saturate: this.saturate + }); + } + }); + + /** + * Returns filter instance from an object representation + * @static + * @param {Object} object Object to create an instance from + * @param {Function} [callback] to be invoked after filter creation + * @return {fabric.Image.filters.Saturate} Instance of fabric.Image.filters.Saturate + */ + fabric.Image.filters.Saturate.fromObject = fabric.Image.filters.BaseFilter.fromObject; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed, + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, + MIN_TEXT_WIDTH = 2; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + + var stateProperties = fabric.Object.prototype.stateProperties.concat(); + stateProperties.push( + 'fontFamily', + 'fontWeight', + 'fontSize', + 'text', + 'textDecoration', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'textBackgroundColor', + 'charSpacing' + ); + + var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); + cacheProperties.push( + 'fontFamily', + 'fontWeight', + 'fontSize', + 'text', + 'textDecoration', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'textBackgroundColor', + 'charSpacing', + 'styles' + ); + /** + * Text class + * @class fabric.Text + * @extends fabric.Object + * @return {fabric.Text} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#text} + * @see {@link fabric.Text#initialize} for constructor definition + */ + 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', + 'fontWeight', + 'fontFamily', + 'fontStyle', + 'lineHeight', + 'text', + 'charSpacing', + 'textAlign' + ], + + /** + * @private + */ + _reNewline: /\r?\n/, + + /** + * Use this regular expression to filter for whitespace that is not a new line. + * Mostly used when text is 'justify' aligned. + * @private + */ + _reSpacesAndTabs: /[ \t\r]+/g, + + /** + * Retrieves object's fontSize + * @method getFontSize + * @memberOf fabric.Text.prototype + * @return {String} Font size (in pixels) + */ + + /** + * Sets object's fontSize + * Does not update the object .width and .height, + * call ._initDimensions() to update the values. + * @method setFontSize + * @memberOf fabric.Text.prototype + * @param {Number} fontSize Font size (in pixels) + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's fontWeight + * @method getFontWeight + * @memberOf fabric.Text.prototype + * @return {(String|Number)} Font weight + */ + + /** + * Sets object's fontWeight + * Does not update the object .width and .height, + * call ._initDimensions() to update the values. + * @method setFontWeight + * @memberOf fabric.Text.prototype + * @param {(Number|String)} fontWeight Font weight + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's fontFamily + * @method getFontFamily + * @memberOf fabric.Text.prototype + * @return {String} Font family + */ + + /** + * Sets object's fontFamily + * Does not update the object .width and .height, + * call ._initDimensions() to update the values. + * @method setFontFamily + * @memberOf fabric.Text.prototype + * @param {String} fontFamily Font family + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's text + * @method getText + * @memberOf fabric.Text.prototype + * @return {String} text + */ + + /** + * Sets object's text + * Does not update the object .width and .height, + * call ._initDimensions() to update the values. + * @method setText + * @memberOf fabric.Text.prototype + * @param {String} text Text + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's textDecoration + * @method getTextDecoration + * @memberOf fabric.Text.prototype + * @return {String} Text decoration + */ + + /** + * Sets object's textDecoration + * @method setTextDecoration + * @memberOf fabric.Text.prototype + * @param {String} textDecoration Text decoration + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's fontStyle + * @method getFontStyle + * @memberOf fabric.Text.prototype + * @return {String} Font style + */ + + /** + * Sets object's fontStyle + * Does not update the object .width and .height, + * call ._initDimensions() to update the values. + * @method setFontStyle + * @memberOf fabric.Text.prototype + * @param {String} fontStyle Font style + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's lineHeight + * @method getLineHeight + * @memberOf fabric.Text.prototype + * @return {Number} Line height + */ + + /** + * Sets object's lineHeight + * @method setLineHeight + * @memberOf fabric.Text.prototype + * @param {Number} lineHeight Line height + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's textAlign + * @method getTextAlign + * @memberOf fabric.Text.prototype + * @return {String} Text alignment + */ + + /** + * Sets object's textAlign + * @method setTextAlign + * @memberOf fabric.Text.prototype + * @param {String} textAlign Text alignment + * @return {fabric.Text} + * @chainable + */ + + /** + * Retrieves object's textBackgroundColor + * @method getTextBackgroundColor + * @memberOf fabric.Text.prototype + * @return {String} Text background color + */ + + /** + * Sets object's textBackgroundColor + * @method setTextBackgroundColor + * @memberOf fabric.Text.prototype + * @param {String} textBackgroundColor Text background color + * @return {fabric.Text} + * @chainable + */ + + /** + * 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", "right" or "justify". + * @type String + * @default + */ + textAlign: 'left', + + /** + * Font style . Possible values: "", "normal", "italic" or "oblique". + * @type String + * @default + */ + fontStyle: '', + + /** + * Line height + * @type Number + * @default + */ + lineHeight: 1.16, + + /** + * Background color of text lines + * @type String + * @default + */ + textBackgroundColor: '', + + /** + * 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, + + /** + * List of properties to consider when checking if cache needs refresh + * @type Array + */ + cacheProperties: cacheProperties, + + /** + * 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, + + /** + * @private + */ + _fontSizeFraction: 0.25, + + /** + * Text Line proportion to font Size (in pixels) + * @type Number + * @default + */ + _fontSizeMult: 1.13, + + /** + * additional space between characters + * expressed in thousands of em unit + * @type Number + * @default + */ + charSpacing: 0, + + /** + * 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.callSuper('initialize', options); + this.__skipDimension = false; + this._initDimensions(); + this.setCoords(); + this.setupState({ propertySet: '_dimensionAffectingProps' }); + }, + + /** + * Initialize text dimensions. Render all text on given context + * or on a offscreen canvas to get the text width with measureText. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + * @param {CanvasRenderingContext2D} [ctx] Context to render on + * @private + */ + _initDimensions: function(ctx) { + if (this.__skipDimension) { + return; + } + if (!ctx) { + ctx = fabric.util.createCanvasElement().getContext('2d'); + this._setTextStyles(ctx); + } + this._textLines = this._splitTextIntoLines(); + this._clearCache(); + this.width = this._getTextWidth(ctx) || this.cursorWidth || MIN_TEXT_WIDTH; + this.height = this._getTextHeight(ctx); + }, + + /** + * Returns string representation of an instance + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * Return the dimension and the zoom level needed to create a cache canvas + * big enough to host the object to be cached. + * @private + * @return {Object}.width width of canvas + * @return {Object}.height height of canvas + * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache + * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache + */ + _getCacheCanvasDimensions: function() { + var dim = this.callSuper('_getCacheCanvasDimensions'); + var fontSize = this.fontSize * 2; + dim.width += fontSize * dim.zoomX; + dim.height += fontSize * dim.zoomY; + return dim; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this._setTextStyles(ctx); + if (this.group && this.group.type === 'path-group') { + ctx.translate(this.left, this.top); + } + this._renderTextLinesBackground(ctx); + this._renderText(ctx); + this._renderTextDecoration(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderText: function(ctx) { + this._renderTextFill(ctx); + this._renderTextStroke(ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _setTextStyles: function(ctx) { + ctx.textBaseline = 'alphabetic'; + ctx.font = this._getFontDeclaration(); + }, + + /** + * @private + * @return {Number} Height of fabric.Text object + */ + _getTextHeight: function() { + return this._getHeightOfSingleLine() + (this._textLines.length - 1) * this._getHeightOfLine(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {Number} Maximum width of fabric.Text object + */ + _getTextWidth: function(ctx) { + var maxWidth = this._getLineWidth(ctx, 0); + + for (var i = 1, len = this._textLines.length; i < len; i++) { + var currentLineWidth = this._getLineWidth(ctx, i); + 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} chars Chars to render + * @param {Number} left Left position of text + * @param {Number} top Top position of text + */ + _renderChars: function(method, ctx, chars, left, top) { + // remove Text word from method var + var shortM = method.slice(0, -4), _char, width; + if (this[shortM].toLive) { + var offsetX = -this.width / 2 + this[shortM].offsetX || 0, + offsetY = -this.height / 2 + this[shortM].offsetY || 0; + ctx.save(); + ctx.translate(offsetX, offsetY); + left -= offsetX; + top -= offsetY; + } + if (this.charSpacing !== 0) { + var additionalSpace = this._getWidthOfCharSpacing(); + chars = chars.split(''); + for (var i = 0, len = chars.length; i < len; i++) { + _char = chars[i]; + width = ctx.measureText(_char).width + additionalSpace; + ctx[method](_char, left, top); + left += width > 0 ? width : 0; + } + } + else { + ctx[method](chars, left, top); + } + this[shortM].toLive && ctx.restore(); + }, + + /** + * @private + * @param {String} method Method name ("fillText" or "strokeText") + * @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 + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + // lift the line by quarter of fontSize + top -= this.fontSize * this._fontSizeFraction; + + // short-circuit + var lineWidth = this._getLineWidth(ctx, lineIndex); + if (this.textAlign !== 'justify' || this.width < lineWidth) { + this._renderChars(method, ctx, line, left, top, lineIndex); + return; + } + + // stretch the line + var words = line.split(/\s+/), + charOffset = 0, + wordsWidth = this._getWidthOfWords(ctx, words.join(' '), lineIndex, 0), + widthDiff = this.width - wordsWidth, + numSpaces = words.length - 1, + spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, + leftOffset = 0, word; + + for (var i = 0, len = words.length; i < len; i++) { + while (line[charOffset] === ' ' && charOffset < line.length) { + charOffset++; + } + word = words[i]; + this._renderChars(method, ctx, word, left + leftOffset, top, lineIndex, charOffset); + leftOffset += this._getWidthOfWords(ctx, word, lineIndex, charOffset) + spaceWidth; + charOffset += word.length; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} word + */ + _getWidthOfWords: function (ctx, word) { + var width = ctx.measureText(word).width, charCount, additionalSpace; + if (this.charSpacing !== 0) { + charCount = word.split('').length; + additionalSpace = charCount * this._getWidthOfCharSpacing(); + width += additionalSpace; + } + return width > 0 ? width : 0; + }, + + /** + * @private + * @return {Number} Left offset + */ + _getLeftOffset: function() { + return -this.width / 2; + }, + + /** + * @private + * @return {Number} Top offset + */ + _getTopOffset: function() { + return -this.height / 2; + }, + + /** + * Returns true because text has no style + */ + isEmptyStyles: function() { + return true; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} method Method name ("fillText" or "strokeText") + */ + _renderTextCommon: function(ctx, method) { + + var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(); + + for (var i = 0, len = this._textLines.length; i < len; i++) { + var heightOfLine = this._getHeightOfLine(ctx, i), + maxHeight = heightOfLine / this.lineHeight, + lineWidth = this._getLineWidth(ctx, i), + leftOffset = this._getLineLeftOffset(lineWidth); + this._renderTextLine( + method, + ctx, + this._textLines[i], + left + leftOffset, + top + lineHeights + maxHeight, + i + ); + lineHeights += heightOfLine; + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextFill: function(ctx) { + if (!this.fill && this.isEmptyStyles()) { + return; + } + + this._renderTextCommon(ctx, 'fillText'); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextStroke: function(ctx) { + if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) { + return; + } + + if (this.shadow && !this.shadow.affectStroke) { + this._removeShadow(ctx); + } + + ctx.save(); + this._setLineDash(ctx, this.strokeDashArray); + ctx.beginPath(); + this._renderTextCommon(ctx, 'strokeText'); + ctx.closePath(); + ctx.restore(); + }, + + /** + * @private + * @return {Number} height of line + */ + _getHeightOfLine: function() { + return this._getHeightOfSingleLine() * this.lineHeight; + }, + + /** + * @private + * @return {Number} height of line without lineHeight + */ + _getHeightOfSingleLine: function() { + return this.fontSize * this._fontSizeMult; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextLinesBackground: function(ctx) { + if (!this.textBackgroundColor) { + return; + } + var lineTopOffset = 0, heightOfLine, + lineWidth, lineLeftOffset, originalFill = ctx.fillStyle; + + ctx.fillStyle = this.textBackgroundColor; + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this._getHeightOfLine(ctx, i); + lineWidth = this._getLineWidth(ctx, i); + if (lineWidth > 0) { + lineLeftOffset = this._getLineLeftOffset(lineWidth); + ctx.fillRect( + this._getLeftOffset() + lineLeftOffset, + this._getTopOffset() + lineTopOffset, + lineWidth, + heightOfLine / this.lineHeight + ); + } + lineTopOffset += heightOfLine; + } + ctx.fillStyle = originalFill; + // if there is text background color no + // other shadows should be casted + this._removeShadow(ctx); + }, + + /** + * @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 + */ + _clearCache: function() { + this.__lineWidths = []; + this.__lineHeights = []; + }, + + /** + * @private + */ + _shouldClearDimensionCache: function() { + var shouldClear = this._forceClearCache; + shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps')); + if (shouldClear) { + this.saveState({ propertySet: '_dimensionAffectingProps' }); + this.dirty = true; + } + return shouldClear; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + _getLineWidth: function(ctx, lineIndex) { + if (this.__lineWidths[lineIndex]) { + return this.__lineWidths[lineIndex] === -1 ? this.width : this.__lineWidths[lineIndex]; + } + + var width, wordCount, line = this._textLines[lineIndex]; + + if (line === '') { + width = 0; + } + else { + width = this._measureLine(ctx, lineIndex); + } + this.__lineWidths[lineIndex] = width; + + if (width && this.textAlign === 'justify') { + wordCount = line.split(/\s+/); + if (wordCount.length > 1) { + this.__lineWidths[lineIndex] = -1; + } + } + return width; + }, + + _getWidthOfCharSpacing: function() { + if (this.charSpacing !== 0) { + return this.fontSize * this.charSpacing / 1000; + } + return 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + _measureLine: function(ctx, lineIndex) { + var line = this._textLines[lineIndex], + width = ctx.measureText(line).width, + additionalSpace = 0, charCount, finalWidth; + if (this.charSpacing !== 0) { + charCount = line.split('').length; + additionalSpace = (charCount - 1) * this._getWidthOfCharSpacing(); + } + finalWidth = width + additionalSpace; + return finalWidth > 0 ? finalWidth : 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextDecoration: function(ctx) { + if (!this.textDecoration) { + return; + } + var halfOfVerticalBox = this.height / 2, + _this = this, offsets = []; + + /** @ignore */ + function renderLinesAtOffset(offsets) { + var i, lineHeight = 0, len, j, oLen, lineWidth, + lineLeftOffset, heightOfLine; + + for (i = 0, len = _this._textLines.length; i < len; i++) { + + lineWidth = _this._getLineWidth(ctx, i); + lineLeftOffset = _this._getLineLeftOffset(lineWidth); + heightOfLine = _this._getHeightOfLine(ctx, i); + + for (j = 0, oLen = offsets.length; j < oLen; j++) { + ctx.fillRect( + _this._getLeftOffset() + lineLeftOffset, + lineHeight + (_this._fontSizeMult - 1 + offsets[j] ) * _this.fontSize - halfOfVerticalBox, + lineWidth, + _this.fontSize / 15); + } + lineHeight += heightOfLine; + } + } + + if (this.textDecoration.indexOf('underline') > -1) { + offsets.push(0.85); // 1 - 3/16 + } + if (this.textDecoration.indexOf('line-through') > -1) { + offsets.push(0.43); + } + if (this.textDecoration.indexOf('overline') > -1) { + offsets.push(-0.12); + } + if (offsets.length > 0) { + renderLinesAtOffset(offsets); + } + }, + + /** + * return font declaration string for canvas context + * @returns {String} font declaration formatted for canvas context. + */ + _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 + */ + render: function(ctx, noTransform) { + // do not render if object is not visible + if (!this.visible) { + return; + } + if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) { + return; + } + if (this._shouldClearDimensionCache()) { + this._setTextStyles(ctx); + this._initDimensions(ctx); + } + this.callSuper('render', ctx, noTransform); + }, + + /** + * Returns the text as an array of lines. + * @returns {Array} Lines in the text + */ + _splitTextIntoLines: function() { + return this.text.split(this._reNewline); + }, + + /** + * 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 additionalProperties = [ + 'text', + 'fontSize', + 'fontWeight', + 'fontFamily', + 'fontStyle', + 'lineHeight', + 'textDecoration', + 'textAlign', + 'textBackgroundColor', + 'charSpacing' + ].concat(propertiesToInclude); + return this.callSuper('toObject', additionalProperties); + }, + + /* _TO_SVG_START_ */ + /** + * Returns SVG representation of an instance + * @param {Function} [reviver] Method for further parsing of svg representation. + * @return {String} svg representation of an instance + */ + toSVG: function(reviver) { + if (!this.ctx) { + this.ctx = fabric.util.createCanvasElement().getContext('2d'); + } + var markup = this._createBaseSVGMarkup(), + offsets = this._getSVGLeftTopOffsets(this.ctx), + textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); + this._wrapSVGTextAndBg(markup, textAndBg); + + return reviver ? reviver(markup.join('')) : markup.join(''); + }, + + /** + * @private + */ + _getSVGLeftTopOffsets: function(ctx) { + var lineTop = this._getHeightOfLine(ctx, 0), + textLeft = -this.width / 2, + textTop = 0; + + return { + textLeft: textLeft + (this.group && this.group.type === 'path-group' ? this.left : 0), + textTop: textTop + (this.group && this.group.type === 'path-group' ? -this.top : 0), + lineTop: lineTop + }; + }, + + /** + * @private + */ + _wrapSVGTextAndBg: function(markup, textAndBg) { + var noShadow = true, filter = this.getSvgFilter(), + style = filter === '' ? '' : ' style="' + filter + '"'; + + markup.push( + '\t\n', + textAndBg.textBgRects.join(''), + '\t\t\n', + textAndBg.textSpans.join(''), + '\t\t\n', + '\t\n' + ); + }, + + /** + * @private + * @param {Number} textTopOffset Text top offset + * @param {Number} textLeftOffset Text left offset + * @return {Object} + */ + _getSVGTextAndBg: function(textTopOffset, textLeftOffset) { + var textSpans = [], + textBgRects = [], + height = 0; + // bounding-box background + this._setSVGBg(textBgRects); + + // text and text-background + for (var i = 0, len = this._textLines.length; i < len; i++) { + if (this.textBackgroundColor) { + this._setSVGTextLineBg(textBgRects, i, textLeftOffset, textTopOffset, height); + } + this._setSVGTextLineText(i, textSpans, height, textLeftOffset, textTopOffset, textBgRects); + height += this._getHeightOfLine(this.ctx, i); + } + + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + _setSVGTextLineText: function(i, textSpans, height, textLeftOffset, textTopOffset) { + var yPos = this.fontSize * (this._fontSizeMult - this._fontSizeFraction) + - textTopOffset + height - this.height / 2; + if (this.textAlign === 'justify') { + // i call from here to do not intefere with IText + this._setSVGTextLineJustifed(i, textSpans, yPos, textLeftOffset); + return; + } + textSpans.push( + '\t\t\t elements since setting opacity + // on containing one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(this._textLines[i]), + '\n' + ); + }, + + _setSVGTextLineJustifed: function(i, textSpans, yPos, textLeftOffset) { + var ctx = fabric.util.createCanvasElement().getContext('2d'); + + this._setTextStyles(ctx); + + var line = this._textLines[i], + words = line.split(/\s+/), + wordsWidth = this._getWidthOfWords(ctx, words.join('')), + widthDiff = this.width - wordsWidth, + numSpaces = words.length - 1, + spaceWidth = numSpaces > 0 ? widthDiff / numSpaces : 0, + word, attributes = this._getFillAttributes(this.fill), + len; + + textLeftOffset += this._getLineLeftOffset(this._getLineWidth(ctx, i)); + + for (i = 0, len = words.length; i < len; i++) { + word = words[i]; + textSpans.push( + '\t\t\t elements since setting opacity + // on containing one doesn't work in Illustrator + attributes, '>', + fabric.util.string.escapeXml(word), + '\n' + ); + textLeftOffset += this._getWidthOfWords(ctx, word) + spaceWidth; + } + }, + + _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, textTopOffset, height) { + textBgRects.push( + '\t\t\n'); + }, + + _setSVGBg: function(textBgRects) { + if (this.backgroundColor) { + textBgRects.push( + '\t\t\n'); + } + }, + + /** + * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values + * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 + * + * @private + * @param {*} value + * @return {String} + */ + _getFillAttributes: function(value) { + var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; + if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { + return 'fill="' + value + '"'; + } + return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; + }, + /* _TO_SVG_END_ */ + + /** + * Sets specified property to a specified value + * @param {String} key + * @param {*} value + * @return {fabric.Text} thisArg + * @chainable + */ + _set: function(key, value) { + this.callSuper('_set', key, value); + + if (this._dimensionAffectingProps.indexOf(key) > -1) { + 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 dx dy font-family font-style font-weight font-size text-decoration text-anchor'.split(' ')); + + /** + * Default SVG font size + * @static + * @memberOf fabric.Text + */ + fabric.Text.DEFAULT_SVG_FONT_SIZE = 16; + + /** + * 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); + + options.top = options.top || 0; + options.left = options.left || 0; + if ('dx' in parsedAttributes) { + options.left += parsedAttributes.dx; + } + if ('dy' in parsedAttributes) { + options.top += parsedAttributes.dy; + } + if (!('fontSize' in options)) { + options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; + } + + if (!options.originX) { + options.originX = 'left'; + } + + var textContent = ''; + + // The XML is not properly parsed in IE9 so a workaround to get + // textContent is through firstChild.data. Another workaround would be + // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does) + if (!('textContent' in element)) { + if ('firstChild' in element && element.firstChild !== null) { + if ('data' in element.firstChild && element.firstChild.data !== null) { + textContent = element.firstChild.data; + } + } + } + else { + textContent = element.textContent; + } + + textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '); + + var text = new fabric.Text(textContent, options), + textHeightScaleFactor = text.getHeight() / text.height, + lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height, + scaledDiff = lineHeightDiff * textHeightScaleFactor, + textHeight = text.getHeight() + scaledDiff, + offX = 0; + /* + Adjust positioning: + x/y attributes in SVG correspond to the bottom-left corner of text bounding box + top/left properties in Fabric correspond to center point of text bounding box + */ + if (text.originX === 'left') { + offX = text.getWidth() / 2; + } + if (text.originX === 'right') { + offX = -text.getWidth() / 2; + } + text.set({ + left: text.getLeft() + offX, + top: text.getTop() - textHeight / 2 + text.fontSize * (0.18 + text._fontSizeFraction) / text.lineHeight /* 0.3 is the old lineHeight */ + }); + + return text; + }; + /* _FROM_SVG_END_ */ + + /** + * Returns fabric.Text instance from an object representation + * @static + * @memberOf fabric.Text + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Text} Instance of fabric.Text + */ + fabric.Text.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Text', object, callback, forceAsync, 'text'); + }; + + fabric.util.createAccessors(fabric.Text); + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + var clone = fabric.util.object.clone; + + /** + * IText class (introduced in v1.4) Events are also fired with "text:" + * prefix when observing canvas. + * @class fabric.IText + * @extends fabric.Text + * @mixes fabric.Observable + * + * @fires changed + * @fires selection:changed + * @fires editing:entered + * @fires editing:exited + * + * @return {fabric.IText} thisArg + * @see {@link fabric.IText#initialize} for constructor definition + * + *

Supported key combinations:

+ *
+   *   Move cursor:                    left, right, up, down
+   *   Select character:               shift + left, shift + right
+   *   Select text vertically:         shift + up, shift + down
+   *   Move cursor by word:            alt + left, alt + right
+   *   Select words:                   shift + alt + left, shift + alt + right
+   *   Move cursor to line start/end:  cmd + left, cmd + right or home, end
+   *   Select till start/end of line:  cmd + shift + left, cmd + shift + right or shift + home, shift + end
+   *   Jump to start/end of text:      cmd + up, cmd + down
+   *   Select till start/end of text:  cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
+   *   Delete character:               backspace
+   *   Delete word:                    alt + backspace
+   *   Delete line:                    cmd + backspace
+   *   Forward delete:                 delete
+   *   Copy text:                      ctrl/cmd + c
+   *   Paste text:                     ctrl/cmd + v
+   *   Cut text:                       ctrl/cmd + x
+   *   Select entire text:             ctrl/cmd + a
+   *   Quit editing                    tab or esc
+   * 
+ * + *

Supported mouse/touch combination

+ *
+   *   Position cursor:                click/touch
+   *   Create selection:               click/touch & drag
+   *   Create selection:               click & shift + click
+   *   Select word:                    double click
+   *   Select line:                    triple click
+   * 
+ */ + fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { + + /** + * Type of an object + * @type String + * @default + */ + type: 'i-text', + + /** + * Index where text selection starts (or where cursor is when there is no selection) + * @type Number + * @default + */ + selectionStart: 0, + + /** + * Index where text selection ends + * @type Number + * @default + */ + selectionEnd: 0, + + /** + * Color of text selection + * @type String + * @default + */ + selectionColor: 'rgba(17,119,255,0.3)', + + /** + * Indicates whether text is in editing mode + * @type Boolean + * @default + */ + isEditing: false, + + /** + * Indicates whether a text can be edited + * @type Boolean + * @default + */ + editable: true, + + /** + * Border color of text object while it's in editing mode + * @type String + * @default + */ + editingBorderColor: 'rgba(102,153,255,0.25)', + + /** + * Width of cursor (in px) + * @type Number + * @default + */ + cursorWidth: 2, + + /** + * Color of default cursor (when not overwritten by character style) + * @type String + * @default + */ + cursorColor: '#333', + + /** + * Delay between cursor blink (in ms) + * @type Number + * @default + */ + cursorDelay: 1000, + + /** + * Duration of cursor fadein (in ms) + * @type Number + * @default + */ + cursorDuration: 600, + + /** + * Object containing character styles + * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) + * @type Object + * @default + */ + styles: null, + + /** + * Indicates whether internal text char widths can be cached + * @type Boolean + * @default + */ + caching: true, + + /** + * @private + */ + _reSpace: /\s|\n/, + + /** + * @private + */ + _currentCursorOpacity: 0, + + /** + * @private + */ + _selectionDirection: null, + + /** + * @private + */ + _abortCursorAnimation: false, + + /** + * @private + */ + __widthOfSpace: [], + + /** + * Constructor + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.IText} thisArg + */ + initialize: function(text, options) { + this.styles = options ? (options.styles || { }) : { }; + this.callSuper('initialize', text, options); + this.initBehavior(); + }, + + /** + * @private + */ + _clearCache: function() { + this.callSuper('_clearCache'); + this.__widthOfSpace = []; + }, + + /** + * Returns true if object has no styling + */ + isEmptyStyles: function() { + if (!this.styles) { + return true; + } + var obj = this.styles; + + for (var p1 in obj) { + for (var p2 in obj[p1]) { + // eslint-disable-next-line no-unused-vars + for (var p3 in obj[p1][p2]) { + return false; + } + } + } + return true; + }, + + /** + * Sets selection start (left boundary of a selection) + * @param {Number} index Index to set selection start to + */ + setSelectionStart: function(index) { + index = Math.max(index, 0); + this._updateAndFire('selectionStart', index); + }, + + /** + * Sets selection end (right boundary of a selection) + * @param {Number} index Index to set selection end to + */ + setSelectionEnd: function(index) { + index = Math.min(index, this.text.length); + this._updateAndFire('selectionEnd', index); + }, + + /** + * @private + * @param {String} property 'selectionStart' or 'selectionEnd' + * @param {Number} index new position of property + */ + _updateAndFire: function(property, index) { + if (this[property] !== index) { + this._fireSelectionChanged(); + this[property] = index; + } + this._updateTextarea(); + }, + + /** + * Fires the even of selection changed + * @private + */ + _fireSelectionChanged: function() { + this.fire('selection:changed'); + this.canvas && this.canvas.fire('text:selection:changed', { target: this }); + }, + + /** + * Gets style of a current selection/cursor (at the start position) + * @param {Number} [startIndex] Start index to get styles at + * @param {Number} [endIndex] End index to get styles at + * @return {Object} styles Style object at a specified (or current) index + */ + getSelectionStyles: function(startIndex, endIndex) { + + if (arguments.length === 2) { + var styles = []; + for (var i = startIndex; i < endIndex; i++) { + styles.push(this.getSelectionStyles(i)); + } + return styles; + } + + var loc = this.get2DCursorLocation(startIndex), + style = this._getStyleDeclaration(loc.lineIndex, loc.charIndex); + + return style || {}; + }, + + /** + * Sets style of a current selection + * @param {Object} [styles] Styles object + * @return {fabric.IText} thisArg + * @chainable + */ + setSelectionStyles: function(styles) { + if (this.selectionStart === this.selectionEnd) { + this._extendStyles(this.selectionStart, styles); + } + else { + for (var i = this.selectionStart; i < this.selectionEnd; i++) { + this._extendStyles(i, styles); + } + } + /* not included in _extendStyles to avoid clearing cache more than once */ + this._forceClearCache = true; + return this; + }, + + /** + * @private + */ + _extendStyles: function(index, styles) { + var loc = this.get2DCursorLocation(index); + + if (!this._getLineStyle(loc.lineIndex)) { + this._setLineStyle(loc.lineIndex, {}); + } + + if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) { + this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {}); + } + + fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles); + }, + + /** + * Initialize text dimensions. Render all text on given context + * or on a offscreen canvas to get the text width with measureText. + * Updates this.width and this.height with the proper values. + * Does not return dimensions. + * @param {CanvasRenderingContext2D} [ctx] Context to render on + * @private + */ + _initDimensions: function(ctx) { + if (!ctx) { + this.clearContextTop(); + } + this.callSuper('_initDimensions', ctx); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + render: function(ctx, noTransform) { + this.clearContextTop(); + this.callSuper('render', ctx, noTransform); + // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor + // the correct position but not at every cursor animation. + this.cursorOffsetCache = { }; + this.renderCursorOrSelection(); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + this.callSuper('_render', ctx); + this.ctx = ctx; + }, + + /** + * Prepare and clean the contextTop + */ + clearContextTop: function() { + if (!this.active || !this.isEditing) { + return; + } + if (this.canvas && this.canvas.contextTop) { + var ctx = this.canvas.contextTop; + ctx.save(); + ctx.transform.apply(ctx, this.canvas.viewportTransform); + this.transform(ctx); + this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix); + this._clearTextArea(ctx); + ctx.restore(); + } + }, + + /** + * Renders cursor or selection (depending on what exists) + */ + renderCursorOrSelection: function() { + if (!this.active || !this.isEditing) { + return; + } + var chars = this.text.split(''), + boundaries, ctx; + if (this.canvas && this.canvas.contextTop) { + ctx = this.canvas.contextTop; + ctx.save(); + ctx.transform.apply(ctx, this.canvas.viewportTransform); + this.transform(ctx); + this.transformMatrix && ctx.transform.apply(ctx, this.transformMatrix); + this._clearTextArea(ctx); + } + else { + ctx = this.ctx; + ctx.save(); + } + if (this.selectionStart === this.selectionEnd) { + boundaries = this._getCursorBoundaries(chars, 'cursor'); + this.renderCursor(boundaries, ctx); + } + else { + boundaries = this._getCursorBoundaries(chars, 'selection'); + this.renderSelection(chars, boundaries, ctx); + } + ctx.restore(); + }, + + _clearTextArea: function(ctx) { + // we add 4 pixel, to be sure to do not leave any pixel out + var width = this.width + 4, height = this.height + 4; + ctx.clearRect(-width / 2, -height / 2, width, height); + }, + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + */ + get2DCursorLocation: function(selectionStart) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + var len = this._textLines.length; + for (var i = 0; i < len; i++) { + if (selectionStart <= this._textLines[i].length) { + return { + lineIndex: i, + charIndex: selectionStart + }; + } + selectionStart -= this._textLines[i].length + 1; + } + return { + lineIndex: i - 1, + charIndex: this._textLines[i - 1].length < selectionStart ? this._textLines[i - 1].length : selectionStart + }; + }, + + /** + * Returns complete style of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {Object} Character style + */ + getCurrentCharStyle: function(lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); + + return { + fontSize: style && style.fontSize || this.fontSize, + fill: style && style.fill || this.fill, + textBackgroundColor: style && style.textBackgroundColor || this.textBackgroundColor, + textDecoration: style && style.textDecoration || this.textDecoration, + fontFamily: style && style.fontFamily || this.fontFamily, + fontWeight: style && style.fontWeight || this.fontWeight, + fontStyle: style && style.fontStyle || this.fontStyle, + stroke: style && style.stroke || this.stroke, + strokeWidth: style && style.strokeWidth || this.strokeWidth + }; + }, + + /** + * Returns fontSize of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {Number} Character font size + */ + getCurrentCharFontSize: function(lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); + return style && style.fontSize ? style.fontSize : this.fontSize; + }, + + /** + * Returns color (fill) of char at the current cursor + * @param {Number} lineIndex Line index + * @param {Number} charIndex Char index + * @return {String} Character color (fill) + */ + getCurrentCharColor: function(lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex === 0 ? 0 : charIndex - 1); + return style && style.fill ? style.fill : this.cursorColor; + }, + + /** + * Returns cursor boundaries (left, top, leftOffset, topOffset) + * @private + * @param {Array} chars Array of characters + * @param {String} typeOfBoundaries + */ + _getCursorBoundaries: function(chars, typeOfBoundaries) { + + // left/top are left/top of entire text box + // leftOffset/topOffset are offset from that left/top point of a text box + + var left = Math.round(this._getLeftOffset()), + top = this._getTopOffset(), + + offsets = this._getCursorBoundariesOffsets( + chars, typeOfBoundaries); + + return { + left: left, + top: top, + leftOffset: offsets.left + offsets.lineLeft, + topOffset: offsets.top + }; + }, + + /** + * @private + */ + _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) { + if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) { + return this.cursorOffsetCache; + } + var lineLeftOffset = 0, + lineIndex = 0, + charIndex = 0, + topOffset = 0, + leftOffset = 0, + boundaries; + + for (var i = 0; i < this.selectionStart; i++) { + if (chars[i] === '\n') { + leftOffset = 0; + topOffset += this._getHeightOfLine(this.ctx, lineIndex); + + lineIndex++; + charIndex = 0; + } + else { + leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); + charIndex++; + } + + lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)); + } + if (typeOfBoundaries === 'cursor') { + topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, lineIndex) / this.lineHeight + - this.getCurrentCharFontSize(lineIndex, charIndex) * (1 - this._fontSizeFraction); + } + if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) { + leftOffset -= this._getWidthOfCharSpacing(); + } + boundaries = { + top: topOffset, + left: leftOffset > 0 ? leftOffset : 0, + lineLeft: lineLeftOffset + }; + this.cursorOffsetCache = boundaries; + return this.cursorOffsetCache; + }, + + /** + * Renders cursor + * @param {Object} boundaries + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderCursor: function(boundaries, ctx) { + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getCurrentCharFontSize(lineIndex, charIndex), + leftOffset = boundaries.leftOffset, + multiplier = this.scaleX * this.canvas.getZoom(), + cursorWidth = this.cursorWidth / multiplier; + + ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); + ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; + + ctx.fillRect( + boundaries.left + leftOffset - cursorWidth / 2, + boundaries.top + boundaries.topOffset, + cursorWidth, + charHeight); + }, + + /** + * Renders text selection + * @param {Array} chars Array of characters + * @param {Object} boundaries Object with left/top/leftOffset/topOffset + * @param {CanvasRenderingContext2D} ctx transformed context to draw on + */ + renderSelection: function(chars, boundaries, ctx) { + + ctx.fillStyle = this.selectionColor; + + var start = this.get2DCursorLocation(this.selectionStart), + end = this.get2DCursorLocation(this.selectionEnd), + startLine = start.lineIndex, + endLine = end.lineIndex; + for (var i = startLine; i <= endLine; i++) { + var lineOffset = this._getLineLeftOffset(this._getLineWidth(ctx, i)) || 0, + lineHeight = this._getHeightOfLine(this.ctx, i), + realLineHeight = 0, boxWidth = 0, line = this._textLines[i]; + + if (i === startLine) { + for (var j = 0, len = line.length; j < len; j++) { + if (j >= start.charIndex && (i !== endLine || j < end.charIndex)) { + boxWidth += this._getWidthOfChar(ctx, line[j], i, j); + } + if (j < start.charIndex) { + lineOffset += this._getWidthOfChar(ctx, line[j], i, j); + } + } + if (j === line.length) { + boxWidth -= this._getWidthOfCharSpacing(); + } + } + else if (i > startLine && i < endLine) { + boxWidth += this._getLineWidth(ctx, i) || 5; + } + else if (i === endLine) { + for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) { + boxWidth += this._getWidthOfChar(ctx, line[j2], i, j2); + } + if (end.charIndex === line.length) { + boxWidth -= this._getWidthOfCharSpacing(); + } + } + realLineHeight = lineHeight; + if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) { + lineHeight /= this.lineHeight; + } + ctx.fillRect( + boundaries.left + lineOffset, + boundaries.top + boundaries.topOffset, + boxWidth > 0 ? boxWidth : 0, + lineHeight); + + boundaries.topOffset += realLineHeight; + } + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Content of the line + * @param {Number} left + * @param {Number} top + * @param {Number} lineIndex + * @param {Number} charOffset + */ + _renderChars: function(method, ctx, line, left, top, lineIndex, charOffset) { + + if (this.isEmptyStyles()) { + return this._renderCharsFast(method, ctx, line, left, top); + } + + charOffset = charOffset || 0; + + // set proper line offset + var lineHeight = this._getHeightOfLine(ctx, lineIndex), + prevStyle, + thisStyle, + charsToRender = ''; + + ctx.save(); + top -= lineHeight / this.lineHeight * this._fontSizeFraction; + for (var i = charOffset, len = line.length + charOffset; i <= len; i++) { + prevStyle = prevStyle || this.getCurrentCharStyle(lineIndex, i); + thisStyle = this.getCurrentCharStyle(lineIndex, i + 1); + + if (this._hasStyleChanged(prevStyle, thisStyle) || i === len) { + this._renderChar(method, ctx, lineIndex, i - 1, charsToRender, left, top, lineHeight); + charsToRender = ''; + prevStyle = thisStyle; + } + charsToRender += line[i - charOffset]; + } + ctx.restore(); + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line Content of the line + * @param {Number} left Left coordinate + * @param {Number} top Top coordinate + */ + _renderCharsFast: function(method, ctx, line, left, top) { + + if (method === 'fillText' && this.fill) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + if (method === 'strokeText' && ((this.stroke && this.strokeWidth > 0) || this.skipFillStrokeCheck)) { + this.callSuper('_renderChars', method, ctx, line, left, top); + } + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + * @param {Number} i + * @param {String} _char + * @param {Number} left Left coordinate + * @param {Number} top Top coordinate + * @param {Number} lineHeight Height of the line + */ + _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { + var charWidth, charHeight, shouldFill, shouldStroke, + decl = this._getStyleDeclaration(lineIndex, i), + offset, textDecoration, chars, additionalSpace, _charWidth; + + if (decl) { + charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i); + shouldStroke = decl.stroke; + shouldFill = decl.fill; + textDecoration = decl.textDecoration; + } + else { + charHeight = this.fontSize; + } + + shouldStroke = (shouldStroke || this.stroke) && method === 'strokeText'; + shouldFill = (shouldFill || this.fill) && method === 'fillText'; + + decl && ctx.save(); + + charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl || null); + textDecoration = textDecoration || this.textDecoration; + + if (decl && decl.textBackgroundColor) { + this._removeShadow(ctx); + } + if (this.charSpacing !== 0) { + additionalSpace = this._getWidthOfCharSpacing(); + chars = _char.split(''); + charWidth = 0; + for (var j = 0, len = chars.length, jChar; j < len; j++) { + jChar = chars[j]; + shouldFill && ctx.fillText(jChar, left + charWidth, top); + shouldStroke && ctx.strokeText(jChar, left + charWidth, top); + _charWidth = ctx.measureText(jChar).width + additionalSpace; + charWidth += _charWidth > 0 ? _charWidth : 0; + } + } + else { + shouldFill && ctx.fillText(_char, left, top); + shouldStroke && ctx.strokeText(_char, left, top); + } + + if (textDecoration || textDecoration !== '') { + offset = this._fontSizeFraction * lineHeight / this.lineHeight; + this._renderCharDecoration(ctx, textDecoration, left, top, offset, charWidth, charHeight); + } + + decl && ctx.restore(); + ctx.translate(charWidth, 0); + }, + + /** + * @private + * @param {Object} prevStyle + * @param {Object} thisStyle + */ + _hasStyleChanged: function(prevStyle, thisStyle) { + return (prevStyle.fill !== thisStyle.fill || + prevStyle.fontSize !== thisStyle.fontSize || + prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor || + prevStyle.textDecoration !== thisStyle.textDecoration || + prevStyle.fontFamily !== thisStyle.fontFamily || + prevStyle.fontWeight !== thisStyle.fontWeight || + prevStyle.fontStyle !== thisStyle.fontStyle || + prevStyle.stroke !== thisStyle.stroke || + prevStyle.strokeWidth !== thisStyle.strokeWidth + ); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderCharDecoration: function(ctx, textDecoration, left, top, offset, charWidth, charHeight) { + + if (!textDecoration) { + return; + } + + var decorationWeight = charHeight / 15, + positions = { + underline: top + charHeight / 10, + 'line-through': top - charHeight * (this._fontSizeFraction + this._fontSizeMult - 1) + decorationWeight, + overline: top - (this._fontSizeMult - this._fontSizeFraction) * charHeight + }, + decorations = ['underline', 'line-through', 'overline'], i, decoration; + + for (i = 0; i < decorations.length; i++) { + decoration = decorations[i]; + if (textDecoration.indexOf(decoration) > -1) { + ctx.fillRect(left, positions[decoration], charWidth , decorationWeight); + } + } + }, + + /** + * @private + * @param {String} method + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + * @param {Number} left + * @param {Number} top + * @param {Number} lineIndex + */ + _renderTextLine: function(method, ctx, line, left, top, lineIndex) { + // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine + // the adding 0.03 is just to align text with itext by overlap test + if (!this.isEmptyStyles()) { + top += this.fontSize * (this._fontSizeFraction + 0.03); + } + this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextDecoration: function(ctx) { + if (this.isEmptyStyles()) { + return this.callSuper('_renderTextDecoration', ctx); + } + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderTextLinesBackground: function(ctx) { + this.callSuper('_renderTextLinesBackground', ctx); + + var lineTopOffset = 0, heightOfLine, + lineWidth, lineLeftOffset, + leftOffset = this._getLeftOffset(), + topOffset = this._getTopOffset(), + colorCache = '', + line, _char, style, leftCache, + topCache, widthCache, heightCache; + ctx.save(); + for (var i = 0, len = this._textLines.length; i < len; i++) { + heightOfLine = this._getHeightOfLine(ctx, i); + line = this._textLines[i]; + + if (line === '' || !this.styles || !this._getLineStyle(i)) { + lineTopOffset += heightOfLine; + continue; + } + + lineWidth = this._getLineWidth(ctx, i); + lineLeftOffset = this._getLineLeftOffset(lineWidth); + leftCache = topCache = widthCache = heightCache = 0; + for (var j = 0, jlen = line.length; j < jlen; j++) { + style = this._getStyleDeclaration(i, j) || {}; + + if (colorCache !== style.textBackgroundColor) { + if (heightCache && widthCache) { + ctx.fillStyle = colorCache; + ctx.fillRect(leftCache, topCache, widthCache, heightCache); + } + leftCache = topCache = widthCache = heightCache = 0; + colorCache = style.textBackgroundColor || ''; + } + + if (!style.textBackgroundColor) { + colorCache = ''; + continue; + } + _char = line[j]; + + if (colorCache === style.textBackgroundColor) { + colorCache = style.textBackgroundColor; + if (!leftCache) { + leftCache = leftOffset + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j); + } + topCache = topOffset + lineTopOffset; + widthCache += this._getWidthOfChar(ctx, _char, i, j); + heightCache = heightOfLine / this.lineHeight; + } + } + // if a textBackgroundColor ends on the last character of a line + if (heightCache && widthCache) { + ctx.fillStyle = colorCache; + ctx.fillRect(leftCache, topCache, widthCache, heightCache); + leftCache = topCache = widthCache = heightCache = 0; + } + lineTopOffset += heightOfLine; + } + ctx.restore(); + }, + + /** + * @private + */ + _getCacheProp: function(_char, styleDeclaration) { + return _char + + styleDeclaration.fontSize + + styleDeclaration.fontWeight + + styleDeclaration.fontStyle; + }, + + /** + * @private + * @param {String} fontFamily name + * @return {Object} reference to cache + */ + _getFontCache: function(fontFamily) { + if (!fabric.charWidthsCache[fontFamily]) { + fabric.charWidthsCache[fontFamily] = { }; + } + return fabric.charWidthsCache[fontFamily]; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} _char + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} [decl] + */ + _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { + var charDecl = decl || this._getStyleDeclaration(lineIndex, charIndex), + styleDeclaration = clone(charDecl), + width, cacheProp, charWidthsCache; + + this._applyFontStyles(styleDeclaration); + charWidthsCache = this._getFontCache(styleDeclaration.fontFamily); + cacheProp = this._getCacheProp(_char, styleDeclaration); + + // short-circuit if no styles for this char + // global style from object is always applyed and handled by save and restore + if (!charDecl && charWidthsCache[cacheProp] && this.caching) { + return charWidthsCache[cacheProp]; + } + + if (typeof styleDeclaration.shadow === 'string') { + styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow); + } + + var fill = styleDeclaration.fill || this.fill; + ctx.fillStyle = fill.toLive + ? fill.toLive(ctx, this) + : fill; + + if (styleDeclaration.stroke) { + ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) + ? styleDeclaration.stroke.toLive(ctx, this) + : styleDeclaration.stroke; + } + + ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; + ctx.font = this._getFontDeclaration.call(styleDeclaration); + + //if we want this._setShadow.call to work with styleDeclarion + //we have to add those references + if (styleDeclaration.shadow) { + styleDeclaration.scaleX = this.scaleX; + styleDeclaration.scaleY = this.scaleY; + styleDeclaration.canvas = this.canvas; + styleDeclaration.getObjectScaling = this.getObjectScaling; + this._setShadow.call(styleDeclaration, ctx); + } + + if (!this.caching || !charWidthsCache[cacheProp]) { + width = ctx.measureText(_char).width; + this.caching && (charWidthsCache[cacheProp] = width); + return width; + } + + return charWidthsCache[cacheProp]; + }, + + /** + * @private + * @param {Object} styleDeclaration + */ + _applyFontStyles: function(styleDeclaration) { + if (!styleDeclaration.fontFamily) { + styleDeclaration.fontFamily = this.fontFamily; + } + if (!styleDeclaration.fontSize) { + styleDeclaration.fontSize = this.fontSize; + } + if (!styleDeclaration.fontWeight) { + styleDeclaration.fontWeight = this.fontWeight; + } + if (!styleDeclaration.fontStyle) { + styleDeclaration.fontStyle = this.fontStyle; + } + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Boolean} [returnCloneOrEmpty=false] + * @private + */ + _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) { + if (returnCloneOrEmpty) { + return (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) + ? clone(this.styles[lineIndex][charIndex]) + : { }; + } + + return this.styles[lineIndex] && this.styles[lineIndex][charIndex] ? this.styles[lineIndex][charIndex] : null; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + this.styles[lineIndex][charIndex] = style; + }, + + /** + * + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + delete this.styles[lineIndex][charIndex]; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _getLineStyle: function(lineIndex) { + return this.styles[lineIndex]; + }, + + /** + * @param {Number} lineIndex + * @param {Object} style + * @private + */ + _setLineStyle: function(lineIndex, style) { + this.styles[lineIndex] = style; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _deleteLineStyle: function(lineIndex) { + delete this.styles[lineIndex]; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { + if (!this._isMeasuring && this.textAlign === 'justify' && this._reSpacesAndTabs.test(_char)) { + return this._getWidthOfSpace(ctx, lineIndex); + } + ctx.save(); + var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); + if (this.charSpacing !== 0) { + width += this._getWidthOfCharSpacing(); + } + ctx.restore(); + return width > 0 ? width : 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + * @param {Number} charIndex + */ + _getHeightOfChar: function(ctx, lineIndex, charIndex) { + var style = this._getStyleDeclaration(lineIndex, charIndex); + return style && style.fontSize ? style.fontSize : this.fontSize; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + * @param {Number} charIndex + */ + _getWidthOfCharsAt: function(ctx, lineIndex, charIndex) { + var width = 0, i, _char; + for (i = 0; i < charIndex; i++) { + _char = this._textLines[lineIndex][i]; + width += this._getWidthOfChar(ctx, _char, lineIndex, i); + } + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex line number + * @return {Number} Line width + */ + _measureLine: function(ctx, lineIndex) { + this._isMeasuring = true; + var width = this._getWidthOfCharsAt(ctx, lineIndex, this._textLines[lineIndex].length); + if (this.charSpacing !== 0) { + width -= this._getWidthOfCharSpacing(); + } + this._isMeasuring = false; + return width > 0 ? width : 0; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Number} lineIndex + */ + _getWidthOfSpace: function (ctx, lineIndex) { + if (this.__widthOfSpace[lineIndex]) { + return this.__widthOfSpace[lineIndex]; + } + var line = this._textLines[lineIndex], + wordsWidth = this._getWidthOfWords(ctx, line, lineIndex, 0), + widthDiff = this.width - wordsWidth, + numSpaces = line.length - line.replace(this._reSpacesAndTabs, '').length, + width = Math.max(widthDiff / numSpaces, ctx.measureText(' ').width); + this.__widthOfSpace[lineIndex] = width; + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {String} line + * @param {Number} lineIndex + * @param {Number} charOffset + */ + _getWidthOfWords: function (ctx, line, lineIndex, charOffset) { + var width = 0; + + for (var charIndex = 0; charIndex < line.length; charIndex++) { + var _char = line[charIndex]; + + if (!_char.match(/\s/)) { + width += this._getWidthOfChar(ctx, _char, lineIndex, charIndex + charOffset); + } + } + + return width; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getHeightOfLine: function(ctx, lineIndex) { + if (this.__lineHeights[lineIndex]) { + return this.__lineHeights[lineIndex]; + } + + var line = this._textLines[lineIndex], + maxHeight = this._getHeightOfChar(ctx, lineIndex, 0); + + for (var i = 1, len = line.length; i < len; i++) { + var currentCharHeight = this._getHeightOfChar(ctx, lineIndex, i); + if (currentCharHeight > maxHeight) { + maxHeight = currentCharHeight; + } + } + this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult; + return this.__lineHeights[lineIndex]; + }, + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _getTextHeight: function(ctx) { + var lineHeight, height = 0; + for (var i = 0, len = this._textLines.length; i < len; i++) { + lineHeight = this._getHeightOfLine(ctx, i); + height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight); + } + return height; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { + styles: clone(this.styles, true) + }); + } + }); + + /** + * Returns fabric.IText instance from an object representation + * @static + * @memberOf fabric.IText + * @param {Object} object Object to create an instance from + * @param {function} [callback] invoked with new instance as argument + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.IText} instance of fabric.IText + */ + fabric.IText.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('IText', object, callback, forceAsync, 'text'); + }; +})(); + + +(function() { + + var clone = fabric.util.object.clone; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes all the interactive behavior of IText + */ + initBehavior: function() { + this.initAddedHandler(); + this.initRemovedHandler(); + this.initCursorSelectionHandlers(); + this.initDoubleClickSimulation(); + this.mouseMoveHandler = this.mouseMoveHandler.bind(this); + }, + + onDeselect: function() { + this.isEditing && this.exitEditing(); + this.selected = false; + this.callSuper('onDeselect'); + }, + + /** + * Initializes "added" event handler + */ + initAddedHandler: function() { + var _this = this; + this.on('added', function() { + var canvas = _this.canvas; + if (canvas) { + if (!canvas._hasITextHandlers) { + canvas._hasITextHandlers = true; + _this._initCanvasHandlers(canvas); + } + canvas._iTextInstances = canvas._iTextInstances || []; + canvas._iTextInstances.push(_this); + } + }); + }, + + initRemovedHandler: function() { + var _this = this; + this.on('removed', function() { + var canvas = _this.canvas; + if (canvas) { + canvas._iTextInstances = canvas._iTextInstances || []; + fabric.util.removeFromArray(canvas._iTextInstances, _this); + if (canvas._iTextInstances.length === 0) { + canvas._hasITextHandlers = false; + _this._removeCanvasHandlers(canvas); + } + } + }); + }, + + /** + * register canvas event to manage exiting on other instances + * @private + */ + _initCanvasHandlers: function(canvas) { + canvas._mouseUpITextHandler = (function() { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.__isMousedown = false; + }); + } + }).bind(this); + canvas.on('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * remove canvas event to manage exiting on other instances + * @private + */ + _removeCanvasHandlers: function(canvas) { + canvas.off('mouse:up', canvas._mouseUpITextHandler); + }, + + /** + * @private + */ + _tick: function() { + this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete'); + }, + + /** + * @private + */ + _animateCursor: function(obj, targetOpacity, duration, completeMethod) { + + var tickState; + + tickState = { + isAborted: false, + abort: function() { + this.isAborted = true; + }, + }; + + obj.animate('_currentCursorOpacity', targetOpacity, { + duration: duration, + onComplete: function() { + if (!tickState.isAborted) { + obj[completeMethod](); + } + }, + onChange: function() { + // we do not want to animate a selection, only cursor + if (obj.canvas && obj.selectionStart === obj.selectionEnd) { + obj.renderCursorOrSelection(); + } + }, + abort: function() { + return tickState.isAborted; + } + }); + return tickState; + }, + + /** + * @private + */ + _onTickComplete: function() { + + var _this = this; + + if (this._cursorTimeout1) { + clearTimeout(this._cursorTimeout1); + } + this._cursorTimeout1 = setTimeout(function() { + _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick'); + }, 100); + }, + + /** + * Initializes delayed cursor + */ + initDelayedCursor: function(restart) { + var _this = this, + delay = restart ? 0 : this.cursorDelay; + + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this._cursorTimeout2 = setTimeout(function() { + _this._tick(); + }, delay); + }, + + /** + * Aborts cursor animation and clears all timeouts + */ + abortCursorAnimation: function() { + var shouldClear = this._currentTickState || this._currentTickCompleteState; + this._currentTickState && this._currentTickState.abort(); + this._currentTickCompleteState && this._currentTickCompleteState.abort(); + + clearTimeout(this._cursorTimeout1); + clearTimeout(this._cursorTimeout2); + + this._currentCursorOpacity = 0; + // to clear just itext area we need to transform the context + // it may not be worth it + if (shouldClear) { + this.canvas && this.canvas.clearContext(this.canvas.contextTop || this.ctx); + } + + }, + + /** + * Selects entire text + */ + selectAll: function() { + this.selectionStart = 0; + this.selectionEnd = this.text.length; + this._fireSelectionChanged(); + this._updateTextarea(); + }, + + /** + * Returns selected text + * @return {String} + */ + getSelectedText: function() { + return this.text.slice(this.selectionStart, this.selectionEnd); + }, + + /** + * Find new selection index representing start of current word according to current selection index + * @param {Number} startFrom Surrent selection index + * @return {Number} New selection index + */ + findWordBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + // remove space before cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index--; + } + } + while (/\S/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current word according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findWordBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + // remove space after cursor first + if (this._reSpace.test(this.text.charAt(index))) { + while (this._reSpace.test(this.text.charAt(index))) { + offset++; + index++; + } + } + while (/\S/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Find new selection index representing start of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryLeft: function(startFrom) { + var offset = 0, index = startFrom - 1; + + while (!/\n/.test(this.text.charAt(index)) && index > -1) { + offset++; + index--; + } + + return startFrom - offset; + }, + + /** + * Find new selection index representing end of current line according to current selection index + * @param {Number} startFrom Current selection index + * @return {Number} New selection index + */ + findLineBoundaryRight: function(startFrom) { + var offset = 0, index = startFrom; + + while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) { + offset++; + index++; + } + + return startFrom + offset; + }, + + /** + * Returns number of newlines in selected text + * @return {Number} Number of newlines in selected text + */ + getNumNewLinesInSelectedText: function() { + var selectedText = this.getSelectedText(), + numNewLines = 0; + + for (var i = 0, len = selectedText.length; i < len; i++) { + if (selectedText[i] === '\n') { + numNewLines++; + } + } + return numNewLines; + }, + + /** + * Finds index corresponding to beginning or end of a word + * @param {Number} selectionStart Index of a character + * @param {Number} direction 1 or -1 + * @return {Number} Index of the beginning or end of a word + */ + searchWordBoundary: function(selectionStart, direction) { + var index = this._reSpace.test(this.text.charAt(selectionStart)) ? selectionStart - 1 : selectionStart, + _char = this.text.charAt(index), + reNonWord = /[ \n\.,;!\?\-]/; + + while (!reNonWord.test(_char) && index > 0 && index < this.text.length) { + index += direction; + _char = this.text.charAt(index); + } + if (reNonWord.test(_char) && _char !== '\n') { + index += direction === 1 ? 0 : 1; + } + return index; + }, + + /** + * Selects a word based on the index + * @param {Number} selectionStart Index of a character + */ + selectWord: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */ + newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + }, + + /** + * Selects a line based on the index + * @param {Number} selectionStart Index of a character + */ + selectLine: function(selectionStart) { + selectionStart = selectionStart || this.selectionStart; + var newSelectionStart = this.findLineBoundaryLeft(selectionStart), + newSelectionEnd = this.findLineBoundaryRight(selectionStart); + + this.selectionStart = newSelectionStart; + this.selectionEnd = newSelectionEnd; + this._fireSelectionChanged(); + this._updateTextarea(); + }, + + /** + * Enters editing state + * @return {fabric.IText} thisArg + * @chainable + */ + enterEditing: function(e) { + if (this.isEditing || !this.editable) { + return; + } + + if (this.canvas) { + this.exitEditingOnOthers(this.canvas); + } + + this.isEditing = true; + this.selected = true; + this.initHiddenTextarea(e); + this.hiddenTextarea.focus(); + this._updateTextarea(); + this._saveEditingProps(); + this._setEditingProps(); + this._textBeforeEdit = this.text; + + this._tick(); + this.fire('editing:entered'); + this._fireSelectionChanged(); + if (!this.canvas) { + return this; + } + this.canvas.fire('text:editing:entered', { target: this }); + this.initMouseMoveHandler(); + this.canvas.renderAll(); + return this; + }, + + exitEditingOnOthers: function(canvas) { + if (canvas._iTextInstances) { + canvas._iTextInstances.forEach(function(obj) { + obj.selected = false; + if (obj.isEditing) { + obj.exitEditing(); + } + }); + } + }, + + /** + * Initializes "mousemove" event handler + */ + initMouseMoveHandler: function() { + this.canvas.on('mouse:move', this.mouseMoveHandler); + }, + + /** + * @private + */ + mouseMoveHandler: function(options) { + if (!this.__isMousedown || !this.isEditing) { + return; + } + + var newSelectionStart = this.getSelectionStartFromPointer(options.e), + currentStart = this.selectionStart, + currentEnd = this.selectionEnd; + if ( + (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd) + && + (currentStart === newSelectionStart || currentEnd === newSelectionStart) + ) { + return; + } + if (newSelectionStart > this.__selectionStartOnMouseDown) { + this.selectionStart = this.__selectionStartOnMouseDown; + this.selectionEnd = newSelectionStart; + } + else { + this.selectionStart = newSelectionStart; + this.selectionEnd = this.__selectionStartOnMouseDown; + } + if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) { + this.restartCursorIfNeeded(); + this._fireSelectionChanged(); + this._updateTextarea(); + this.renderCursorOrSelection(); + } + }, + + /** + * @private + */ + _setEditingProps: function() { + this.hoverCursor = 'text'; + + if (this.canvas) { + this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; + } + + this.borderColor = this.editingBorderColor; + + this.hasControls = this.selectable = false; + this.lockMovementX = this.lockMovementY = true; + }, + + /** + * @private + */ + _updateTextarea: function() { + if (!this.hiddenTextarea || this.inCompositionMode) { + return; + } + this.cursorOffsetCache = { }; + this.hiddenTextarea.value = this.text; + this.hiddenTextarea.selectionStart = this.selectionStart; + this.hiddenTextarea.selectionEnd = this.selectionEnd; + if (this.selectionStart === this.selectionEnd) { + var style = this._calcTextareaPosition(); + this.hiddenTextarea.style.left = style.left; + this.hiddenTextarea.style.top = style.top; + this.hiddenTextarea.style.fontSize = style.fontSize; + } + }, + + /** + * @private + * @return {Object} style contains style for hiddenTextarea + */ + _calcTextareaPosition: function() { + if (!this.canvas) { + return { x: 1, y: 1 }; + } + var chars = this.text.split(''), + boundaries = this._getCursorBoundaries(chars, 'cursor'), + cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex, + charHeight = this.getCurrentCharFontSize(lineIndex, charIndex), + leftOffset = boundaries.leftOffset, + m = this.calcTransformMatrix(), + p = { + x: boundaries.left + leftOffset, + y: boundaries.top + boundaries.topOffset + charHeight + }, + upperCanvas = this.canvas.upperCanvasEl, + maxWidth = upperCanvas.width - charHeight, + maxHeight = upperCanvas.height - charHeight; + + p = fabric.util.transformPoint(p, m); + p = fabric.util.transformPoint(p, this.canvas.viewportTransform); + + if (p.x < 0) { + p.x = 0; + } + if (p.x > maxWidth) { + p.x = maxWidth; + } + if (p.y < 0) { + p.y = 0; + } + if (p.y > maxHeight) { + p.y = maxHeight; + } + + // add canvas offset on document + p.x += this.canvas._offset.left; + p.y += this.canvas._offset.top; + + return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight }; + }, + + /** + * @private + */ + _saveEditingProps: function() { + this._savedProps = { + hasControls: this.hasControls, + borderColor: this.borderColor, + lockMovementX: this.lockMovementX, + lockMovementY: this.lockMovementY, + hoverCursor: this.hoverCursor, + defaultCursor: this.canvas && this.canvas.defaultCursor, + moveCursor: this.canvas && this.canvas.moveCursor + }; + }, + + /** + * @private + */ + _restoreEditingProps: function() { + if (!this._savedProps) { + return; + } + + this.hoverCursor = this._savedProps.overCursor; + this.hasControls = this._savedProps.hasControls; + this.borderColor = this._savedProps.borderColor; + this.lockMovementX = this._savedProps.lockMovementX; + this.lockMovementY = this._savedProps.lockMovementY; + + if (this.canvas) { + this.canvas.defaultCursor = this._savedProps.defaultCursor; + this.canvas.moveCursor = this._savedProps.moveCursor; + } + }, + + /** + * Exits from editing state + * @return {fabric.IText} thisArg + * @chainable + */ + exitEditing: function() { + var isTextChanged = (this._textBeforeEdit !== this.text); + this.selected = false; + this.isEditing = false; + this.selectable = true; + + this.selectionEnd = this.selectionStart; + + if (this.hiddenTextarea) { + this.hiddenTextarea.blur && this.hiddenTextarea.blur(); + this.canvas && this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea); + this.hiddenTextarea = null; + } + + this.abortCursorAnimation(); + this._restoreEditingProps(); + this._currentCursorOpacity = 0; + + this.fire('editing:exited'); + isTextChanged && this.fire('modified'); + if (this.canvas) { + this.canvas.off('mouse:move', this.mouseMoveHandler); + this.canvas.fire('text:editing:exited', { target: this }); + isTextChanged && this.canvas.fire('object:modified', { target: this }); + } + return this; + }, + + /** + * @private + */ + _removeExtraneousStyles: function() { + for (var prop in this.styles) { + if (!this._textLines[prop]) { + delete this.styles[prop]; + } + } + }, + + /** + * @private + */ + _removeCharsFromTo: function(start, end) { + while (end !== start) { + this._removeSingleCharAndStyle(start + 1); + end--; + } + this.selectionStart = start; + this.selectionEnd = start; + }, + + _removeSingleCharAndStyle: function(index) { + var isBeginningOfLine = this.text[index - 1] === '\n', + indexStyle = isBeginningOfLine ? index : index - 1; + this.removeStyleObject(isBeginningOfLine, indexStyle); + this.text = this.text.slice(0, index - 1) + + this.text.slice(index); + + this._textLines = this._splitTextIntoLines(); + }, + + /** + * Inserts characters where cursor is (replacing selection if one exists) + * @param {String} _chars Characters to insert + * @param {Boolean} useCopiedStyle use fabric.copiedTextStyle + */ + insertChars: function(_chars, useCopiedStyle) { + var style; + + if (this.selectionEnd - this.selectionStart > 1) { + this._removeCharsFromTo(this.selectionStart, this.selectionEnd); + } + //short circuit for block paste + if (!useCopiedStyle && this.isEmptyStyles()) { + this.insertChar(_chars, false); + return; + } + for (var i = 0, len = _chars.length; i < len; i++) { + if (useCopiedStyle) { + style = fabric.util.object.clone(fabric.copiedTextStyle[i], true); + } + this.insertChar(_chars[i], i < len - 1, style); + } + }, + + /** + * Inserts a character where cursor is + * @param {String} _char Characters to insert + * @param {Boolean} skipUpdate trigger rendering and updates at the end of text insert + * @param {Object} styleObject Style to be inserted for the new char + */ + insertChar: function(_char, skipUpdate, styleObject) { + var isEndOfLine = this.text[this.selectionStart] === '\n'; + this.text = this.text.slice(0, this.selectionStart) + + _char + this.text.slice(this.selectionEnd); + this._textLines = this._splitTextIntoLines(); + this.insertStyleObjects(_char, isEndOfLine, styleObject); + this.selectionStart += _char.length; + this.selectionEnd = this.selectionStart; + if (skipUpdate) { + return; + } + this._updateTextarea(); + this.setCoords(); + this._fireSelectionChanged(); + this.fire('changed'); + this.restartCursorIfNeeded(); + if (this.canvas) { + this.canvas.fire('text:changed', { target: this }); + this.canvas.renderAll(); + } + }, + + restartCursorIfNeeded: function() { + if (!this._currentTickState || this._currentTickState.isAborted + || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted + ) { + this.initDelayedCursor(); + } + }, + + /** + * Inserts new style object + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { + + this.shiftLineStyles(lineIndex, +1); + + var currentCharStyle = {}, + newLineStyles = {}; + + if (this.styles[lineIndex] && this.styles[lineIndex][charIndex - 1]) { + currentCharStyle = this.styles[lineIndex][charIndex - 1]; + } + + // if there's nothing after cursor, + // we clone current char style onto the next (otherwise empty) line + if (isEndOfLine && currentCharStyle) { + newLineStyles[0] = clone(currentCharStyle); + this.styles[lineIndex + 1] = newLineStyles; + } + // otherwise we clone styles of all chars + // after cursor onto the next line, from the beginning + else { + var somethingAdded = false; + for (var index in this.styles[lineIndex]) { + var numIndex = parseInt(index, 10); + if (numIndex >= charIndex) { + somethingAdded = true; + newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index]; + // remove lines from the previous line since they're on a new line now + delete this.styles[lineIndex][index]; + } + } + somethingAdded && (this.styles[lineIndex + 1] = newLineStyles); + } + this._forceClearCache = true; + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Object} [style] Style object to insert, if given + */ + insertCharStyleObject: function(lineIndex, charIndex, style) { + + var currentLineStyles = this.styles[lineIndex], + currentLineStylesCloned = clone(currentLineStyles); + + if (charIndex === 0 && !style) { + charIndex = 1; + } + + // shift all char styles by 1 forward + // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 + for (var index in currentLineStylesCloned) { + var numericIndex = parseInt(index, 10); + + if (numericIndex >= charIndex) { + currentLineStyles[numericIndex + 1] = currentLineStylesCloned[numericIndex]; + + // only delete the style if there was nothing moved there + if (!currentLineStylesCloned[numericIndex - 1]) { + delete currentLineStyles[numericIndex]; + } + } + } + var newStyle = style || clone(currentLineStyles[charIndex - 1]); + newStyle && (this.styles[lineIndex][charIndex] = newStyle); + this._forceClearCache = true; + }, + + /** + * Inserts style object(s) + * @param {String} _chars Characters at the location where style is inserted + * @param {Boolean} isEndOfLine True if it's end of line + * @param {Object} [styleObject] Style to insert + */ + insertStyleObjects: function(_chars, isEndOfLine, styleObject) { + // removed shortcircuit over isEmptyStyles + + var cursorLocation = this.get2DCursorLocation(), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + if (!this._getLineStyle(lineIndex)) { + this._setLineStyle(lineIndex, {}); + } + + if (_chars === '\n') { + this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); + } + else { + this.insertCharStyleObject(lineIndex, charIndex, styleObject); + } + }, + + /** + * Shifts line styles up or down + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can be -1 or +1 + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by 1 upward or downward + var clonedStyles = clone(this.styles); + for (var line in clonedStyles) { + var numericLine = parseInt(line, 10); + if (numericLine <= lineIndex) { + delete clonedStyles[numericLine]; + } + } + for (var line in this.styles) { + var numericLine = parseInt(line, 10); + if (numericLine > lineIndex) { + this.styles[numericLine + offset] = clonedStyles[numericLine]; + if (!clonedStyles[numericLine - offset]) { + delete this.styles[numericLine]; + } + } + } + //TODO: evaluate if delete old style lines with offset -1 + }, + + /** + * Removes style object + * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line + * @param {Number} [index] Optional index. When not given, current selectionStart is used. + */ + removeStyleObject: function(isBeginningOfLine, index) { + + var cursorLocation = this.get2DCursorLocation(index), + lineIndex = cursorLocation.lineIndex, + charIndex = cursorLocation.charIndex; + + this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex); + }, + + _getTextOnPreviousLine: function(lIndex) { + return this._textLines[lIndex - 1]; + }, + + _removeStyleObject: function(isBeginningOfLine, cursorLocation, lineIndex, charIndex) { + + if (isBeginningOfLine) { + var textOnPreviousLine = this._getTextOnPreviousLine(cursorLocation.lineIndex), + newCharIndexOnPrevLine = textOnPreviousLine ? textOnPreviousLine.length : 0; + + if (!this.styles[lineIndex - 1]) { + this.styles[lineIndex - 1] = {}; + } + for (charIndex in this.styles[lineIndex]) { + this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine] + = this.styles[lineIndex][charIndex]; + } + this.shiftLineStyles(cursorLocation.lineIndex, -1); + } + else { + var currentLineStyles = this.styles[lineIndex]; + + if (currentLineStyles) { + delete currentLineStyles[charIndex]; + } + var currentLineStylesCloned = clone(currentLineStyles); + // shift all styles by 1 backwards + for (var i in currentLineStylesCloned) { + var numericIndex = parseInt(i, 10); + if (numericIndex >= charIndex && numericIndex !== 0) { + currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex]; + delete currentLineStyles[numericIndex]; + } + } + } + }, + + /** + * Inserts new line + */ + insertNewline: function() { + this.insertChars('\n'); + }, + + /** + * Set the selectionStart and selectionEnd according to the ne postion of cursor + * mimic the key - mouse navigation when shift is pressed. + */ + setSelectionStartEndWithShift: function(start, end, newSelection) { + if (newSelection <= start) { + if (end === start) { + this._selectionDirection = 'left'; + } + else if (this._selectionDirection === 'right') { + this._selectionDirection = 'left'; + this.selectionEnd = start; + } + this.selectionStart = newSelection; + } + else if (newSelection > start && newSelection < end) { + if (this._selectionDirection === 'right') { + this.selectionEnd = newSelection; + } + else { + this.selectionStart = newSelection; + } + } + else { + // newSelection is > selection start and end + if (end === start) { + this._selectionDirection = 'right'; + } + else if (this._selectionDirection === 'left') { + this._selectionDirection = 'right'; + this.selectionStart = end; + } + this.selectionEnd = newSelection; + } + }, + + setSelectionInBoundaries: function() { + var length = this.text.length; + if (this.selectionStart > length) { + this.selectionStart = length; + } + else if (this.selectionStart < 0) { + this.selectionStart = 0; + } + if (this.selectionEnd > length) { + this.selectionEnd = length; + } + else if (this.selectionEnd < 0) { + this.selectionEnd = 0; + } + } + }); +})(); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + /** + * Initializes "dbclick" event handler + */ + initDoubleClickSimulation: function() { + + // for double click + this.__lastClickTime = +new Date(); + + // for triple click + this.__lastLastClickTime = +new Date(); + + this.__lastPointer = { }; + + this.on('mousedown', this.onMouseDown.bind(this)); + }, + + onMouseDown: function(options) { + + this.__newClickTime = +new Date(); + var newPointer = this.canvas.getPointer(options.e); + + if (this.isTripleClick(newPointer)) { + this.fire('tripleclick', options); + this._stopEvent(options.e); + } + else if (this.isDoubleClick(newPointer)) { + this.fire('dblclick', options); + this._stopEvent(options.e); + } + + this.__lastLastClickTime = this.__lastClickTime; + this.__lastClickTime = this.__newClickTime; + this.__lastPointer = newPointer; + this.__lastIsEditing = this.isEditing; + this.__lastSelected = this.selected; + }, + + isDoubleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y && this.__lastIsEditing; + }, + + isTripleClick: function(newPointer) { + return this.__newClickTime - this.__lastClickTime < 500 && + this.__lastClickTime - this.__lastLastClickTime < 500 && + this.__lastPointer.x === newPointer.x && + this.__lastPointer.y === newPointer.y; + }, + + /** + * @private + */ + _stopEvent: function(e) { + e.preventDefault && e.preventDefault(); + e.stopPropagation && e.stopPropagation(); + }, + + /** + * Initializes event handlers related to cursor or selection + */ + initCursorSelectionHandlers: function() { + this.initMousedownHandler(); + this.initMouseupHandler(); + this.initClicks(); + }, + + /** + * Initializes double and triple click event handlers + */ + initClicks: function() { + this.on('dblclick', function(options) { + this.selectWord(this.getSelectionStartFromPointer(options.e)); + }); + this.on('tripleclick', function(options) { + this.selectLine(this.getSelectionStartFromPointer(options.e)); + }); + }, + + /** + * Initializes "mousedown" event handler + */ + initMousedownHandler: function() { + this.on('mousedown', function(options) { + if (!this.editable) { + return; + } + var pointer = this.canvas.getPointer(options.e); + this.__mousedownX = pointer.x; + this.__mousedownY = pointer.y; + this.__isMousedown = true; + + if (this.selected) { + this.setCursorByClick(options.e); + } + + if (this.isEditing) { + this.__selectionStartOnMouseDown = this.selectionStart; + if (this.selectionStart === this.selectionEnd) { + this.abortCursorAnimation(); + } + this.renderCursorOrSelection(); + } + }); + }, + + /** + * @private + */ + _isObjectMoved: function(e) { + var pointer = this.canvas.getPointer(e); + + return this.__mousedownX !== pointer.x || + this.__mousedownY !== pointer.y; + }, + + /** + * Initializes "mouseup" event handler + */ + initMouseupHandler: function() { + this.on('mouseup', function(options) { + this.__isMousedown = false; + if (!this.editable || this._isObjectMoved(options.e)) { + return; + } + + if (this.__lastSelected && !this.__corner) { + this.enterEditing(options.e); + if (this.selectionStart === this.selectionEnd) { + this.initDelayedCursor(true); + } + else { + this.renderCursorOrSelection(); + } + } + this.selected = true; + }); + }, + + /** + * Changes cursor location in a text depending on passed pointer (x/y) object + * @param {Event} e Event object + */ + setCursorByClick: function(e) { + var newSelection = this.getSelectionStartFromPointer(e), + start = this.selectionStart, end = this.selectionEnd; + if (e.shiftKey) { + this.setSelectionStartEndWithShift(start, end, newSelection); + } + else { + this.selectionStart = newSelection; + this.selectionEnd = newSelection; + } + if (this.isEditing) { + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Returns index of a character corresponding to where an object was clicked + * @param {Event} e Event object + * @return {Number} Index of a character + */ + getSelectionStartFromPointer: function(e) { + var mouseOffset = this.getLocalPointer(e), + prevWidth = 0, + width = 0, + height = 0, + charIndex = 0, + newSelectionStart, + line; + + for (var i = 0, len = this._textLines.length; i < len; i++) { + line = this._textLines[i]; + height += this._getHeightOfLine(this.ctx, i) * this.scaleY; + + var widthOfLine = this._getLineWidth(this.ctx, i), + lineLeftOffset = this._getLineLeftOffset(widthOfLine); + + width = lineLeftOffset * this.scaleX; + + for (var j = 0, jlen = line.length; j < jlen; j++) { + + prevWidth = width; + + width += this._getWidthOfChar(this.ctx, line[j], i, this.flipX ? jlen - j : j) * + this.scaleX; + + if (height <= mouseOffset.y || width <= mouseOffset.x) { + charIndex++; + continue; + } + + return this._getNewSelectionStartFromOffset( + mouseOffset, prevWidth, width, charIndex + i, jlen); + } + + if (mouseOffset.y < height) { + //this happens just on end of lines. + return this._getNewSelectionStartFromOffset( + mouseOffset, prevWidth, width, charIndex + i - 1, jlen); + } + } + + // clicked somewhere after all chars, so set at the end + if (typeof newSelectionStart === 'undefined') { + return this.text.length; + } + }, + + /** + * @private + */ + _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { + + var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, + distanceBtwNextCharAndCursor = width - mouseOffset.x, + offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1, + newSelectionStart = index + offset; + + // if object is horizontally flipped, mirror cursor location from the end + if (this.flipX) { + newSelectionStart = jlen - newSelectionStart; + } + + if (newSelectionStart > this.text.length) { + newSelectionStart = this.text.length; + } + + return newSelectionStart; + } +}); + + +fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * Initializes hidden textarea (needed to bring up keyboard in iOS) + */ + initHiddenTextarea: function() { + this.hiddenTextarea = fabric.document.createElement('textarea'); + this.hiddenTextarea.setAttribute('autocapitalize', 'off'); + var style = this._calcTextareaPosition(); + this.hiddenTextarea.style.cssText = 'white-space: nowrap; position: absolute; top: ' + style.top + + '; left: ' + style.left + '; opacity: 0; width: 1px; height: 1px; z-index: -999;'; + fabric.document.body.appendChild(this.hiddenTextarea); + + fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'cut', this.cut.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this)); + fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this)); + + if (!this._clickHandlerInitialized && this.canvas) { + fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this)); + this._clickHandlerInitialized = true; + } + }, + + /** + * @private + */ + _keysMap: { + 8: 'removeChars', + 9: 'exitEditing', + 27: 'exitEditing', + 13: 'insertNewline', + 33: 'moveCursorUp', + 34: 'moveCursorDown', + 35: 'moveCursorRight', + 36: 'moveCursorLeft', + 37: 'moveCursorLeft', + 38: 'moveCursorUp', + 39: 'moveCursorRight', + 40: 'moveCursorDown', + 46: 'forwardDelete' + }, + + /** + * @private + */ + _ctrlKeysMapUp: { + 67: 'copy', + 88: 'cut' + }, + + /** + * @private + */ + _ctrlKeysMapDown: { + 65: 'selectAll' + }, + + onClick: function() { + // No need to trigger click event here, focus is enough to have the keyboard appear on Android + this.hiddenTextarea && this.hiddenTextarea.focus(); + }, + + /** + * Handles keyup event + * @param {Event} e Event object + */ + onKeyDown: function(e) { + if (!this.isEditing) { + return; + } + if (e.keyCode in this._keysMap) { + this[this._keysMap[e.keyCode]](e); + } + else if ((e.keyCode in this._ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) { + this[this._ctrlKeysMapDown[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.keyCode >= 33 && e.keyCode <= 40) { + // if i press an arrow key just update selection + this.clearContextTop(); + this.renderCursorOrSelection(); + } + else { + this.canvas && this.canvas.renderAll(); + } + }, + + /** + * Handles keyup event + * We handle KeyUp because ie11 and edge have difficulties copy/pasting + * if a copy/cut event fired, keyup is dismissed + * @param {Event} e Event object + */ + onKeyUp: function(e) { + if (!this.isEditing || this._copyDone) { + this._copyDone = false; + return; + } + if ((e.keyCode in this._ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) { + this[this._ctrlKeysMapUp[e.keyCode]](e); + } + else { + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + this.canvas && this.canvas.renderAll(); + }, + + /** + * Handles onInput event + * @param {Event} e Event object + */ + onInput: function(e) { + if (!this.isEditing || this.inCompositionMode) { + return; + } + var offset = this.selectionStart || 0, + offsetEnd = this.selectionEnd || 0, + textLength = this.text.length, + newTextLength = this.hiddenTextarea.value.length, + diff, charsToInsert, start; + if (newTextLength > textLength) { + //we added some character + start = this._selectionDirection === 'left' ? offsetEnd : offset; + diff = newTextLength - textLength; + charsToInsert = this.hiddenTextarea.value.slice(start, start + diff); + } + else { + //we selected a portion of text and then input something else. + //Internet explorer does not trigger this else + diff = newTextLength - textLength + offsetEnd - offset; + charsToInsert = this.hiddenTextarea.value.slice(offset, offset + diff); + } + this.insertChars(charsToInsert); + e.stopPropagation(); + }, + + /** + * Composition start + */ + onCompositionStart: function() { + this.inCompositionMode = true; + this.prevCompositionLength = 0; + this.compositionStart = this.selectionStart; + }, + + /** + * Composition end + */ + onCompositionEnd: function() { + this.inCompositionMode = false; + }, + + /** + * Composition update + */ + onCompositionUpdate: function(e) { + var data = e.data; + this.selectionStart = this.compositionStart; + this.selectionEnd = this.selectionEnd === this.selectionStart ? + this.compositionStart + this.prevCompositionLength : this.selectionEnd; + this.insertChars(data, false); + this.prevCompositionLength = data.length; + }, + + /** + * Forward delete + */ + forwardDelete: function(e) { + if (this.selectionStart === this.selectionEnd) { + if (this.selectionStart === this.text.length) { + return; + } + this.moveCursorRight(e); + } + this.removeChars(e); + }, + + /** + * Copies selected text + * @param {Event} e Event object + */ + copy: function(e) { + if (this.selectionStart === this.selectionEnd) { + //do not cut-copy if no selection + return; + } + var selectedText = this.getSelectedText(), + clipboardData = this._getClipboardData(e); + + // Check for backward compatibility with old browsers + if (clipboardData) { + clipboardData.setData('text', selectedText); + } + + fabric.copiedText = selectedText; + fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd); + e.stopImmediatePropagation(); + e.preventDefault(); + this._copyDone = true; + }, + + /** + * Pastes text + * @param {Event} e Event object + */ + paste: function(e) { + var copiedText = null, + clipboardData = this._getClipboardData(e), + useCopiedStyle = true; + + // Check for backward compatibility with old browsers + if (clipboardData) { + copiedText = clipboardData.getData('text').replace(/\r/g, ''); + if (!fabric.copiedTextStyle || fabric.copiedText !== copiedText) { + useCopiedStyle = false; + } + } + else { + copiedText = fabric.copiedText; + } + + if (copiedText) { + this.insertChars(copiedText, useCopiedStyle); + } + e.stopImmediatePropagation(); + e.preventDefault(); + }, + + /** + * Cuts text + * @param {Event} e Event object + */ + cut: function(e) { + if (this.selectionStart === this.selectionEnd) { + return; + } + + this.copy(e); + this.removeChars(e); + }, + + /** + * @private + * @param {Event} e Event object + * @return {Object} Clipboard data object + */ + _getClipboardData: function(e) { + return (e && e.clipboardData) || fabric.window.clipboardData; + }, + + /** + * Finds the width in pixels before the cursor on the same line + * @private + * @param {Number} lineIndex + * @param {Number} charIndex + * @return {Number} widthBeforeCursor width before cursor + */ + _getWidthBeforeCursor: function(lineIndex, charIndex) { + var textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex), + widthOfLine = this._getLineWidth(this.ctx, lineIndex), + widthBeforeCursor = this._getLineLeftOffset(widthOfLine), _char; + + for (var i = 0, len = textBeforeCursor.length; i < len; i++) { + _char = textBeforeCursor[i]; + widthBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); + } + return widthBeforeCursor; + }, + + /** + * Gets start offset of a selection + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getDownCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + // if on last line, down cursor goes to end of line + if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) { + // move to the end of a text + return this.text.length - selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor), + textAfterCursor = this._textLines[lineIndex].slice(charIndex); + + return textAfterCursor.length + indexOnOtherLine + 2; + }, + + /** + * private + * Helps finding if the offset should be counted from Start or End + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + _getSelectionForOffset: function(e, isRight) { + if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) { + return this.selectionEnd; + } + else { + return this.selectionStart; + } + }, + + /** + * @param {Event} e Event object + * @param {Boolean} isRight + * @return {Number} + */ + getUpCursorOffset: function(e, isRight) { + var selectionProp = this._getSelectionForOffset(e, isRight), + cursorLocation = this.get2DCursorLocation(selectionProp), + lineIndex = cursorLocation.lineIndex; + if (lineIndex === 0 || e.metaKey || e.keyCode === 33) { + // if on first line, up cursor goes to start of line + return -selectionProp; + } + var charIndex = cursorLocation.charIndex, + widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), + indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), + textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex); + // return a negative offset + return -this._textLines[lineIndex - 1].length + indexOnOtherLine - textBeforeCursor.length; + }, + + /** + * find for a given width it founds the matching character. + * @private + */ + _getIndexOnLine: function(lineIndex, width) { + + var widthOfLine = this._getLineWidth(this.ctx, lineIndex), + textOnLine = this._textLines[lineIndex], + lineLeftOffset = this._getLineLeftOffset(widthOfLine), + widthOfCharsOnLine = lineLeftOffset, + indexOnLine = 0, + foundMatch; + + for (var j = 0, jlen = textOnLine.length; j < jlen; j++) { + + var _char = textOnLine[j], + widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); + + widthOfCharsOnLine += widthOfChar; + + if (widthOfCharsOnLine > width) { + + foundMatch = true; + + var leftEdge = widthOfCharsOnLine - widthOfChar, + rightEdge = widthOfCharsOnLine, + offsetFromLeftEdge = Math.abs(leftEdge - width), + offsetFromRightEdge = Math.abs(rightEdge - width); + + indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); + + break; + } + } + + // reached end + if (!foundMatch) { + indexOnLine = textOnLine.length - 1; + } + + return indexOnLine; + }, + + + /** + * Moves cursor down + * @param {Event} e Event object + */ + moveCursorDown: function(e) { + if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) { + return; + } + this._moveCursorUpOrDown('Down', e); + }, + + /** + * Moves cursor up + * @param {Event} e Event object + */ + moveCursorUp: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorUpOrDown('Up', e); + }, + + /** + * Moves cursor up or down, fires the events + * @param {String} direction 'Up' or 'Down' + * @param {Event} e Event object + */ + _moveCursorUpOrDown: function(direction, e) { + // getUpCursorOffset + // getDownCursorOffset + var action = 'get' + direction + 'CursorOffset', + offset = this[action](e, this._selectionDirection === 'right'); + if (e.shiftKey) { + this.moveCursorWithShift(offset); + } + else { + this.moveCursorWithoutShift(offset); + } + if (offset !== 0) { + this.setSelectionInBoundaries(); + this.abortCursorAnimation(); + this._currentCursorOpacity = 1; + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor with shift + * @param {Number} offset + */ + moveCursorWithShift: function(offset) { + var newSelection = this._selectionDirection === 'left' + ? this.selectionStart + offset + : this.selectionEnd + offset; + this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection); + return offset !== 0; + }, + + /** + * Moves cursor up without shift + * @param {Number} offset + */ + moveCursorWithoutShift: function(offset) { + if (offset < 0) { + this.selectionStart += offset; + this.selectionEnd = this.selectionStart; + } + else { + this.selectionEnd += offset; + this.selectionStart = this.selectionEnd; + } + return offset !== 0; + }, + + /** + * Moves cursor left + * @param {Event} e Event object + */ + moveCursorLeft: function(e) { + if (this.selectionStart === 0 && this.selectionEnd === 0) { + return; + } + this._moveCursorLeftOrRight('Left', e); + }, + + /** + * @private + * @return {Boolean} true if a change happened + */ + _move: function(e, prop, direction) { + var newValue; + if (e.altKey) { + newValue = this['findWordBoundary' + direction](this[prop]); + } + else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { + newValue = this['findLineBoundary' + direction](this[prop]); + } + else { + this[prop] += direction === 'Left' ? -1 : 1; + return true; + } + if (typeof newValue !== undefined && this[prop] !== newValue) { + this[prop] = newValue; + return true; + } + }, + + /** + * @private + */ + _moveLeft: function(e, prop) { + return this._move(e, prop, 'Left'); + }, + + /** + * @private + */ + _moveRight: function(e, prop) { + return this._move(e, prop, 'Right'); + }, + + /** + * Moves cursor left without keeping selection + * @param {Event} e + */ + moveCursorLeftWithoutShift: function(e) { + var change = true; + this._selectionDirection = 'left'; + + // only move cursor when there is no selection, + // otherwise we discard it, and leave cursor on same place + if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) { + change = this._moveLeft(e, 'selectionStart'); + + } + this.selectionEnd = this.selectionStart; + return change; + }, + + /** + * Moves cursor left while keeping selection + * @param {Event} e + */ + moveCursorLeftWithShift: function(e) { + if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { + return this._moveLeft(e, 'selectionEnd'); + } + else if (this.selectionStart !== 0){ + this._selectionDirection = 'left'; + return this._moveLeft(e, 'selectionStart'); + } + }, + + /** + * Moves cursor right + * @param {Event} e Event object + */ + moveCursorRight: function(e) { + if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) { + return; + } + this._moveCursorLeftOrRight('Right', e); + }, + + /** + * Moves cursor right or Left, fires event + * @param {String} direction 'Left', 'Right' + * @param {Event} e Event object + */ + _moveCursorLeftOrRight: function(direction, e) { + var actionName = 'moveCursor' + direction + 'With'; + this._currentCursorOpacity = 1; + + if (e.shiftKey) { + actionName += 'Shift'; + } + else { + actionName += 'outShift'; + } + if (this[actionName](e)) { + this.abortCursorAnimation(); + this.initDelayedCursor(); + this._fireSelectionChanged(); + this._updateTextarea(); + } + }, + + /** + * Moves cursor right while keeping selection + * @param {Event} e + */ + moveCursorRightWithShift: function(e) { + if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { + return this._moveRight(e, 'selectionStart'); + } + else if (this.selectionEnd !== this.text.length) { + this._selectionDirection = 'right'; + return this._moveRight(e, 'selectionEnd'); + } + }, + + /** + * Moves cursor right without keeping selection + * @param {Event} e Event object + */ + moveCursorRightWithoutShift: function(e) { + var changed = true; + this._selectionDirection = 'right'; + + if (this.selectionStart === this.selectionEnd) { + changed = this._moveRight(e, 'selectionStart'); + this.selectionEnd = this.selectionStart; + } + else { + this.selectionStart = this.selectionEnd; + } + return changed; + }, + + /** + * Removes characters selected by selection + * @param {Event} e Event object + */ + removeChars: function(e) { + if (this.selectionStart === this.selectionEnd) { + this._removeCharsNearCursor(e); + } + else { + this._removeCharsFromTo(this.selectionStart, this.selectionEnd); + } + + this.set('dirty', true); + this.setSelectionEnd(this.selectionStart); + + this._removeExtraneousStyles(); + + this.canvas && this.canvas.renderAll(); + + this.setCoords(); + this.fire('changed'); + this.canvas && this.canvas.fire('text:changed', { target: this }); + }, + + /** + * @private + * @param {Event} e Event object + */ + _removeCharsNearCursor: function(e) { + if (this.selectionStart === 0) { + return; + } + if (e.metaKey) { + // remove all till the start of current line + var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftLineBoundary, this.selectionStart); + this.setSelectionStart(leftLineBoundary); + } + else if (e.altKey) { + // remove all till the start of current word + var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart); + + this._removeCharsFromTo(leftWordBoundary, this.selectionStart); + this.setSelectionStart(leftWordBoundary); + } + else { + this._removeSingleCharAndStyle(this.selectionStart); + this.setSelectionStart(this.selectionStart - 1); + } + } +}); + + +/* _TO_SVG_START_ */ +(function() { + var toFixed = fabric.util.toFixed, + NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { + + /** + * @private + */ + _setSVGTextLineText: function(lineIndex, textSpans, height, textLeftOffset, textTopOffset, textBgRects) { + if (!this._getLineStyle(lineIndex)) { + fabric.Text.prototype._setSVGTextLineText.call(this, + lineIndex, textSpans, height, textLeftOffset, textTopOffset); + } + else { + this._setSVGTextLineChars( + lineIndex, textSpans, height, textLeftOffset, textBgRects); + } + }, + + /** + * @private + */ + _setSVGTextLineChars: function(lineIndex, textSpans, height, textLeftOffset, textBgRects) { + + var chars = this._textLines[lineIndex], + charOffset = 0, + lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)) - this.width / 2, + lineOffset = this._getSVGLineTopOffset(lineIndex), + heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); + + for (var i = 0, len = chars.length; i < len; i++) { + var styleDecl = this._getStyleDeclaration(lineIndex, i) || { }; + + textSpans.push( + this._createTextCharSpan( + chars[i], styleDecl, lineLeftOffset, lineOffset.lineTop + lineOffset.offset, charOffset)); + + var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); + + if (styleDecl.textBackgroundColor) { + textBgRects.push( + this._createTextCharBg( + styleDecl, lineLeftOffset, lineOffset.lineTop, heightOfLine, charWidth, charOffset)); + } + + charOffset += charWidth; + } + }, + + /** + * @private + */ + _getSVGLineTopOffset: function(lineIndex) { + var lineTopOffset = 0, lastHeight = 0; + for (var j = 0; j < lineIndex; j++) { + lineTopOffset += this._getHeightOfLine(this.ctx, j); + } + lastHeight = this._getHeightOfLine(this.ctx, j); + return { + lineTop: lineTopOffset, + offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) + }; + }, + + /** + * @private + */ + _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) { + return [ + '\t\t\n' + ].join(''); + }, + + /** + * @private + */ + _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, charOffset) { + + var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ + visible: true, + fill: this.fill, + stroke: this.stroke, + type: 'text', + getSvgFilter: fabric.Object.prototype.getSvgFilter + }, styleDecl)); + + return [ + '\t\t\t', + fabric.util.string.escapeXml(_char), + '\n' + ].join(''); + } + }); +})(); +/* _TO_SVG_END_ */ + + +(function(global) { + + 'use strict'; + + var fabric = global.fabric || (global.fabric = {}); + + /** + * Textbox class, based on IText, allows the user to resize the text rectangle + * and wraps lines automatically. Textboxes have their Y scaling locked, the + * user can only change width. Height is adjusted automatically based on the + * wrapping of lines. + * @class fabric.Textbox + * @extends fabric.IText + * @mixes fabric.Observable + * @return {fabric.Textbox} thisArg + * @see {@link fabric.Textbox#initialize} for constructor definition + */ + fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, { + + /** + * Type of an object + * @type String + * @default + */ + type: 'textbox', + + /** + * Minimum width of textbox, in pixels. + * @type Number + * @default + */ + minWidth: 20, + + /** + * Minimum calculated width of a textbox, in pixels. + * fixed to 2 so that an empty textbox cannot go to 0 + * and is still selectable without text. + * @type Number + * @default + */ + dynamicMinWidth: 2, + + /** + * Cached array of text wrapping. + * @type Array + */ + __cachedLines: null, + + /** + * Override standard Object class values + */ + lockScalingY: true, + + /** + * Override standard Object class values + */ + lockScalingFlip: true, + + /** + * Override standard Object class values + * Textbox needs this on false + */ + noScaleCache: false, + + /** + * Constructor. Some scaling related property values are forced. Visibility + * of controls is also fixed; only the rotation and width controls are + * made available. + * @param {String} text Text string + * @param {Object} [options] Options object + * @return {fabric.Textbox} thisArg + */ + initialize: function(text, options) { + + this.callSuper('initialize', text, options); + this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); + this.ctx = this.objectCaching ? this._cacheContext : fabric.util.createCanvasElement().getContext('2d'); + // add width to this list of props that effect line wrapping. + this._dimensionAffectingProps.push('width'); + }, + + /** + * Unlike superclass's version of this function, Textbox does not update + * its width. + * @param {CanvasRenderingContext2D} ctx Context to use for measurements + * @private + * @override + */ + _initDimensions: function(ctx) { + if (this.__skipDimension) { + return; + } + + if (!ctx) { + ctx = fabric.util.createCanvasElement().getContext('2d'); + this._setTextStyles(ctx); + this.clearContextTop(); + } + + // clear dynamicMinWidth as it will be different after we re-wrap line + this.dynamicMinWidth = 0; + + // wrap lines + this._textLines = this._splitTextIntoLines(ctx); + // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap + if (this.dynamicMinWidth > this.width) { + this._set('width', this.dynamicMinWidth); + } + + // clear cache and re-calculate height + this._clearCache(); + this.height = this._getTextHeight(ctx); + }, + + /** + * Generate an object that translates the style object so that it is + * broken up by visual lines (new lines and automatic wrapping). + * The original text styles object is broken up by actual lines (new lines only), + * which is only sufficient for Text / IText + * @private + */ + _generateStyleMap: function() { + var realLineCount = 0, + realLineCharCount = 0, + charCount = 0, + map = {}; + + for (var i = 0; i < this._textLines.length; i++) { + if (this.text[charCount] === '\n' && i > 0) { + realLineCharCount = 0; + charCount++; + realLineCount++; + } + else if (this.text[charCount] === ' ' && i > 0) { + // this case deals with space's that are removed from end of lines when wrapping + realLineCharCount++; + charCount++; + } + + map[i] = { line: realLineCount, offset: realLineCharCount }; + + charCount += this._textLines[i].length; + realLineCharCount += this._textLines[i].length; + } + + return map; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Boolean} [returnCloneOrEmpty=false] + * @private + */ + _getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) { + if (this._styleMap) { + var map = this._styleMap[lineIndex]; + if (!map) { + return returnCloneOrEmpty ? { } : null; + } + lineIndex = map.line; + charIndex = map.offset + charIndex; + } + return this.callSuper('_getStyleDeclaration', lineIndex, charIndex, returnCloneOrEmpty); + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @param {Object} style + * @private + */ + _setStyleDeclaration: function(lineIndex, charIndex, style) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + + this.styles[lineIndex][charIndex] = style; + }, + + /** + * @param {Number} lineIndex + * @param {Number} charIndex + * @private + */ + _deleteStyleDeclaration: function(lineIndex, charIndex) { + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + + delete this.styles[lineIndex][charIndex]; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _getLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + return this.styles[map.line]; + }, + + /** + * @param {Number} lineIndex + * @param {Object} style + * @private + */ + _setLineStyle: function(lineIndex, style) { + var map = this._styleMap[lineIndex]; + this.styles[map.line] = style; + }, + + /** + * @param {Number} lineIndex + * @private + */ + _deleteLineStyle: function(lineIndex) { + var map = this._styleMap[lineIndex]; + delete this.styles[map.line]; + }, + + /** + * Wraps text using the 'width' property of Textbox. First this function + * splits text on newlines, so we preserve newlines entered by the user. + * Then it wraps each line using the width of the Textbox by calling + * _wrapLine(). + * @param {CanvasRenderingContext2D} ctx Context to use for measurements + * @param {String} text The string of text that is split into lines + * @returns {Array} Array of lines + */ + _wrapText: function(ctx, text) { + var lines = text.split(this._reNewline), wrapped = [], i; + + for (i = 0; i < lines.length; i++) { + wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i)); + } + + return wrapped; + }, + + /** + * Helper function to measure a string of text, given its lineIndex and charIndex offset + * + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {number} lineIndex + * @param {number} charOffset + * @returns {number} + * @private + */ + _measureText: function(ctx, text, lineIndex, charOffset) { + var width = 0; + charOffset = charOffset || 0; + for (var i = 0, len = text.length; i < len; i++) { + width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset); + } + return width; + }, + + /** + * Wraps a line of text using the width of the Textbox and a context. + * @param {CanvasRenderingContext2D} ctx Context to use for measurements + * @param {String} text The string of text to split into lines + * @param {Number} lineIndex + * @returns {Array} Array of line(s) into which the given text is wrapped + * to. + */ + _wrapLine: function(ctx, text, lineIndex) { + var lineWidth = 0, + lines = [], + line = '', + words = text.split(' '), + word = '', + offset = 0, + infix = ' ', + wordWidth = 0, + infixWidth = 0, + largestWordWidth = 0, + lineJustStarted = true, + additionalSpace = this._getWidthOfCharSpacing(); + + for (var i = 0; i < words.length; i++) { + word = words[i]; + wordWidth = this._measureText(ctx, word, lineIndex, offset); + + offset += word.length; + + lineWidth += infixWidth + wordWidth - additionalSpace; + + if (lineWidth >= this.width && !lineJustStarted) { + lines.push(line); + line = ''; + lineWidth = wordWidth; + lineJustStarted = true; + } + else { + lineWidth += additionalSpace; + } + + if (!lineJustStarted) { + line += infix; + } + line += word; + + infixWidth = this._measureText(ctx, infix, lineIndex, offset); + offset++; + lineJustStarted = false; + // keep track of largest word + if (wordWidth > largestWordWidth) { + largestWordWidth = wordWidth; + } + } + + i && lines.push(line); + + if (largestWordWidth > this.dynamicMinWidth) { + this.dynamicMinWidth = largestWordWidth - additionalSpace; + } + + return lines; + }, + /** + * Gets lines of text to render in the Textbox. This function calculates + * text wrapping on the fly everytime it is called. + * @returns {Array} Array of lines in the Textbox. + * @override + */ + _splitTextIntoLines: function(ctx) { + ctx = ctx || this.ctx; + var originalAlign = this.textAlign; + this._styleMap = null; + ctx.save(); + this._setTextStyles(ctx); + this.textAlign = 'left'; + var lines = this._wrapText(ctx, this.text); + this.textAlign = originalAlign; + ctx.restore(); + this._textLines = lines; + this._styleMap = this._generateStyleMap(); + return lines; + }, + + /** + * When part of a group, we don't want the Textbox's scale to increase if + * the group's increases. That's why we reduce the scale of the Textbox by + * the amount that the group's increases. This is to maintain the effective + * scale of the Textbox at 1, so that font-size values make sense. Otherwise + * the same font-size value would result in different actual size depending + * on the value of the scale. + * @param {String} key + * @param {*} value + */ + setOnGroup: function(key, value) { + if (key === 'scaleX') { + this.set('scaleX', Math.abs(1 / value)); + this.set('width', (this.get('width') * value) / + (typeof this.__oldScaleX === 'undefined' ? 1 : this.__oldScaleX)); + this.__oldScaleX = value; + } + }, + + /** + * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start). + * Overrides the superclass function to take into account text wrapping. + * + * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. + */ + get2DCursorLocation: function(selectionStart) { + if (typeof selectionStart === 'undefined') { + selectionStart = this.selectionStart; + } + + var numLines = this._textLines.length, + removed = 0; + + for (var i = 0; i < numLines; i++) { + var line = this._textLines[i], + lineLen = line.length; + + if (selectionStart <= removed + lineLen) { + return { + lineIndex: i, + charIndex: selectionStart - removed + }; + } + + removed += lineLen; + + if (this.text[removed] === '\n' || this.text[removed] === ' ') { + removed++; + } + } + + return { + lineIndex: numLines - 1, + charIndex: this._textLines[numLines - 1].length + }; + }, + + /** + * Overrides superclass function and uses text wrapping data to get cursor + * boundary offsets instead of the array of chars. + * @param {Array} chars Unused + * @param {String} typeOfBoundaries Can be 'cursor' or 'selection' + * @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set. + */ + _getCursorBoundariesOffsets: function(chars, typeOfBoundaries) { + var topOffset = 0, + leftOffset = 0, + cursorLocation = this.get2DCursorLocation(), + lineChars = this._textLines[cursorLocation.lineIndex].split(''), + lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, cursorLocation.lineIndex)); + + for (var i = 0; i < cursorLocation.charIndex; i++) { + leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i); + } + + for (i = 0; i < cursorLocation.lineIndex; i++) { + topOffset += this._getHeightOfLine(this.ctx, i); + } + + if (typeOfBoundaries === 'cursor') { + topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex) + / this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex) + * (1 - this._fontSizeFraction); + } + + return { + top: topOffset, + left: leftOffset, + lineLeft: lineLeftOffset + }; + }, + + getMinWidth: function() { + return Math.max(this.minWidth, this.dynamicMinWidth); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject: function(propertiesToInclude) { + return this.callSuper('toObject', ['minWidth'].concat(propertiesToInclude)); + } + }); + + /** + * Returns fabric.Textbox instance from an object representation + * @static + * @memberOf fabric.Textbox + * @param {Object} object Object to create an instance from + * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created + * @param {Boolean} [forceAsync] Force an async behaviour trying to create pattern first + * @return {fabric.Textbox} instance of fabric.Textbox + */ + fabric.Textbox.fromObject = function(object, callback, forceAsync) { + return fabric.Object._fromObject('Textbox', object, callback, forceAsync, 'text'); + }; + + /** + * Returns the default controls visibility required for Textboxes. + * @returns {Object} + */ + fabric.Textbox.getTextboxControlVisibility = function() { + return { + tl: false, + tr: false, + br: false, + bl: false, + ml: true, + mt: false, + mr: true, + mb: false, + mtr: true + }; + }; + +})(typeof exports !== 'undefined' ? exports : this); + + +(function() { + + /** + * Override _setObjectScale and add Textbox specific resizing behavior. Resizing + * a Textbox doesn't scale text, it only changes width and makes text wrap automatically. + */ + var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale; + + fabric.Canvas.prototype._setObjectScale = function(localMouse, transform, + lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { + + var t = transform.target; + if (t instanceof fabric.Textbox) { + var w = t.width * ((localMouse.x / transform.scaleX) / (t.width + t.strokeWidth)); + if (w >= t.getMinWidth()) { + t.set('width', w); + return true; + } + } + else { + return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, + lockScalingX, lockScalingY, by, lockScalingFlip, _dim); + } + }; + + /** + * Sets controls of this group to the Textbox's special configuration if + * one is present in the group. Deletes _controlsVisibility otherwise, so that + * it gets initialized to default value at runtime. + */ + fabric.Group.prototype._refreshControlsVisibility = function() { + if (typeof fabric.Textbox === 'undefined') { + return; + } + for (var i = this._objects.length; i--;) { + if (this._objects[i] instanceof fabric.Textbox) { + this.setControlsVisibility(fabric.Textbox.getTextboxControlVisibility()); + return; + } + } + }; + + fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.IText.prototype */ { + /** + * @private + */ + _removeExtraneousStyles: function() { + for (var prop in this._styleMap) { + if (!this._textLines[prop]) { + delete this.styles[this._styleMap[prop].line]; + } + } + }, + + /** + * Inserts style object for a given line/char index + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Object} [style] Style object to insert, if given + */ + insertCharStyleObject: function(lineIndex, charIndex, style) { + // adjust lineIndex and charIndex + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + + fabric.IText.prototype.insertCharStyleObject.apply(this, [lineIndex, charIndex, style]); + }, + + /** + * Inserts new style object + * @param {Number} lineIndex Index of a line + * @param {Number} charIndex Index of a char + * @param {Boolean} isEndOfLine True if it's end of line + */ + insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { + // adjust lineIndex and charIndex + var map = this._styleMap[lineIndex]; + lineIndex = map.line; + charIndex = map.offset + charIndex; + + fabric.IText.prototype.insertNewlineStyleObject.apply(this, [lineIndex, charIndex, isEndOfLine]); + }, + + /** + * Shifts line styles up or down. This function is slightly different than the one in + * itext_behaviour as it takes into account the styleMap. + * + * @param {Number} lineIndex Index of a line + * @param {Number} offset Can be -1 or +1 + */ + shiftLineStyles: function(lineIndex, offset) { + // shift all line styles by 1 upward + var map = this._styleMap[lineIndex]; + // adjust line index + lineIndex = map.line; + fabric.IText.prototype.shiftLineStyles.call(this, lineIndex, offset); + }, + + /** + * Figure out programatically the text on previous actual line (actual = separated by \n); + * + * @param {Number} lIndex + * @returns {String} + * @private + */ + _getTextOnPreviousLine: function(lIndex) { + var textOnPreviousLine = this._textLines[lIndex - 1]; + + while (this._styleMap[lIndex - 2] && this._styleMap[lIndex - 2].line === this._styleMap[lIndex - 1].line) { + textOnPreviousLine = this._textLines[lIndex - 2] + textOnPreviousLine; + + lIndex--; + } + + return textOnPreviousLine; + }, + + /** + * Removes style object + * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line + * @param {Number} [index] Optional index. When not given, current selectionStart is used. + */ + removeStyleObject: function(isBeginningOfLine, index) { + + var cursorLocation = this.get2DCursorLocation(index), + map = this._styleMap[cursorLocation.lineIndex], + lineIndex = map.line, + charIndex = map.offset + cursorLocation.charIndex; + this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex); + } + }); +})(); + + +(function() { + var override = fabric.IText.prototype._getNewSelectionStartFromOffset; + /** + * Overrides the IText implementation and adjusts character index as there is not always a linebreak + * + * @param {Number} mouseOffset + * @param {Number} prevWidth + * @param {Number} width + * @param {Number} index + * @param {Number} jlen + * @returns {Number} + */ + fabric.IText.prototype._getNewSelectionStartFromOffset = function(mouseOffset, prevWidth, width, index, jlen) { + index = override.call(this, mouseOffset, prevWidth, width, index, jlen); + + // the index passed into the function is padded by the amount of lines from _textLines (to account for \n) + // we need to remove this padding, and pad it by actual lines, and / or spaces that are meant to be there + var tmp = 0, + removed = 0; + + // account for removed characters + for (var i = 0; i < this._textLines.length; i++) { + tmp += this._textLines[i].length; + + if (tmp + removed >= index) { + break; + } + + if (this.text[tmp + removed] === '\n' || this.text[tmp + removed] === ' ') { + removed++; + } + } + + return index - i + removed; + }; +})(); + + +(function() { + + if (typeof document !== 'undefined' && typeof window !== 'undefined') { + return; + } + + var DOMParser = require('xmldom').DOMParser, + URL = require('url'), + HTTP = require('http'), + HTTPS = require('https'), + + Canvas = require('canvas'), + Image = require('canvas').Image; + + /** @private */ + function request(url, encoding, callback) { + var oURL = URL.parse(url); + + // detect if http or https is used + if ( !oURL.port ) { + oURL.port = ( oURL.protocol.indexOf('https:') === 0 ) ? 443 : 80; + } + + // assign request handler based on protocol + var reqHandler = (oURL.protocol.indexOf('https:') === 0 ) ? HTTPS : HTTP, + req = reqHandler.request({ + hostname: oURL.hostname, + port: oURL.port, + path: oURL.path, + method: 'GET' + }, function(response) { + var body = ''; + if (encoding) { + response.setEncoding(encoding); + } + response.on('end', function () { + callback(body); + }); + response.on('data', function (chunk) { + if (response.statusCode === 200) { + body += chunk; + } + }); + }); + + req.on('error', function(err) { + if (err.errno === process.ECONNREFUSED) { + fabric.log('ECONNREFUSED: connection refused to ' + oURL.hostname + ':' + oURL.port); + } + else { + fabric.log(err.message); + } + callback(null); + }); + + req.end(); + } + + /** @private */ + function requestFs(path, callback) { + var fs = require('fs'); + fs.readFile(path, function (err, data) { + if (err) { + fabric.log(err); + throw err; + } + else { + callback(data); + } + }); + } + + fabric.util.loadImage = function(url, callback, context) { + function createImageAndCallBack(data) { + if (data) { + img.src = new Buffer(data, 'binary'); + // preserving original url, which seems to be lost in node-canvas + img._src = url; + callback && callback.call(context, img); + } + else { + img = null; + callback && callback.call(context, null, true); + } + } + var img = new Image(); + if (url && (url instanceof Buffer || url.indexOf('data') === 0)) { + img.src = img._src = url; + callback && callback.call(context, img); + } + else if (url && url.indexOf('http') !== 0) { + requestFs(url, createImageAndCallBack); + } + else if (url) { + request(url, 'binary', createImageAndCallBack); + } + else { + callback && callback.call(context, url); + } + }; + + fabric.loadSVGFromURL = function(url, callback, reviver) { + url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); + if (url.indexOf('http') !== 0) { + requestFs(url, function(body) { + fabric.loadSVGFromString(body.toString(), callback, reviver); + }); + } + else { + request(url, '', function(body) { + fabric.loadSVGFromString(body, callback, reviver); + }); + } + }; + + fabric.loadSVGFromString = function(string, callback, reviver) { + var doc = new DOMParser().parseFromString(string); + fabric.parseSVGDocument(doc.documentElement, function(results, options) { + callback && callback(results, options); + }, reviver); + }; + + fabric.util.getScript = function(url, callback) { + request(url, '', function(body) { + // eslint-disable-next-line no-eval + eval(body); + callback && callback(); + }); + }; + + // fabric.util.createCanvasElement = function(_, width, height) { + // return new Canvas(width, height); + // } + + /** + * Only available when running fabric on node.js + * @param {Number} width Canvas width + * @param {Number} height Canvas height + * @param {Object} [options] Options to pass to FabricCanvas. + * @param {Object} [nodeCanvasOptions] Options to pass to NodeCanvas. + * @return {Object} wrapped canvas instance + */ + fabric.createCanvasForNode = function(width, height, options, nodeCanvasOptions) { + nodeCanvasOptions = nodeCanvasOptions || options; + + var canvasEl = fabric.document.createElement('canvas'), + nodeCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions), + nodeCacheCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions); + + // jsdom doesn't create style on canvas element, so here be temp. workaround + canvasEl.style = { }; + + canvasEl.width = nodeCanvas.width; + canvasEl.height = nodeCanvas.height; + options = options || { }; + options.nodeCanvas = nodeCanvas; + options.nodeCacheCanvas = nodeCacheCanvas; + var FabricCanvas = fabric.Canvas || fabric.StaticCanvas, + fabricCanvas = new FabricCanvas(canvasEl, options); + fabricCanvas.nodeCanvas = nodeCanvas; + fabricCanvas.nodeCacheCanvas = nodeCacheCanvas; + fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); + fabricCanvas.contextCache = nodeCacheCanvas.getContext('2d'); + fabricCanvas.Font = Canvas.Font; + return fabricCanvas; + }; + + var originaInitStatic = fabric.StaticCanvas.prototype._initStatic; + fabric.StaticCanvas.prototype._initStatic = function(el, options) { + el = el || fabric.document.createElement('canvas'); + this.nodeCanvas = new Canvas(el.width, el.height); + this.nodeCacheCanvas = new Canvas(el.width, el.height); + originaInitStatic.call(this, el, options); + this.contextContainer = this.nodeCanvas.getContext('2d'); + this.contextCache = this.nodeCacheCanvas.getContext('2d'); + this.Font = Canvas.Font; + }; + + /** @ignore */ + fabric.StaticCanvas.prototype.createPNGStream = function() { + return this.nodeCanvas.createPNGStream(); + }; + + fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { + return this.nodeCanvas.createJPEGStream(opts); + }; + + fabric.StaticCanvas.prototype._initRetinaScaling = function() { + if (!this._isRetinaScaling()) { + return; + } + + this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio); + this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio); + this.nodeCanvas.width = this.width * fabric.devicePixelRatio; + this.nodeCanvas.height = this.height * fabric.devicePixelRatio; + this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio); + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype._initRetinaScaling = fabric.StaticCanvas.prototype._initRetinaScaling; + } + + var origSetBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension; + fabric.StaticCanvas.prototype._setBackstoreDimension = function(prop, value) { + origSetBackstoreDimension.call(this, prop, value); + this.nodeCanvas[prop] = value; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype._setBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension; + } + +})(); + diff --git a/test/vendor/jquery.js b/test/vendor/jquery.js new file mode 100644 index 0000000..4c5be4c --- /dev/null +++ b/test/vendor/jquery.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("